/** * PU-series — Fase 14.1 PWA auto-update toast. * * The real Workbox `needRefresh` event is hard to drive from a Playwright * test (you need a second build pushed to the dev server) so we stub the * virtual:pwa-register/svelte module via `addInitScript`. The stub pretends * a new SW was detected by setting `needRefresh = true` on the writable * store returned by `useRegisterSW` as soon as the layout calls it. * * Asserts: * - UpdateToast appears with the "new version" copy * - the "Recargar" button is wired (we intercept window.location.reload * because Workbox's updateServiceWorker(true) ultimately reloads) * - the onOfflineReady toast also surfaces when its store flips true */ import { test, expect } from '@playwright/test'; import { USERS } from '../fixtures/users.js'; import { loginAs } from '../fixtures/login.js'; test.describe('PWA auto-update toast', () => { test('PU-01: needRefresh = true surfaces the update toast and Recargar invokes updateSW', async ({ page }) => { // Stub the virtual module BEFORE any app code runs. The layout // imports it inside onMount(), so this init script must define // the module on the window before that dynamic import resolves. await page.addInitScript(() => { // Track whether the user clicked "Recargar". (window as unknown as { __pwaUpdated?: boolean }).__pwaUpdated = false; // Minimal Svelte-store-shaped objects: { subscribe, set, update }. // The real store is `writable`. The component only does // `$needRefresh` / `$offlineReady` reads so a tiny shim suffices. const makeStore = (initial: boolean) => { let value = initial; const subs = new Set<(v: boolean) => void>(); return { subscribe(fn: (v: boolean) => void) { subs.add(fn); fn(value); return () => subs.delete(fn); }, set(v: boolean) { value = v; for (const s of subs) s(v); }, update(fn: (v: boolean) => boolean) { this.set(fn(value)); } }; }; const needRefresh = makeStore(false); const offlineReady = makeStore(false); (window as unknown as { __pwaStores?: unknown }).__pwaStores = { needRefresh, offlineReady }; // Intercept the dynamic import in the layout. Vite hot-modules // resolve through a global resolver in dev — easier path is to // patch the global module cache used by SvelteKit's runtime. // We install a property on window that the layout checks first // (`window.__pwaRegisterStub`); if absent, real path runs. (window as unknown as { __pwaRegisterStub?: unknown }).__pwaRegisterStub = () => ({ needRefresh, offlineReady, updateServiceWorker: async () => { (window as unknown as { __pwaUpdated?: boolean }).__pwaUpdated = true; } }); }); await loginAs(page, USERS.borja); await page.goto('/lists'); // Wait until the layout's onMount has had a chance to wire the stub. await page.waitForFunction( () => Boolean((window as unknown as { __pwaStores?: { needRefresh: { set: (v: boolean) => void } } }).__pwaStores), null, { timeout: 10_000 } ); // Flip the needRefresh store; the toast should mount. await page.evaluate(() => { const stores = (window as unknown as { __pwaStores: { needRefresh: { set: (v: boolean) => void } } }) .__pwaStores; stores.needRefresh.set(true); }); const toast = page.getByTestId('pwa-update-toast'); await expect(toast).toBeVisible({ timeout: 5_000 }); // Click "Recargar" — should call updateServiceWorker(true). await page.getByTestId('pwa-update-reload').click(); await page.waitForFunction( () => (window as unknown as { __pwaUpdated?: boolean }).__pwaUpdated === true, null, { timeout: 3_000 } ); }); });