Files
Codeman/test/sw-fetch-skip.test.ts
Codeman maintainer 7dd78d9d71 fix(sw): leave rendered HTML pages to the network
The fetch handler skipped only /api/, so a popped-out /html-view/ page (a
top-level navigation the worker intercepts) was put into CacheStorage
despite its `Cache-Control: no-store`, and the offline fallback could
serve it after logout had revoked the capability. /html-view/ is now
skipped like /api/. The new test runs the shipped sw.js against a stub
worker scope and drives its real fetch listener.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:13:29 +02:00

58 lines
2.2 KiB
TypeScript

/**
* @fileoverview Which requests the service worker leaves to the network.
*
* The fetch handler caches every OK GET it answers, and the offline fallback
* serves from that cache. A rendered HTML page (`/html-view/<cap>/...`) is
* served `no-store` behind a capability that logout revokes, so a popped-out
* page must never land there: it is skipped like `/api/`.
*
* Runs the shipped sw.js in a `vm` context with a stub ServiceWorkerGlobalScope
* and drives its real fetch listener.
*/
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import vm from 'node:vm';
import { describe, expect, it, vi } from 'vitest';
const swJs = readFileSync(resolve(import.meta.dirname, '../src/web/public/sw.js'), 'utf8');
function loadFetchListener(): (event: unknown) => void {
const listeners = new Map<string, (event: unknown) => void>();
const self = {
location: { pathname: '/sw.js', origin: 'http://localhost' },
addEventListener: (type: string, fn: (event: unknown) => void) => listeners.set(type, fn),
};
// A fetch that never settles: the listener only has to decide whether to answer.
const fetch = vi.fn(() => new Promise(() => {}));
vm.runInContext(swJs, vm.createContext({ self, console, caches: {}, fetch, URL }), { filename: 'sw.js' });
const listener = listeners.get('fetch');
expect(listener, 'sw.js registers a fetch listener').toBeDefined();
return listener!;
}
function fetchEvent(path: string) {
return {
request: { method: 'GET', url: `http://localhost${path}`, headers: { get: () => null } },
respondWith: vi.fn(),
};
}
describe('service worker fetch skip list', () => {
it('leaves rendered HTML pages and the API to the network', () => {
const onFetch = loadFetchListener();
for (const path of ['/html-view/AAAAAAAAAAAAAAAAAAAAAAAA/index.html', '/api/status']) {
const event = fetchEvent(path);
onFetch(event);
expect(event.respondWith, path).not.toHaveBeenCalled();
}
});
it('still answers an app asset', () => {
const onFetch = loadFetchListener();
const event = fetchEvent('/app.js');
onFetch(event);
expect(event.respondWith).toHaveBeenCalledTimes(1);
});
});