// 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() { // 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, }; let failed = 0; let n = 0; // Starts at zero rather than at the current mark on purpose: errors // and escaping requests recorded during launch — before any test ran, // which is when the background worker does its startup fetches — are // attributed to the first test instead of being discarded. let mark = 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 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); mark = session.errors.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; });