Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: test the service worker's install and navigation logic

review-classifier asked for a test of sw.js's new behaviour. sw.test.mjs loads
the worker in a sandbox with a fake cache and network: the install fetches past
the HTTP cache and keeps every asset, a redirected or refused asset fails it
with a logged reason, a navigation that reaches the network refreshes the cached
entry document, a redirected or failed one does not, and offline a navigation is
answered from the cache.
This commit is contained in:
iris 2026-10-28 18:25:12 +01:00 • committed by mara
commit 067a260b85

View file

@ -0,0 +1,148 @@
// 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("/"));
});