|
1 | 1 | // test/load-sw.mjs |
2 | 2 | // |
3 | | -// Loads assets/sw.js under plain Node for unit testing. |
| 3 | +// Loads the REAL assets/sw.js under plain Node for unit testing, WITH V8 test-coverage attribution. |
4 | 4 | // |
5 | 5 | // sw.js is authored to run as a browser module Service Worker: it has ONE absolute-path ES import |
6 | 6 | // (the wasm-bindgen glue at "/__dig/dig_client.js", meaningless outside a browser origin) and |
7 | 7 | // registers top-level `self.addEventListener(...)` listeners on import — neither resolves under |
8 | | -// plain Node. Rather than fork sw.js's pure orchestration logic into a second, drift-prone copy |
9 | | -// just to make it importable, this loader: |
| 8 | +// plain Node. Rather than fork sw.js's logic into a second, drift-prone copy (or rewrite its source |
| 9 | +// into an anonymous `data:` URL, which hides the file from the coverage report — see #sw-coverage), |
| 10 | +// this loader imports the genuine file and makes it runnable under Node by: |
10 | 11 | // |
11 | | -// 1. Rewrites the one wasm-bindgen import to a file:// import of stub-dig-client.mjs |
12 | | -// (deterministic fake crypto — see that file's doc comment for why real AEAD/merkle |
13 | | -// correctness is intentionally NOT re-tested here; it's covered by digstore's Rust suite). |
14 | | -// 2. Installs a minimal `self` (addEventListener/location/clients/skipWaiting) and an in-memory |
| 12 | +// 1. Registering a module-resolve hook (test/sw-import-hooks.mjs) that remaps the one |
| 13 | +// wasm-bindgen import to test/stub-dig-client.mjs (deterministic fake crypto — see that file's |
| 14 | +// doc comment for why real AEAD/merkle correctness is intentionally NOT re-tested here; it's |
| 15 | +// covered by digstore's Rust suite). The file itself is imported UNMODIFIED, so V8 attributes |
| 16 | +// coverage to `assets/sw.js`. |
| 17 | +// 2. Installing a minimal `self` (addEventListener/location/clients/skipWaiting) and an in-memory |
15 | 18 | // `caches` (CacheStorage) polyfill on `globalThis`, since neither exists in Node. `fetch`, |
16 | 19 | // `Response`, `Request`, `ReadableStream`, `crypto.subtle`, and `atob` are real Node globals |
17 | 20 | // (Node 18+) and are used as-is — no polyfill needed for those. |
18 | | -// 3. Imports the transformed source via a `data:` URL (a stable Node dynamic-import target since |
19 | | -// Node 17.5), so nothing is written to disk. |
20 | 21 | // |
21 | 22 | // Returns the sw.js module namespace — its named exports (see the "Test-only exports" block at the |
22 | | -// bottom of sw.js). |
| 23 | +// bottom of sw.js). The module is import-cached (one instance), so `self.location` is (re)set on |
| 24 | +// each `loadSw` call and read at call-time by the exported helpers. |
| 25 | +import { register } from "node:module"; |
23 | 26 | import { readFileSync } from "node:fs"; |
24 | 27 | import { fileURLToPath, pathToFileURL } from "node:url"; |
25 | 28 | import path from "node:path"; |
26 | 29 |
|
27 | 30 | const here = path.dirname(fileURLToPath(import.meta.url)); |
28 | 31 | const swPath = path.join(here, "..", "assets", "sw.js"); |
29 | | -const stubUrl = pathToFileURL(path.join(here, "stub-dig-client.mjs")).href; |
| 32 | +const swUrl = pathToFileURL(swPath).href; |
30 | 33 |
|
| 34 | +// The wasm-bindgen import statement shape sw.js MUST keep for the resolve hook to find it. Asserted |
| 35 | +// once (below) so a future edit to that import fails loudly here instead of silently breaking the |
| 36 | +// remap and every sw.js test with it. |
31 | 37 | const IMPORT_RE = /import initDigClient,\s*\{[\s\S]*?\}\s*from\s*["']\/__dig\/dig_client\.js["'];/; |
32 | 38 |
|
| 39 | +let hooksRegistered = false; |
| 40 | + |
33 | 41 | export async function loadSw({ locationHref = "https://teststore.on.dig.net/" } = {}) { |
34 | | - const src = readFileSync(swPath, "utf8"); |
35 | | - if (!IMPORT_RE.test(src)) { |
36 | | - throw new Error( |
37 | | - "sw.js's wasm-bindgen import statement shape changed — update load-sw.mjs's IMPORT_RE to match" |
38 | | - ); |
| 42 | + if (!hooksRegistered) { |
| 43 | + const src = readFileSync(swPath, "utf8"); |
| 44 | + if (!IMPORT_RE.test(src)) { |
| 45 | + throw new Error( |
| 46 | + "sw.js's wasm-bindgen import statement shape changed — update test/sw-import-hooks.mjs's WASM_GLUE_SPECIFIER and this IMPORT_RE to match" |
| 47 | + ); |
| 48 | + } |
| 49 | + register("./sw-import-hooks.mjs", import.meta.url); |
| 50 | + hooksRegistered = true; |
39 | 51 | } |
40 | | - const transformed = src.replace( |
41 | | - IMPORT_RE, |
42 | | - `import initDigClient, { retrievalKey, deriveKey, verifyInclusion, decryptChunk, install_global } from ${JSON.stringify(stubUrl)};` |
43 | | - ); |
44 | 52 |
|
45 | 53 | installSwGlobals(locationHref); |
46 | | - |
47 | | - const dataUrl = "data:text/javascript;base64," + Buffer.from(transformed, "utf8").toString("base64"); |
48 | | - return import(dataUrl); |
| 54 | + return import(swUrl); |
49 | 55 | } |
50 | 56 |
|
51 | 57 | /** Minimal `self` + `caches` polyfill so sw.js's top-level side effects (addEventListener calls) |
52 | | - * and any Cache Storage use don't throw on import under Node. */ |
53 | | -function installSwGlobals(locationHref) { |
| 58 | + * and any Cache Storage use don't throw under Node. */ |
| 59 | +// sw.js registers its install/activate/fetch listeners exactly ONCE, at module import — but each |
| 60 | +// `installSwGlobals` call swaps in a fresh `self`. Share one listener registry across every `self` |
| 61 | +// so a listener captured at import is still reachable via a later `self.dispatch(...)`. |
| 62 | +const swListeners = new Map(); |
| 63 | + |
| 64 | +export function installSwGlobals(locationHref) { |
| 65 | + const listeners = swListeners; |
54 | 66 | globalThis.self = { |
55 | | - addEventListener() {}, // sw.js registers install/activate/fetch listeners; tests call the |
56 | | - // exported functions directly rather than dispatching fake events. |
| 67 | + // sw.js registers install/activate/fetch listeners at import; capture them so tests that want |
| 68 | + // to exercise the lifecycle/fetch handlers can dispatch a fake event (see sw-runtime.test.mjs). |
| 69 | + addEventListener(type, handler) { |
| 70 | + listeners.set(type, handler); |
| 71 | + }, |
| 72 | + dispatch(type, event) { |
| 73 | + const handler = listeners.get(type); |
| 74 | + if (!handler) throw new Error(`no '${type}' listener registered`); |
| 75 | + return handler(event); |
| 76 | + }, |
57 | 77 | location: new URL(locationHref), |
| 78 | + origin: new URL(locationHref).origin, |
58 | 79 | clients: { claim: async () => {} }, |
59 | 80 | skipWaiting: async () => {}, |
60 | 81 | }; |
61 | 82 | globalThis.caches = makeFakeCacheStorage(); |
| 83 | + return globalThis.self; |
62 | 84 | } |
63 | 85 |
|
64 | 86 | /** A tiny in-memory CacheStorage stand-in: `open(name)` returns a Map-backed cache exposing the |
65 | 87 | * same `match`/`put`/`keys`/`delete` surface sw.js uses. */ |
66 | | -function makeFakeCacheStorage() { |
| 88 | +export function makeFakeCacheStorage({ failOpen = false } = {}) { |
67 | 89 | const stores = new Map(); |
68 | 90 | return { |
69 | 91 | async open(name) { |
| 92 | + if (failOpen) throw new Error("cache storage unavailable"); |
70 | 93 | if (!stores.has(name)) stores.set(name, new Map()); |
71 | 94 | const store = stores.get(name); |
72 | 95 | return { |
|
0 commit comments