// The service worker's own logic, run in a sandbox with a fake cache and a fake // fetch: what the install keeps, when it fails, and when a navigation refreshes // the cached entry document. Run by `npm test`. import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { test } from "node:test"; import { runInNewContext } from "node:vm"; const SOURCE = readFileSync(new URL("../src/sw.js", import.meta.url), "utf8"); const ORIGIN = "https://swarm.example"; function res({ ok = true, redirected = false, status = 200 } = {}) { const r = { ok, redirected, status, body: `body-${Math.random()}` }; r.clone = () => ({ ...r }); return r; } // A worker loaded against a fake network. `network(url, init)` returns a // response or throws; `stores` is every cache the worker opened. function load(network) { const handlers = {}; const stores = new Map(); const calls = []; const errors = []; const open = async (name) => { if (!stores.has(name)) stores.set(name, new Map()); const store = stores.get(name); return { put: async (key, value) => { store.set(typeof key === "string" ? key : key.url, value); }, keys: async () => [...store.keys()], }; }; const sandbox = { self: { location: { origin: ORIGIN }, addEventListener: (type, fn) => (handlers[type] = fn), skipWaiting: () => {}, clients: { claim: async () => {} }, }, caches: { open, keys: async () => [...stores.keys()], delete: async (name) => stores.delete(name), match: async (url) => { for (const store of stores.values()) { if (store.has(url)) return store.get(url); } return undefined; }, }, fetch: async (input, init) => { const url = typeof input === "string" ? input : input.url; calls.push({ url, init }); return network(url, init); }, URL, console: { error: (...args) => errors.push(args.join(" ")) }, }; runInNewContext(SOURCE, sandbox); return { handlers, stores, calls, errors }; } function install(worker) { const waits = []; worker.handlers.install({ waitUntil: (p) => waits.push(p) }); return Promise.all(waits); } function navigate(worker, url) { const waits = []; let answer; worker.handlers.fetch({ request: { url: ORIGIN + url, mode: "navigate" }, respondWith: (p) => (answer = p), waitUntil: (p) => waits.push(p), }); return { answer, settled: () => Promise.all(waits) }; } test("the install fetches every shell asset past the HTTP cache and keeps it", async () => { const worker = load(() => res()); await install(worker); assert.ok(worker.calls.length >= 7); assert.ok(worker.calls.every((c) => c.init?.cache === "reload")); const [store] = [...worker.stores.values()]; assert.ok(store.has("/")); assert.ok(store.has("/static/colors.css")); }); test("a redirected asset fails the install and says why", async () => { const worker = load((url) => url === "/static/colors.css" ? res({ redirected: true }) : res(), ); await assert.rejects(install(worker), /colors\.css.*redirect/); assert.equal(worker.errors.length, 1); }); test("an asset the server refuses fails the install", async () => { const worker = load((url) => url === "/manifest.webmanifest" ? res({ ok: false, status: 404 }) : res(), ); await assert.rejects(install(worker), /manifest\.webmanifest.*404/); }); test("a navigation that reaches the network refreshes the cached entry document", async () => { const fresh = res(); const net = load(() => fresh); await install(net); const nav = navigate(net, "/agents/iris/terminal"); const answered = await nav.answer; await nav.settled(); assert.equal(answered, fresh); const [store] = [...net.stores.values()]; assert.equal(store.get("/").body, fresh.body); }); test("a redirected or failed navigation does not replace the cached entry document", async () => { let next = res(); const worker = load((url) => (url === "/" ? res() : next)); await install(worker); const [store] = [...worker.stores.values()]; const before = store.get("/"); for (const bad of [ res({ redirected: true }), res({ ok: false, status: 502 }), ]) { next = bad; const nav = navigate(worker, "/agents"); await nav.answer; await nav.settled(); assert.equal(store.get("/"), before); } }); test("offline, a navigation is answered from the cached entry document", async () => { let online = true; const worker = load((url) => { if (online || url === "/") return res(); throw new TypeError("offline"); }); await install(worker); online = false; const [store] = [...worker.stores.values()]; const nav = navigate(worker, "/jobs"); assert.equal(await nav.answer, store.get("/")); });