// @vitest-environment jsdom import { describe, it, expect, beforeEach, vi } from 'vitest'; class FakeSW extends EventTarget { scriptURL = '/service-worker.js'; state: 'installed' | 'activated' = 'activated'; version: string | null; postMessage = vi.fn((msg: unknown, transfer?: Transferable[]) => { if ((msg as { type?: string } | null)?.type === 'GET_VERSION') { const port = transfer?.[0] as MessagePort | undefined; if (port && this.version !== null) port.postMessage({ version: this.version }); } }); constructor(version: string | null = null) { super(); this.version = version; } } type Reg = { active: FakeSW | null; waiting: FakeSW | null; installing: FakeSW | null; addEventListener: ReturnType; update: ReturnType; }; function mountFakeSW(init: Partial): { registration: Reg; container: { swListeners: Record void)[]> }; } { const registration: Reg = { active: init.active ?? null, waiting: init.waiting ?? null, installing: init.installing ?? null, addEventListener: vi.fn(), update: vi.fn().mockResolvedValue(undefined) }; const swListeners: Record void)[]> = {}; Object.defineProperty(navigator, 'serviceWorker', { configurable: true, value: { ready: Promise.resolve(registration), controller: registration.active, addEventListener: vi.fn((type: string, fn: (e: Event) => void) => { (swListeners[type] ??= []).push(fn); }) } }); return { registration, container: { swListeners } }; } async function flush(ms = 20): Promise { await new Promise((r) => setTimeout(r, ms)); } describe('pwa store', () => { beforeEach(() => { vi.resetModules(); }); it('zombie-waiter (gleiche Version): kein Toast, silent SKIP_WAITING', async () => { const active = new FakeSW('1776527907402'); const waiting = new FakeSW('1776527907402'); waiting.state = 'installed'; mountFakeSW({ active, waiting }); const { pwaStore } = await import('../../src/lib/client/pwa.svelte'); await pwaStore.init(); await flush(); expect(pwaStore.updateAvailable).toBe(false); expect(waiting.postMessage).toHaveBeenCalledWith({ type: 'SKIP_WAITING' }); }); it('echtes Update (unterschiedliche Version): Toast', async () => { const active = new FakeSW('1776526292782'); const waiting = new FakeSW('1776527907402'); waiting.state = 'installed'; mountFakeSW({ active, waiting }); const { pwaStore } = await import('../../src/lib/client/pwa.svelte'); await pwaStore.init(); await flush(); expect(pwaStore.updateAvailable).toBe(true); expect(waiting.postMessage).not.toHaveBeenCalledWith({ type: 'SKIP_WAITING' }); }); it('alter active-SW ohne GET_VERSION (Fallback): Toast', async () => { const active = new FakeSW(null); const waiting = new FakeSW('1776527907402'); waiting.state = 'installed'; mountFakeSW({ active, waiting }); const { pwaStore } = await import('../../src/lib/client/pwa.svelte'); await pwaStore.init(); await flush(); expect(pwaStore.updateAvailable).toBe(true); }); it('kein waiting-SW: kein Toast', async () => { mountFakeSW({ active: new FakeSW('1776527907402') }); const { pwaStore } = await import('../../src/lib/client/pwa.svelte'); await pwaStore.init(); await flush(); expect(pwaStore.updateAvailable).toBe(false); }); it('reload() postet SKIP_WAITING, reload erst bei controllerchange', async () => { const active = new FakeSW('v1'); const waiting = new FakeSW('v2'); waiting.state = 'installed'; const { container } = mountFakeSW({ active, waiting }); const reload = vi.fn(); Object.defineProperty(window, 'location', { configurable: true, value: { ...window.location, reload } }); const { pwaStore } = await import('../../src/lib/client/pwa.svelte'); await pwaStore.init(); await flush(); pwaStore.reload(); expect(waiting.postMessage).toHaveBeenCalledWith({ type: 'SKIP_WAITING' }); expect(reload).not.toHaveBeenCalled(); container.swListeners['controllerchange']?.forEach((fn) => fn(new Event('controllerchange'))); expect(reload).toHaveBeenCalledTimes(1); }); it('refreshing-Flag unterdrückt mehrfache Reloads', async () => { const active = new FakeSW('v1'); const waiting = new FakeSW('v2'); waiting.state = 'installed'; const { container } = mountFakeSW({ active, waiting }); const reload = vi.fn(); Object.defineProperty(window, 'location', { configurable: true, value: { ...window.location, reload } }); const { pwaStore } = await import('../../src/lib/client/pwa.svelte'); await pwaStore.init(); await flush(); pwaStore.reload(); const fire = () => container.swListeners['controllerchange']?.forEach((fn) => fn(new Event('controllerchange'))); fire(); fire(); expect(reload).toHaveBeenCalledTimes(1); }); it('reload() ohne waiting-SW ruft location.reload() sofort auf', async () => { mountFakeSW({ active: new FakeSW('v1') }); const reload = vi.fn(); Object.defineProperty(window, 'location', { configurable: true, value: { ...window.location, reload } }); const { pwaStore } = await import('../../src/lib/client/pwa.svelte'); await pwaStore.init(); await flush(); pwaStore.reload(); expect(reload).toHaveBeenCalledTimes(1); }); });