diff --git a/scripts/test-pwa.mjs b/scripts/test-pwa.mjs index d27a51f..84cd5b7 100644 --- a/scripts/test-pwa.mjs +++ b/scripts/test-pwa.mjs @@ -173,38 +173,287 @@ check('pwa.js tears the worker down in local dev', /getRegistrations\(\)/.test(p // The critical guard: the first-install claim also fires controllerchange, so a // reload must only happen for a user-ACCEPTED update — never on first load. check('pwa.js only reloads on an accepted update', /updateAccepted/.test(pwa) && /!updateAccepted/.test(pwa) && /controllerchange/.test(pwa)); -check('pwa.js never auto-skips waiting without intent', /SKIP_WAITING/.test(pwa) && /updateAccepted\s*=\s*true/.test(pwa)); check('pwa.js exposes a programmatic install trigger', /window\.promptPWAInstall\s*=/.test(pwa)); -// The update toast's Reload, run for real. In a second tab the version is -// usually already active by the time it is clicked (the first tab's Reload -// activated it, and clients.claim() took this tab over without reloading it): -// posting SKIP_WAITING to the active worker changed nothing, and the tab kept -// running the old code. +check('pwa.js snoozes a dismissed install prompt', /pwaInstallDismissedAt/.test(pwa) && /onDismiss/.test(pwa)); +// The composer's icon isn't the turn. The clarify card draws the Send icon +// (updateSendButtonState(false)) while its turn is still running, and that +// released a held update prompt mid-turn, where Reload kills the turn. +{ + const helpers = read('src/js/helpers.js'); + const a = helpers.indexOf('function updateSendButtonState('); + const b = helpers.indexOf('\n/**', a); + const chain = { html: () => chain, toggleClass: () => chain, attr: () => chain, prop: () => chain }; + const $ = (sel) => (sel === '.send' ? chain : { val: () => '' }); + const seen = []; + const win = { pwaTurnChanged: (p) => seen.push(p) }; + const run = (isProcessing, processing) => new Function('$', 'window', 'pause_svg', 'send_svg', 'isProcessing', + helpers.slice(a, b) + '\nreturn updateSendButtonState;')($, win, '', '', isProcessing)(processing); + run(true, false); // the clarify card, mid-turn + run(false, false); // the turn ending + check('helpers.js reports the real turn state to pwa.js, not the icon', seen[0] === true && seen[1] === false); +} + +// sw.js switches quietly only while one page is open. Activating drops the old +// caches, and another tab still on the old bundles may yet need them. { - const a = pwa.indexOf('var updateAccepted = false;'); - const b = pwa.indexOf("window.addEventListener('load'", a); - let toast = null, reloads = 0; + const src = renderServiceWorker(swSrc, { version: 't', precache: [] }); + const listeners = {}; + let skips = 0, pages = [], matchOpts = null; + const self = { + addEventListener: (t, fn) => { listeners[t] = fn; }, + skipWaiting: () => { skips++; return Promise.resolve(); }, + clients: { matchAll: (o) => { matchOpts = o; return Promise.resolve(pages); } }, + }; + new Function('self', src)(self); + const post = async (type, open) => { + pages = new Array(open).fill({}); + let wait = null; + listeners.message({ data: { type }, waitUntil: (p) => { wait = p; } }); + await wait; + }; + await post('SKIP_WAITING_IF_ALONE', 1); + check('sw.js switches quietly when one page is open', skips === 1); + check('sw.js counts every open page, controlled or not', matchOpts && matchOpts.type === 'window' && matchOpts.includeUncontrolled === true); + await post('SKIP_WAITING_IF_ALONE', 2); + check('sw.js keeps waiting while another tab is open', skips === 1); + await post('SKIP_WAITING', 2); + check('sw.js still activates on an accepted update with tabs open', skips === 2); +} + +// --- pwa.js run for real against a fake browser ----------------------------- +// Both toasts used to land seconds after load, right as people went to type. +// The "new version" one was usually false too. Navigations are network-first, +// so a visit after a deploy already runs the new bundles while the browser +// installs the new worker beside them. Now the page compares its bundles with +// the live site's. A current tab switches quietly, only a tab that is behind +// gets the prompt (held while a turn runs), and the install offer waits for +// the first message. +const ORIGIN = 'https://builder.puter.com'; +const NEW = ['app-new.js', 'index-new.css']; +const OLD = ['app-old.js', 'index-old.css']; +const flush = () => new Promise((r) => setImmediate(r)); + +function fakeWorker() { + return { + state: 'installing', posted: [], listeners: {}, + postMessage(m) { this.posted.push(m); }, + addEventListener(t, fn) { (this.listeners[t] = this.listeners[t] || []).push(fn); }, + quiet() { return this.posted.some((m) => m.type === 'SKIP_WAITING_IF_ALONE'); }, + accepted() { return this.posted.some((m) => m.type === 'SKIP_WAITING'); }, + }; +} + +// The