Migrate from `registerSW({ immediate: true })` to `useRegisterSW` from
`virtual:pwa-register/svelte`, mirror its `needRefresh` / `offlineReady`
stores into stable layout-local writables, and render a new
`UpdateToast` pill (bottom-center) that surfaces:
- "New version available" + a "Reload" button that calls
`updateServiceWorker(true)`
- "Ready to use offline" (auto-dismiss after 4s) on first install
Adds `[pwa] …` console.info telemetry on register / need-refresh /
apply. Stand-in for the unavailable `updateViaCache: 'none'` runtime
option (RegisterSWOptions doesn't expose it in vite-plugin-pwa 0.21.2):
poll `reg.update()` every 15min, which keeps a too-aggressively-cached
SW file from pinning users on the previous build for up to 24h.
`apps/web/src/global.d.ts` declares the virtual module's surface inline
so svelte-check picks it up without a relative path into node_modules;
the same file pre-declares the Fase-14.3 `__APP_VERSION__` triplet.
PU-01 stubs the registrar via `window.__pwaRegisterStub` (the layout
checks for it before calling the real `useRegisterSW`) so the toast
flow can be driven from a Playwright test without a second build.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
/**
|
|
* 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<boolean>`. 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 }
|
|
);
|
|
});
|
|
});
|