All checks were successful
check / check (push) Successful in 24s
Three review findings, no redesign. seal() was installed after session.close(), so onLate could never fire: once the context is destroyed no route handler and no console listener exists to record anything. It was dead code that the attribution table, run.js and harness.js all described as the final safety net. Deleted rather than moved — take() already drains everything the collector holds before teardown, so it covered nothing, and this harness must not ship a mechanism it cannot demonstrate. The now-unreachable `late > 0` term in the failure condition goes with it. handleRpc() parsed `postData || "null"` and then dereferenced the result, so a POST whose body Playwright reports as null — a bodyless request, or any payload it cannot decode as UTF-8, e.g. sendBeacon with a Blob — threw a TypeError inside the route handler and killed node mid-suite: truncated TAP, no summary, no failure line. Non-object payloads now take the same path as unparseable ones and are reported as unstubbed traffic, which is the entire point of that branch. README claimed unrecognised outbound requests are reported as failures, unqualified, while observation in fact ends TRAILING_WATCH_MS after the last test returns. The limit is now stated where it lands.
265 lines
9.5 KiB
JavaScript
265 lines
9.5 KiB
JavaScript
// End-to-end suite entrypoint. Run via script/test-e2e (which builds
|
|
// dist/chrome/ and starts the pinned container); running it directly
|
|
// requires a Chromium that playwright-core can find.
|
|
//
|
|
// A plain runner rather than jest on purpose: jest's default testMatch
|
|
// would pull these files into script/test, and browser tests do not fit
|
|
// inside the 20-second cap REPO_POLICIES.md puts on make test. Nothing
|
|
// here is named *.test.js for the same reason.
|
|
|
|
"use strict";
|
|
|
|
const {
|
|
createWallet,
|
|
launch,
|
|
openAddressDetail,
|
|
openPopup,
|
|
visible,
|
|
} = require("./harness");
|
|
const { STUB_TOKEN, STUB_TX_HASH } = require("./network");
|
|
|
|
const TEST_TIMEOUT_MS = 120000;
|
|
|
|
// How long to keep collecting after the final test returns; see the
|
|
// trailing drain in main().
|
|
const TRAILING_WATCH_MS = 1500;
|
|
|
|
const tests = [];
|
|
|
|
function test(name, fn) {
|
|
tests.push({ name, fn });
|
|
}
|
|
|
|
function assert(cond, message) {
|
|
if (!cond) throw new Error(message);
|
|
}
|
|
|
|
function withTimeout(promise, name) {
|
|
let timer;
|
|
const timeout = new Promise((_, reject) => {
|
|
timer = setTimeout(
|
|
() =>
|
|
reject(new Error("timed out after " + TEST_TIMEOUT_MS + "ms")),
|
|
TEST_TIMEOUT_MS,
|
|
);
|
|
});
|
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
}
|
|
|
|
// ----------------------------------------------------------------- tests
|
|
|
|
test("popup loads and reaches the welcome view", async (env) => {
|
|
env.page = await openPopup(env.ctx, env.popupUrl);
|
|
await visible(env.page, "#view-welcome");
|
|
const title = await env.page.title();
|
|
assert(title === "AutistMask", "unexpected popup title: " + title);
|
|
});
|
|
|
|
test("wallet creation through the UI reaches the main view", async (env) => {
|
|
await createWallet(env.page);
|
|
const addrCount = await env.page
|
|
.locator("#wallet-list .btn-addr-info")
|
|
.count();
|
|
assert(addrCount > 0, "no addresses rendered in the wallet list");
|
|
});
|
|
|
|
test("add token screen opens from address detail (#150)", async (env) => {
|
|
await openAddressDetail(env.page);
|
|
await env.page.click("#btn-add-token");
|
|
await visible(env.page, "#view-add-token");
|
|
const quickPicks = await env.page
|
|
.locator("#common-token-list .common-token")
|
|
.count();
|
|
assert(quickPicks > 0, "no common-token quick-pick buttons rendered");
|
|
});
|
|
|
|
test("transaction detail renders an ERC-20 transfer (#151)", async (env) => {
|
|
// Serve the stubbed token transfer from here on, then reload so the
|
|
// address detail screen refetches its transaction list.
|
|
env.routeOpts.seedTokenTransfer = true;
|
|
await env.page.reload();
|
|
await openAddressDetail(env.page);
|
|
|
|
await visible(env.page, "#tx-list .tx-row");
|
|
const rowText = await env.page
|
|
.locator("#tx-list .tx-row")
|
|
.first()
|
|
.innerText();
|
|
assert(
|
|
rowText.includes(STUB_TOKEN.symbol),
|
|
"token transfer row missing symbol " +
|
|
STUB_TOKEN.symbol +
|
|
", got: " +
|
|
JSON.stringify(rowText),
|
|
);
|
|
|
|
await env.page.locator("#tx-list .tx-row").first().click();
|
|
await visible(env.page, "#view-transaction");
|
|
|
|
const hash = await env.page.locator("#tx-detail-hash").innerText();
|
|
assert(
|
|
hash.includes(STUB_TX_HASH),
|
|
"transaction detail shows the wrong hash: " + hash,
|
|
);
|
|
|
|
// The token contract row is the field that crashes when
|
|
// addressDotHtml is not imported: it renders only for transfers with
|
|
// a contractAddress, which is every ERC-20 transfer.
|
|
await visible(env.page, "#tx-detail-token-contract-section");
|
|
const contract = env.page.locator("#tx-detail-token-contract");
|
|
const contractText = await contract.innerText();
|
|
assert(
|
|
contractText.toLowerCase().includes(STUB_TOKEN.address),
|
|
"token contract row missing the contract address, got: " +
|
|
JSON.stringify(contractText),
|
|
);
|
|
const dots = await contract.locator('span[style*="border-radius"]').count();
|
|
assert(dots > 0, "token contract row rendered without its colour dot");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- runner
|
|
|
|
async function main() {
|
|
// A suite that runs nothing must never report success. If a refactor
|
|
// drops the registrations above, or a require() of this file stops
|
|
// reaching them, the only honest outcome is a red run — reporting
|
|
// "0/0 passed" and exiting 0 is the same vacuous-check failure this
|
|
// whole harness exists to prevent.
|
|
if (tests.length === 0) {
|
|
console.log("1..0");
|
|
console.log("# FAILED: the e2e suite registered no tests");
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
const routeOpts = { seedTokenTransfer: false };
|
|
|
|
let session;
|
|
try {
|
|
session = await launch(routeOpts);
|
|
} catch (e) {
|
|
// Never skip and report success: a browser we cannot start, or
|
|
// one whose network interception is not in force, is a failure of
|
|
// the suite, not an absent one.
|
|
console.error("e2e: cannot run the suite: " + e.message);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
console.log("# extension id: " + session.extensionId);
|
|
console.log("1.." + tests.length);
|
|
|
|
const env = {
|
|
ctx: session.ctx,
|
|
popupUrl: session.popupUrl,
|
|
routeOpts,
|
|
page: null,
|
|
};
|
|
|
|
// Attribution of collected errors is total. session.errors has no
|
|
// window API at all: take() always drains everything outstanding, so
|
|
// successive takes partition the whole stream, and the phases below
|
|
// cover the entire life of the run. Nothing the collector holds can
|
|
// go unread.
|
|
//
|
|
// launch .. end of test 1 -> test 1 (so the worker's startup
|
|
// fetches land on a test, not
|
|
// nowhere)
|
|
// end of test k .. end of k+1 -> test k+1
|
|
// last test .. teardown -> the suite, via the trailing drain
|
|
//
|
|
// Those three phases cover the entire life of the browser context.
|
|
// There is no fourth: once the context is closed nothing can record,
|
|
// because the route handler and the console listeners died with it.
|
|
// Traffic that a test defers past the trailing drain is therefore
|
|
// never observed at all — a real limit of this design, stated in the
|
|
// README, and not one any post-teardown hook could close.
|
|
//
|
|
// Two green-but-vacuous runs on this harness were the same shape: a
|
|
// record falling outside somebody's window and being dropped. First
|
|
// the mark started after test 1, discarding launch-time records;
|
|
// then the tail after the last test was never read. Patching a
|
|
// second boundary would have invited a third, so the window concept
|
|
// is gone rather than fixed.
|
|
let failed = 0;
|
|
let n = 0;
|
|
for (const t of tests) {
|
|
n += 1;
|
|
let failure = null;
|
|
try {
|
|
await withTimeout(t.fn(env), t.name);
|
|
} catch (e) {
|
|
failure = e.message;
|
|
}
|
|
|
|
// Any uncaught page error, console.error or unstubbed request
|
|
// fails the test that provoked it, whether or not its assertions
|
|
// passed. This is the mechanism that caught #150.
|
|
const newErrors = session.errors.take();
|
|
if (!failure && newErrors.length > 0) {
|
|
failure = "uncaught browser errors during this test";
|
|
}
|
|
|
|
if (failure) {
|
|
failed += 1;
|
|
console.log("not ok " + n + " - " + t.name);
|
|
console.log(" " + failure);
|
|
for (const line of newErrors) {
|
|
console.log(" " + line);
|
|
}
|
|
} else {
|
|
console.log("ok " + n + " - " + t.name);
|
|
}
|
|
}
|
|
|
|
// Keep watching after the last test returns, before tearing the
|
|
// browser down. A request a test fires without awaiting is still in
|
|
// flight when its function resolves; measured here it reaches the
|
|
// route handler about 10ms later, but closing the context does not
|
|
// wait for it — with no window at all the request dies unobserved
|
|
// and the run goes green, which is exactly how escaping traffic
|
|
// stays invisible.
|
|
//
|
|
// A fixed bounded window rather than a quiescence poll on purpose:
|
|
// the collector being quiet is not evidence, because a request that
|
|
// has not been dispatched yet has recorded nothing to be quiet
|
|
// about. Playwright offers no "is anything in flight" question to
|
|
// ask either — the route handler is the only observation point — so
|
|
// a grace period is the mechanism available, and this one is ~150x
|
|
// the measured latency for 1.5s on a ~25s suite.
|
|
await new Promise((resolve) => setTimeout(resolve, TRAILING_WATCH_MS));
|
|
|
|
await session.close();
|
|
|
|
// The tail. These cannot be blamed on any single test, so they are
|
|
// reported against the suite rather than guessed at — but they are
|
|
// reported, and they fail the run.
|
|
const trailing = session.errors.take();
|
|
|
|
console.log(
|
|
"# " + (tests.length - failed) + "/" + tests.length + " tests passed",
|
|
);
|
|
|
|
if (trailing.length > 0) {
|
|
console.log(
|
|
"# " +
|
|
trailing.length +
|
|
" browser error(s) recorded after the last test finished, " +
|
|
"not attributable to any single test:",
|
|
);
|
|
for (const line of trailing) {
|
|
console.log("# " + line);
|
|
}
|
|
}
|
|
|
|
if (failed > 0 || trailing.length > 0) {
|
|
console.log("# FAILED");
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error("e2e: " + (e && e.stack ? e.stack : e));
|
|
process.exitCode = 1;
|
|
});
|