Files
kochwas/tests/unit/pwa-store.test.ts
hsiegeln c2074c9768
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
refactor(pwa): auf Workbox-Standard vereinfacht, refreshing-Flag
Der Zombie-Version-Check (858d4c1) ging über das Standard-Handshake-
Pattern hinaus. User will Industry-Standard: Workbox/web.dev-Pattern
ohne GET_VERSION-Sonderlocke.

Änderungen:
- service-worker.ts: GET_VERSION-Handler entfernt. SW reagiert nur
  noch auf SKIP_WAITING.
- pwa.svelte.ts: queryVersion + evaluateWaiting entfernt. init()
  zeigt Toast wieder schlicht bei registration.waiting (das ist
  kanonisch — bit-gleiche Bytes erzeugen keinen waiting-Slot).
- controllerchange-Listener wandert nach init() mit refreshing-Flag
  (CRA-Idiom): verhindert Doppel-Reload, wenn User zusätzlich F5
  drückt, und stellt sicher, dass der Listener in _jeder_ Session
  aktiv ist, nicht erst nach dem ersten reload()-Call.
- pwa-store.test.ts: Tests decken jetzt waiting→Toast, no-waiting→
  kein Toast, Handshake, refreshing-Flag und Sofort-Reload ab.

Der Zombie-Edge-Case (Browser-Quirk mit bit-identischem waiting-SW)
wird sich nach einmaligem Klick auflösen — erwarteter Trade-off
gegenüber der eingesparten Komplexität.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:57:51 +02:00

145 lines
4.2 KiB
TypeScript

// @vitest-environment jsdom
import { describe, it, expect, beforeEach, vi } from 'vitest';
class FakeSW extends EventTarget {
scriptURL = '/service-worker.js';
state: 'installed' | 'activated' = 'activated';
postMessage = vi.fn();
}
type Reg = {
active: FakeSW | null;
waiting: FakeSW | null;
installing: FakeSW | null;
addEventListener: ReturnType<typeof vi.fn>;
update: ReturnType<typeof vi.fn>;
};
function mountFakeSW(init: Partial<Reg>): {
registration: Reg;
container: { swListeners: Record<string, ((e: Event) => 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<string, ((e: Event) => 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 = 10): Promise<void> {
await new Promise((r) => setTimeout(r, ms));
}
describe('pwa store', () => {
beforeEach(() => {
vi.resetModules();
});
it('zeigt Toast, wenn beim Mount ein waiting-SW existiert', async () => {
const active = new FakeSW();
const waiting = new FakeSW();
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('zeigt keinen Toast ohne waiting-SW', async () => {
const active = new FakeSW();
mountFakeSW({ active });
const { pwaStore } = await import('../../src/lib/client/pwa.svelte');
await pwaStore.init();
await flush();
expect(pwaStore.updateAvailable).toBe(false);
});
it('reload() postet SKIP_WAITING und reloadet erst beim controllerchange', async () => {
const active = new FakeSW();
const waiting = new FakeSW();
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();
// Simuliere controllerchange, wenn der neue SW übernimmt.
container.swListeners['controllerchange']?.forEach((fn) => fn(new Event('controllerchange')));
expect(reload).toHaveBeenCalledTimes(1);
});
it('refreshing-Flag verhindert doppelten Reload bei mehrfachem controllerchange', async () => {
const active = new FakeSW();
const waiting = new FakeSW();
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();
const fire = () =>
container.swListeners['controllerchange']?.forEach((fn) =>
fn(new Event('controllerchange'))
);
pwaStore.reload();
fire();
fire();
expect(reload).toHaveBeenCalledTimes(1);
});
it('reload() ohne waiting-SW löst location.reload() sofort aus', async () => {
const active = new FakeSW();
mountFakeSW({ active });
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);
});
});