Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/host/public/test-realm-sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,36 @@ self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});

// Interception is opt-in per module. `unregister()` does not evict an already
// active worker from a still-loaded page, so once a module registers this SW it
// keeps controlling the QUnit runner into later modules. If it kept intercepting
// there, those modules (which never installed a `test-realm-fetch` responder)
// would get a 503 for every test-realm fetch and cascade. Gating on `active`
// lets a module turn interception on for its own tests and off on teardown, so a
// lingering worker passes requests straight through — behaving exactly as if no
// SW were installed, which is the state non-intercepting modules expect.
//
// Default off so a cold-started/terminated-and-restarted worker (which loses
// this in-memory flag) fails safe toward passthrough rather than resurrecting
// the leak; the owning module re-asserts `active` in its beforeEach.
let active = false;

self.addEventListener('message', (event) => {
let data = event.data;
if (!data || data.type !== 'test-realm-sw-set-active') {
return;
}
active = Boolean(data.active);
let port = event.ports && event.ports[0];
if (port) {
port.postMessage({ ok: true, active });
}
Comment on lines +29 to +32
});

self.addEventListener('fetch', (event) => {
if (!active) {
return;
}
if (!event.request.url.startsWith('http://test-realm/')) {
return;
}
Expand Down
38 changes: 38 additions & 0 deletions packages/host/tests/helpers/test-realm-service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,36 @@ async function ensureRegistered(): Promise<void> {
return swReady;
}

// Toggle the SW's interception. The worker defaults to passthrough and only
// intercepts test-realm fetches while a module has turned it on, so a worker
// that lingers past `unregister()` stops intercepting for later modules. Awaits
// an ack (bounded, so a missing controller/ack never hangs a hook) to guarantee
// the flag is applied before the module's tests issue their fetches.
async function setSwActive(active: boolean): Promise<void> {
let controller = navigator.serviceWorker.controller;
if (!controller) {
return;
}
await new Promise<void>((resolve) => {
let channel = new MessageChannel();
let settled = false;
let finish = () => {
if (settled) {
return;
}
settled = true;
clearTimeout(timeout);
channel.port1.onmessage = null;
resolve();
};
let timeout = setTimeout(finish, 500);
channel.port1.onmessage = finish;
controller.postMessage({ type: 'test-realm-sw-set-active', active }, [
channel.port2,
]);
});
}
Comment on lines +44 to +67

// Sets up a service worker that intercepts <img> requests to http://test-realm/
// and relays them to the VirtualNetwork so that browser-native resource loads
// (which bypass VirtualNetwork) can reach the test realm's files.
Expand Down Expand Up @@ -70,6 +100,8 @@ export function setupTestRealmServiceWorker(hooks: NestedHooks) {
}
};
navigator.serviceWorker.addEventListener('message', handler);
// Only intercept once the responder above is listening.
await setSwActive(true);
});

hooks.afterEach(function () {
Expand All @@ -83,6 +115,12 @@ export function setupTestRealmServiceWorker(hooks: NestedHooks) {
// doesn't persist and replace the auth service worker (which would break
// the app if the user navigates from /tests back to / during ember serve).
hooks.after(async function () {
// Stop intercepting before we let go of the registration: the worker keeps
// controlling the loaded page after unregister(), and later modules must
// see it as passthrough rather than a 503 source.
if ('serviceWorker' in navigator) {
await setSwActive(false);
}
if (swRegistration) {
await swRegistration.unregister();
swRegistration = undefined;
Expand Down
Loading