Add restart-to-update banner: waiting SW detected via watchForWaitingUpdate, explicit user opt-in applies SKIP_WAITING + reload (no auto mid-session claim, ARCH §8)
Some checks failed
ci / check (push) Has been cancelled

This commit is contained in:
Lumen Stage1 2026-10-03 13:12:25 -05:00
commit e46b14ff07
6 changed files with 266 additions and 5 deletions

View file

@ -6,7 +6,11 @@
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { readFileSync, existsSync } from "node:fs";
import { registerSW, skipWaitingForUpdate } from "../../src/platform/sw/register.js";
import {
registerSW,
skipWaitingForUpdate,
watchForWaitingUpdate,
} from "../../src/platform/sw/register.js";
const SW_PATH = "public/sw.js";
@ -148,6 +152,68 @@ describe("SW registration — page bridge", () => {
});
});
describe("watchForWaitingUpdate — restart-to-update discovery", () => {
function withController(controller: unknown): void {
Object.defineProperty(globalThis, "navigator", {
value: { serviceWorker: { controller } },
writable: true,
configurable: true,
});
}
it("no controller (first install) → never fires", () => {
withController(null);
const onWaiting = vi.fn();
const listeners: Record<string, () => void> = {};
const reg = {
waiting: { postMessage: vi.fn() },
addEventListener: (ev: string, cb: () => void) => {
listeners[ev] = cb;
},
} as unknown as ServiceWorkerRegistration;
watchForWaitingUpdate(reg, onWaiting);
expect(onWaiting).not.toHaveBeenCalled();
expect(listeners.updatefound).toBeUndefined();
});
it("already-waiting worker fires immediately", () => {
withController({ scriptURL: "/sw.js" });
const onWaiting = vi.fn();
const reg = {
waiting: { postMessage: vi.fn() },
addEventListener: vi.fn(),
} as unknown as ServiceWorkerRegistration;
watchForWaitingUpdate(reg, onWaiting);
expect(onWaiting).toHaveBeenCalledWith(reg);
});
it("updatefound → installed waiting worker fires", () => {
withController({ scriptURL: "/sw.js" });
const onWaiting = vi.fn();
let foundCb: (() => void) | undefined;
const stateCbs: (() => void)[] = [];
const installed = {
state: "installed",
addEventListener: (_ev: string, cb: () => void) => {
stateCbs.push(cb);
},
};
const reg = {
waiting: null as unknown,
installing: installed,
addEventListener: (ev: string, cb: () => void) => {
if (ev === "updatefound") foundCb = cb;
},
} as unknown as ServiceWorkerRegistration;
watchForWaitingUpdate(reg, onWaiting);
expect(onWaiting).not.toHaveBeenCalled();
foundCb?.();
(reg as { waiting: unknown }).waiting = { postMessage: vi.fn() };
for (const cb of stateCbs) cb();
expect(onWaiting).toHaveBeenCalledTimes(1);
});
});
describe("Cache safety — versioning and shell assets", () => {
it("vite build injects hashed assets into precache manifest", () => {
const viteConfig = readFileSync("vite.config.ts", "utf8");