test: containerized Chrome end-to-end harness that drives the real popup (closes #181)
Some checks failed
check / check (push) Has been cancelled
Some checks failed
check / check (push) Has been cancelled
`make check` was green while the AddToken screen crashed on every open. `script/lint` is only `prettier --check`, so a used-but-not-imported identifier is invisible until a browser evaluates it. This adds a suite that runs the real popup in a real Chrome and treats any uncaught page error or console.error as a failure. - `script/test-e2e` (with `make test-e2e` as a thin shim) builds `dist/chrome/` and runs `tests/e2e/run.js` inside the Playwright image, pinned by digest. `playwright-core` is pinned to the matching 1.56.0 through `yarn.lock`; the two must be bumped together because the browsers ship inside the image. - Deliberately outside `script/test` and `script/check`: REPO_POLICIES caps `make test` at 20 seconds. Nothing under `tests/e2e/` is named `*.test.js`, so jest cannot pick it up either. - Launches with `channel: "chromium"`; the default headless shell silently refuses to load extensions with no error at all. The extension id is read from the service worker URL, never hardcoded. - All http(s) traffic is intercepted at the browser level and served from fixtures, so the run is deterministic and offline. Unrecognised outbound requests are reported as failures rather than allowed. - A missing build or an unavailable container fails loudly; a skip that looks like a pass is the failure mode this is meant to prevent. - One allowlisted page error, for the libsodium WASM CSP fallback tracked as #182, which is otherwise untouched here. The suite was demonstrated failing against the unfixed tree with `pageerror: showView is not defined` and `pageerror: addressDotHtml is not defined`, so it carries the two one-line import fixes it caught: closes #150 — `showView` restored to the destructure in `src/popup/views/addToken.js`, dropped bya22f33d, which made the AddToken screen unreachable and corrupted the navigation stack. closes #151 — `addressDotHtml` restored in `src/popup/views/transactionDetail.js`, dropped bydf031fd, which threw before `showView("transaction")` for every ERC-20 transfer. The shared `renderAddressHtml` helper is not used here on purpose: it hardcodes the `/address/` explorer URL, and this row needs the token-specific `/token/` link.
This commit is contained in:
188
tests/e2e/run.js
Normal file
188
tests/e2e/run.js
Normal file
@@ -0,0 +1,188 @@
|
||||
// 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;
|
||||
|
||||
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() {
|
||||
const routeOpts = { seedTokenTransfer: false };
|
||||
|
||||
let session;
|
||||
try {
|
||||
session = await launch(routeOpts);
|
||||
} catch (e) {
|
||||
// Never skip and report success: a browser we cannot start is a
|
||||
// failure of the suite, not an absent one.
|
||||
console.error("e2e: could not start the browser: " + 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,
|
||||
};
|
||||
|
||||
let failed = 0;
|
||||
let n = 0;
|
||||
for (const t of tests) {
|
||||
n += 1;
|
||||
const mark = session.errors.mark();
|
||||
let failure = null;
|
||||
try {
|
||||
await withTimeout(t.fn(env), t.name);
|
||||
} catch (e) {
|
||||
failure = e.message;
|
||||
}
|
||||
|
||||
// Any uncaught page error or console.error fails the test that
|
||||
// provoked it, whether or not its assertions passed. This is the
|
||||
// mechanism that caught #150.
|
||||
const newErrors = session.errors.since(mark);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
await session.close();
|
||||
|
||||
console.log(
|
||||
"# " + (tests.length - failed) + "/" + tests.length + " passed",
|
||||
);
|
||||
if (failed > 0) {
|
||||
console.log("# FAILED");
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("e2e: " + (e && e.stack ? e.stack : e));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user