test: intercept the MV3 service worker's network in the e2e harness
All checks were successful
check / check (push) Successful in 43s
All checks were successful
check / check (push) Successful in 43s
ctx.route() does not see requests made by the background service worker unless Playwright is run with PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1, so the phishing blocklist fetch that src/background/index.js issues at worker startup was reaching raw.githubusercontent.com on the real internet on every run. phishingDomains.js swallows fetch failures, so nothing surfaced it, and the raw.githubusercontent.com stub in tests/e2e/network.js was unreachable code that made the gap look covered. script/test-e2e now sets the flag, with a comment recording what to do if a future Playwright drops it. The flag being experimental is not taken on trust: launch() waits for the worker's own startup request to arrive in the route handler and refuses to run the suite if it never does, so escaping traffic fails the run instead of passing unnoticed. Chrome is additionally started with --host-resolver-rules=MAP * ~NOTFOUND, so anything that does slip past interception cannot reach a real host. Also: errors and unstubbed requests recorded during launch are attributed to the first test rather than discarded, a suite that registers no tests now fails instead of exiting 0, a failure after the browser is up tears the context down instead of hanging the process, E2E_TRACE_NETWORK=1 prints every routed request tagged [sw] or [page], and the dead exports in harness.js and network.js are gone.
This commit is contained in:
@@ -74,19 +74,61 @@ function attachErrorListeners(ctx, errors) {
|
||||
ctx.on("page", attachPage);
|
||||
// Per-page listeners only: the context-level "weberror" event covers
|
||||
// the same page exceptions and would double-report them. Playwright
|
||||
// exposes no error event for service workers, so an uncaught error in
|
||||
// the background worker is not visible here — everything this suite
|
||||
// drives lives in the popup page.
|
||||
// exposes no error EVENT for service workers, so an uncaught
|
||||
// exception in the background worker is not visible here — everything
|
||||
// this suite drives lives in the popup page. That is an error-channel
|
||||
// gap only: worker NETWORK traffic is intercepted and reported like
|
||||
// any other, and assertWorkerTrafficIntercepted() below fails the run
|
||||
// if it ever stops being.
|
||||
}
|
||||
|
||||
// The extension id is derived from the unpacked path, so it changes and
|
||||
// must never be hardcoded. It is the host part of the service worker URL.
|
||||
async function extensionId(ctx) {
|
||||
let [sw] = ctx.serviceWorkers();
|
||||
if (!sw) {
|
||||
sw = await ctx.waitForEvent("serviceworker", { timeout: 30000 });
|
||||
}
|
||||
return new URL(sw.url()).host;
|
||||
async function serviceWorker(ctx) {
|
||||
const [existing] = ctx.serviceWorkers();
|
||||
if (existing) return existing;
|
||||
return ctx.waitForEvent("serviceworker", { timeout: 30000 });
|
||||
}
|
||||
|
||||
// How long to wait for the background worker's first outbound request.
|
||||
// Measured at roughly 650ms after the route is installed; the margin is
|
||||
// for a loaded machine, not for hope.
|
||||
const WORKER_TRAFFIC_TIMEOUT_MS = 30000;
|
||||
|
||||
// ctx.route() only sees service-worker requests when Playwright runs with
|
||||
// PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1, which script/test-e2e
|
||||
// sets. Without it the worker's traffic — notably the phishing blocklist
|
||||
// fetch src/background/index.js issues at startup — goes to the real
|
||||
// internet, and nothing says so, because src/shared/phishingDomains.js
|
||||
// swallows fetch failures. A harness whose isolation can lapse in silence
|
||||
// is worthless, so this does not take the flag on trust: the background
|
||||
// worker's own startup fetch has to show up in the route handler, or the
|
||||
// suite refuses to run.
|
||||
//
|
||||
// Deliberately NOT a synthetic probe fetched through worker.evaluate():
|
||||
// evaluating in an extension worker this early kills it (the call fails
|
||||
// with "Target page, context or browser has been closed" and the worker
|
||||
// disappears), which would break the very thing being measured. Observing
|
||||
// traffic the extension already generates costs nothing and cannot
|
||||
// perturb it.
|
||||
async function assertWorkerTrafficIntercepted(stubs) {
|
||||
const seen = await stubs.waitForServiceWorkerTraffic(
|
||||
WORKER_TRAFFIC_TIMEOUT_MS,
|
||||
);
|
||||
if (seen) return seen;
|
||||
|
||||
throw new Error(
|
||||
"no service-worker request reached the route handler within " +
|
||||
WORKER_TRAFFIC_TIMEOUT_MS +
|
||||
"ms, so background worker traffic is escaping this harness and " +
|
||||
"going to the real internet. Run the suite through " +
|
||||
"script/test-e2e, which sets " +
|
||||
"PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1. If a " +
|
||||
"Playwright upgrade dropped that flag, replace the mechanism or " +
|
||||
"downgrade the isolation claims in tests/e2e/network.js and " +
|
||||
"README.md — do not delete this check. If instead the " +
|
||||
"background worker legitimately stopped making startup " +
|
||||
"requests, this check needs a new anchor, because there is no " +
|
||||
"longer any worker traffic to observe",
|
||||
);
|
||||
}
|
||||
|
||||
async function launch(routeOpts) {
|
||||
@@ -112,27 +154,50 @@ async function launch(routeOpts) {
|
||||
// The container runs unprivileged; Chrome's sandbox needs
|
||||
// capabilities the harness deliberately does not grant it.
|
||||
"--no-sandbox",
|
||||
// Belt to the interception braces: nothing that slips past
|
||||
// the route handler can resolve a name, so a request that
|
||||
// escapes cannot actually reach the internet. Detection is
|
||||
// still assertWorkerTrafficIntercepted()'s job — this only
|
||||
// bounds the damage while a gap goes unnoticed. Playwright
|
||||
// fulfils routed requests without touching the resolver, and
|
||||
// it drives the browser over a pipe, so neither is affected.
|
||||
"--host-resolver-rules=MAP * ~NOTFOUND",
|
||||
],
|
||||
});
|
||||
|
||||
const errors = new ErrorCollector();
|
||||
attachErrorListeners(ctx, errors);
|
||||
routeOpts.report = (text) => errors.record("network", text);
|
||||
await installNetworkStubs(ctx, routeOpts);
|
||||
|
||||
const id = await extensionId(ctx);
|
||||
const popupUrl = "chrome-extension://" + id + "/src/popup/index.html";
|
||||
|
||||
return {
|
||||
ctx,
|
||||
errors,
|
||||
extensionId: id,
|
||||
popupUrl,
|
||||
async close() {
|
||||
await ctx.close();
|
||||
fs.rmSync(userDir, { recursive: true, force: true });
|
||||
},
|
||||
const cleanup = async () => {
|
||||
await ctx.close().catch(() => {});
|
||||
fs.rmSync(userDir, { recursive: true, force: true });
|
||||
};
|
||||
|
||||
try {
|
||||
const errors = new ErrorCollector();
|
||||
attachErrorListeners(ctx, errors);
|
||||
routeOpts.report = (text) => errors.record("network", text);
|
||||
const stubs = await installNetworkStubs(ctx, routeOpts);
|
||||
|
||||
await assertWorkerTrafficIntercepted(stubs);
|
||||
|
||||
// The extension id is derived from the unpacked path, so it
|
||||
// changes and must never be hardcoded. It is the host part of the
|
||||
// service worker URL.
|
||||
const sw = await serviceWorker(ctx);
|
||||
const id = new URL(sw.url()).host;
|
||||
|
||||
return {
|
||||
ctx,
|
||||
errors,
|
||||
extensionId: id,
|
||||
popupUrl: "chrome-extension://" + id + "/src/popup/index.html",
|
||||
close: cleanup,
|
||||
};
|
||||
} catch (e) {
|
||||
// Anything that fails after the browser is up has to tear it down
|
||||
// on the way out: an orphaned context keeps node alive forever,
|
||||
// turning a clean failure into a hung run.
|
||||
await cleanup();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- flows
|
||||
@@ -177,9 +242,6 @@ async function openAddressDetail(page) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ALLOWED_ERRORS,
|
||||
EXT_PATH,
|
||||
REPO_ROOT,
|
||||
createWallet,
|
||||
launch,
|
||||
openAddressDetail,
|
||||
|
||||
Reference in New Issue
Block a user