test: intercept the MV3 service worker's network in the e2e harness
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:
clawbot
2026-08-09 15:11:19 +00:00
parent d89629d090
commit a3075f2f47
6 changed files with 219 additions and 41 deletions

View File

@@ -1,11 +1,22 @@
// Browser-level network interception for the end-to-end suite.
//
// Every http(s) request the extension makes is fulfilled from these
// Every http(s) request the extension makes — from the popup page AND
// from the MV3 background service worker — is fulfilled from these
// fixtures, so the suite is deterministic and runs entirely offline. The
// probe that motivated this harness (see issue #181) observed live calls
// to Blockscout returning 401 inside the container, which would make any
// assertion about rendered transaction data worthless.
//
// Service-worker coverage is not free: ctx.route() only sees worker
// traffic when PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1 is set in
// the environment, which script/test-e2e does. Without it the phishing
// blocklist fetch that src/background/index.js issues at worker startup
// silently reaches raw.githubusercontent.com on the open internet, and
// src/shared/phishingDomains.js swallows the failure so nothing surfaces
// it. That is not left to trust: waitForServiceWorkerTraffic() below
// backs the launch-time canary in harness.js, which fails the entire
// suite if worker requests stop being visible here.
//
// Anything not explicitly stubbed here is aborted AND reported to the
// error collector, so a newly added outbound call shows up as a test
// failure rather than as intermittent flakiness.
@@ -144,16 +155,45 @@ function handleRpc(route, postData, report) {
* @param {boolean} [opts.seedTokenTransfer] serve the stubbed ERC-20
* transfer. Read at request time, so a test can flip it on the same
* options object without re-registering the route.
* @returns {Promise<{waitForServiceWorkerTraffic: (ms: number) =>
* Promise<string|null>}>}
*/
async function installNetworkStubs(ctx, opts) {
const report = opts.report;
// First request seen that originated in a service worker, and the
// resolver waiting for it. This is what proves worker interception is
// actually in force; see waitForServiceWorkerTraffic below.
let firstWorkerRequest = null;
let announceWorkerRequest = null;
// E2E_TRACE_NETWORK=1 prints every request that reaches this handler,
// tagged [sw] when it originated in the background service worker.
// It exists so the isolation claim above can be re-checked by anyone
// in one command, without editing files: the phishing blocklist fetch
// showing up with an [sw] tag is the proof that the worker really is
// intercepted and that the raw.githubusercontent.com stub below is
// live code rather than decoration.
const trace = process.env.E2E_TRACE_NETWORK === "1";
// Regex rather than a glob so chrome-extension:// resource loads are
// never touched — routing those would break the popup itself.
await ctx.route(/^https?:\/\//, async (route) => {
const req = route.request();
const url = new URL(req.url());
const p = url.pathname;
const fromWorker = !!req.serviceWorker();
if (fromWorker && !firstWorkerRequest) {
firstWorkerRequest = req.method() + " " + req.url();
if (announceWorkerRequest)
announceWorkerRequest(firstWorkerRequest);
}
if (trace) {
const origin = fromWorker ? "[sw] " : "[page] ";
console.log("# routed " + origin + req.method() + " " + req.url());
}
// JSON-RPC endpoint (any host): a POST with a JSON-RPC body.
if (req.method() === "POST") {
@@ -213,11 +253,38 @@ async function installNetworkStubs(ctx, opts) {
report("unstubbed request: " + req.method() + " " + req.url());
return route.abort();
});
return {
/**
* Resolve with the first service-worker-originated request this
* handler saw, or null if none arrives within `ms`.
*
* The background worker fetches the phishing blocklist at
* startup, unconditionally, within about a second of the context
* coming up — so under working interception this resolves almost
* immediately. Nothing arriving means worker traffic is bypassing
* the handler entirely and going to the real internet, which the
* caller turns into a hard failure of the whole suite.
*/
waitForServiceWorkerTraffic(ms) {
if (firstWorkerRequest) return Promise.resolve(firstWorkerRequest);
return new Promise((resolve) => {
const timer = setTimeout(() => {
announceWorkerRequest = null;
resolve(null);
}, ms);
announceWorkerRequest = (req) => {
clearTimeout(timer);
announceWorkerRequest = null;
resolve(req);
};
});
},
};
}
module.exports = {
installNetworkStubs,
STUB_TOKEN,
STUB_COUNTERPARTY,
STUB_TX_HASH,
};