test: containerized Chrome end-to-end harness that drives the real popup (closes #181)
All checks were successful
check / check (push) Successful in 55s

`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 by a22f33d, which made the
AddToken screen unreachable and corrupted the navigation stack.

closes #151 — `addressDotHtml` restored in
`src/popup/views/transactionDetail.js`, dropped by df031fd, 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:
2026-08-09 14:24:36 +00:00
parent f7f141a757
commit d89629d090
11 changed files with 704 additions and 5 deletions

View File

@@ -1,4 +1,4 @@
.PHONY: bootstrap setup install test lint fmt fmt-check check docker hooks build build-debug clean dev
.PHONY: bootstrap setup install test test-e2e lint fmt fmt-check check docker hooks build build-debug clean dev
# Standard targets are thin shims; the implementations live in script/
# per the scripts-to-rule-them-all pattern (see the Entrypoints section
@@ -16,6 +16,10 @@ install:
test:
@script/test
# Browser end-to-end suite. Requires docker; not part of check.
test-e2e:
@script/test-e2e
lint:
@script/lint

View File

@@ -73,6 +73,8 @@ provide:
git pre-commit hook
- `script/projectname` — print the project name (used for the Docker image tag)
- `script/test` — run the test suite (jest)
- `script/test-e2e` — run the browser end-to-end suite (docker required; see
[End-to-End Tests](#end-to-end-tests))
- `script/lint` — run the linter
- `script/fmt` — format all files (writes)
- `script/fmt-check` — check formatting (read-only)
@@ -82,6 +84,35 @@ provide:
- `script/precommit` — run by the git pre-commit hook; runs `script/check`
- `script/install-precommit` — install the git pre-commit hook
## End-to-End Tests
`make test-e2e` builds `dist/chrome/` and drives the **real popup in a real
Chrome**, loaded as an unpacked MV3 extension inside a pinned
`mcr.microsoft.com/playwright` container (pinned by digest in `script/test-e2e`;
docker is required and the suite fails loudly rather than skipping if it is
unavailable). The suite lives in `tests/e2e/` and is driven by
`playwright-core`, whose version must stay matched to the container's Playwright
version — the browsers ship inside the image.
It covers popup load, wallet creation through the UI, the Add Token screen and
the transaction detail screen for an ERC-20 transfer. All outbound network is
intercepted at the browser level and served from fixtures in
`tests/e2e/network.js`, so the run is deterministic and fully offline;
unrecognised outbound requests are reported as failures rather than silently
allowed.
**Any uncaught page error or `console.error` fails the run.** That is the point:
a `ReferenceError` from a used-but-not-imported identifier is invisible to
`make check` (`script/lint` is only `prettier --check`) but fatal in a browser,
and this suite exists because exactly that class of bug shipped twice.
`make test-e2e` is deliberately **not** part of `make check` or `make test`.
`REPO_POLICIES.md` caps `make test` at 20 seconds and a browser suite does not
fit; nothing in `tests/e2e/` is named `*.test.js`, so jest cannot pick it up
either. It is also not wired into the Gitea workflow yet — docker-in-docker in
CI is a separate question. Run it locally before changing anything under
`src/popup/views/`.
## Rationale
Common popular EVM wallets have become bloated with swap UIs, portfolio

16
TODO.md
View File

@@ -15,7 +15,8 @@ other branch is in flight: the settings About well landed as #145 on 2026-07-26
and scripts-to-rule-them-all landed as #148, so the `scripts/` directory
question is resolved. Full policy file set present. `make check` verified
passing on `main` at `23aeae4` on 2026-08-09. The 1.0.0 backlog is filed as
#149-#168.
#149-#168. A real-browser end-to-end suite (`make test-e2e`) now sits alongside
`make check`, which cannot see a runtime `ReferenceError` in a popup view.
# Next Step
@@ -27,6 +28,11 @@ review.
# Completed Steps
- 2026-08-09: Containerized Chrome end-to-end harness (`make test-e2e` /
`script/test-e2e`) driving the real popup with all network intercepted, plus
the two used-but-not-imported crashes it caught: AddToken unreachable (#150)
and TransactionDetail broken for every ERC-20 transfer (#151). Harness
demonstrated failing before the fixes and passing after (#181).
- 2026-08-09: Reviewed the repo end to end and filed the 1.0.0 backlog
(#149-#168).
- 2026-07-26: About well in settings with build info, repo link and the version
@@ -52,10 +58,14 @@ review.
# Future Steps
- Fix the two `ReferenceError` crashes that make whole screens unreachable:
AddToken (#150) and TransactionDetail for every ERC-20 transfer (#151).
- Add ESLint to `script/lint` (#152). `make check` is `prettier --check` only
and cannot catch undefined identifiers, which is how #150 and #151 shipped.
- Decide the libsodium backend that actually ships (#182) and delete the single
allowlist entry it owns in `tests/e2e/harness.js`.
- Extend the end-to-end suite to the dApp approval signing path (EIP-1193
through the real content script, background worker and approval popup), and
decide separately whether docker-in-docker makes `make test-e2e` runnable in
the Gitea workflow.
- Make the Firefox target functional: Chrome callback APIs are used against the
promise-only `browser` namespace (#153).
- Send and transaction-flow correctness: gas fee excluded from the

View File

@@ -16,6 +16,7 @@
"@tailwindcss/cli": "^4.2.1",
"esbuild": "^0.27.3",
"jest": "^30.2.0",
"playwright-core": "1.56.0",
"prettier": "^3.8.1",
"tailwindcss": "^4.2.1"
},

48
script/test-e2e Executable file
View File

@@ -0,0 +1,48 @@
#!/bin/sh
# script/test-e2e: build the extension and drive the real popup in a real
# Chromium inside a pinned container. Our own extension to
# scripts-to-rule-them-all.
#
# Deliberately NOT called by script/check or script/test: REPO_POLICIES.md
# caps make test at 20 seconds and a browser suite does not fit. Run it
# yourself before touching popup views; it is the only check that can see
# a used-but-not-imported identifier blow up at runtime.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# mcr.microsoft.com/playwright:v1.56.0-noble, 2026-08-09
#
# The playwright-core devDependency is pinned to the matching Playwright
# version (1.56.0) and the two must be bumped together: the browsers ship
# inside this image, and playwright-core looks for the exact browser
# revision its own version expects. A mismatch fails at launch.
IMAGE="mcr.microsoft.com/playwright@sha256:35246d87a7c88ea9b771c65d33171b2611b02a8253b4b12ce6f94376c55f99f2"
main() {
cd "$ROOT"
if ! command -v docker >/dev/null 2>&1; then
echo "test-e2e: docker is required to run the e2e suite" >&2
exit 1
fi
echo "Building extension for e2e..."
yarn run build 2>&1
echo "Running e2e suite in the pinned Playwright container..."
# --ipc=host: Chromium's shared-memory needs more than the default
# 64MB /dev/shm or renderers crash.
# --user: keep files the suite touches owned by the caller, not root.
# HOME=/tmp: the mapped uid has no home directory in the image.
docker run --rm \
--ipc=host \
--user "$(id -u):$(id -g)" \
-e HOME=/tmp \
-v "$ROOT:/work" \
-w /work \
"$IMAGE" \
node tests/e2e/run.js
}
main "$@"

View File

@@ -1,4 +1,4 @@
const { $, showFlash, goBack } = require("./helpers");
const { $, showView, showFlash, goBack } = require("./helpers");
const { getTopTokens } = require("../../shared/tokenList");
const { state, saveState } = require("../../shared/state");
const { lookupTokenInfo } = require("../../shared/balances");

View File

@@ -7,6 +7,7 @@ const {
showFlash,
flashCopyFeedback,
addressTitle,
addressDotHtml,
escapeHtml,
isoDate,
timeAgo,

188
tests/e2e/harness.js Normal file
View File

@@ -0,0 +1,188 @@
// End-to-end harness: launches a real Chromium with the unpacked MV3
// build loaded, collects every uncaught page error and console.error, and
// exposes the popup flows the tests drive.
//
// This runs inside the pinned Playwright container; see script/test-e2e.
// It is deliberately NOT part of make check — REPO_POLICIES.md caps
// make test at 20 seconds and a browser suite does not fit.
"use strict";
const fs = require("fs");
const os = require("os");
const path = require("path");
const { chromium } = require("playwright-core");
const { installNetworkStubs } = require("./network");
const REPO_ROOT = path.resolve(__dirname, "..", "..");
const EXT_PATH = path.join(REPO_ROOT, "dist", "chrome");
// Page errors that are known, tracked, and deliberately tolerated. Every
// entry must name the issue that will remove it. This list is the one
// concession in an otherwise zero-tolerance policy: an uncaught error is
// how this harness caught issue #150 in the first place.
const ALLOWED_ERRORS = [
{
// libsodium ships a WASM build and an asm.js fallback. The
// extension CSP (script-src 'self', with no wasm-unsafe-eval)
// refuses the WASM module on every popup load; libsodium catches
// it and falls back to asm.js, so the wallet works. Deciding
// which backend actually ships is issue #182, and this entry gets
// deleted when that lands.
issue: "#182",
pattern: /Refused to compile or instantiate WebAssembly module/,
},
];
function isAllowed(text) {
return ALLOWED_ERRORS.some((a) => a.pattern.test(text));
}
class ErrorCollector {
constructor() {
this.entries = [];
}
record(kind, text) {
const line = kind + ": " + String(text).split("\n")[0];
if (isAllowed(line)) return;
this.entries.push(line);
}
mark() {
return this.entries.length;
}
since(mark) {
return this.entries.slice(mark);
}
}
function attachErrorListeners(ctx, errors) {
const attachPage = (page) => {
page.on("pageerror", (err) => {
errors.record("pageerror", err.message || String(err));
});
page.on("console", (msg) => {
if (msg.type() === "error") {
errors.record("console.error", msg.text());
}
});
};
ctx.pages().forEach(attachPage);
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.
}
// 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 launch(routeOpts) {
if (!fs.existsSync(path.join(EXT_PATH, "manifest.json"))) {
throw new Error(
"no unpacked build at " +
EXT_PATH +
" — run make build before the e2e suite",
);
}
const userDir = fs.mkdtempSync(path.join(os.tmpdir(), "autistmask-e2e-"));
const ctx = await chromium.launchPersistentContext(userDir, {
// channel: "chromium" is load-bearing. The default headless mode
// uses the headless shell, which silently refuses to load
// extensions: there is no error at all, the service worker simply
// never appears. This cost real debugging time once already.
channel: "chromium",
headless: true,
args: [
"--disable-extensions-except=" + EXT_PATH,
"--load-extension=" + EXT_PATH,
// The container runs unprivileged; Chrome's sandbox needs
// capabilities the harness deliberately does not grant it.
"--no-sandbox",
],
});
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 });
},
};
}
// ---------------------------------------------------------------- flows
const PASSWORD = "e2e-harness-password";
async function visible(page, selector, timeout = 15000) {
await page.waitForSelector(selector, { state: "visible", timeout });
}
async function openPopup(ctx, popupUrl) {
const page = await ctx.newPage();
await page.goto(popupUrl);
return page;
}
// Full wallet creation through the real UI: BIP-39 generation, libsodium
// vault encryption and extension storage persistence, for real.
async function createWallet(page) {
await page.click("#btn-welcome-add");
await visible(page, "#view-add-wallet");
await page.click("#btn-generate-phrase");
await page.waitForFunction(() => {
const el = document.getElementById("wallet-mnemonic");
return el && el.value.trim().split(/\s+/).length >= 12;
});
await page.fill("#add-wallet-password", PASSWORD);
await page.fill("#add-wallet-password-confirm", PASSWORD);
await page.click("#btn-add-wallet-confirm");
await visible(page, "#view-main", 60000);
}
// Reach the address detail screen from wherever the popup restored to.
// Clicking .address-row does not open it; the [info] button does.
async function openAddressDetail(page) {
const onAddress = await page.isVisible("#view-address");
if (!onAddress) {
await visible(page, "#view-main");
await page.click("#wallet-list .btn-addr-info");
}
await visible(page, "#view-address");
}
module.exports = {
ALLOWED_ERRORS,
EXT_PATH,
REPO_ROOT,
createWallet,
launch,
openAddressDetail,
openPopup,
visible,
};

223
tests/e2e/network.js Normal file
View File

@@ -0,0 +1,223 @@
// Browser-level network interception for the end-to-end suite.
//
// Every http(s) request the extension makes 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.
//
// 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.
"use strict";
// Fictional ERC-20 used to seed the transaction-detail test. The symbol
// must not collide with any entry in src/shared/tokenList.js, or
// isSpoofedSymbol() in src/shared/transactions.js drops the transfer as a
// symbol-spoofing attempt; holders_count must be >= 1000 or the default
// hideLowHolderTokens filter drops it. Either would make the test pass
// vacuously by never rendering a row at all.
const STUB_TOKEN = {
address: "0xe2e0000000000000000000000000000000000e2e",
symbol: "E2E",
name: "End To End Test Token",
decimals: "6",
holders: "12345",
};
const STUB_COUNTERPARTY = "0xc0ffee0000000000000000000000000000c0ffee";
const STUB_TX_HASH =
"0xe2e0000000000000000000000000000000000000000000000000000000000e2e";
const STUB_BLOCK_NUMBER = 21000000;
// Fixed instant so timeAgo() output is stable across runs.
const STUB_TX_TIMESTAMP = "2026-01-02T03:04:05.000000Z";
// A 32-byte zero word. Returned for every eth_call, which is what makes
// ethers' ENS reverse lookup resolve to "no resolver set" and return null
// instead of throwing. A throw would be logged by src/shared/ens.js via
// log.errorf(), i.e. console.error, which fails the run on its own.
const ZERO_WORD = "0x" + "0".repeat(64);
const RPC_RESULTS = {
eth_chainId: "0x1",
net_version: "1",
eth_blockNumber: "0x1406f40",
eth_getBalance: "0x0",
eth_call: ZERO_WORD,
eth_gasPrice: "0x3b9aca00",
eth_estimateGas: "0x5208",
eth_getTransactionCount: "0x0",
eth_maxPriorityFeePerGas: "0x3b9aca00",
};
function tokenObject() {
return {
address_hash: STUB_TOKEN.address,
address: STUB_TOKEN.address,
symbol: STUB_TOKEN.symbol,
name: STUB_TOKEN.name,
decimals: STUB_TOKEN.decimals,
holders_count: STUB_TOKEN.holders,
type: "ERC-20",
};
}
// One received ERC-20 transfer of 1.5 E2E to the address under test.
function tokenTransferItems(address) {
return [
{
transaction_hash: STUB_TX_HASH,
block_number: STUB_BLOCK_NUMBER,
timestamp: STUB_TX_TIMESTAMP,
from: { hash: STUB_COUNTERPARTY },
to: { hash: address },
total: { decimals: STUB_TOKEN.decimals, value: "1500000" },
token: tokenObject(),
},
];
}
// Full details for STUB_TX_HASH. raw_input is "0x" so the calldata
// decoder short-circuits; the on-chain detail fields still populate.
function transactionDetails() {
return {
hash: STUB_TX_HASH,
block_number: STUB_BLOCK_NUMBER,
nonce: 7,
gas_used: "51000",
gas_price: "1000000000",
fee: { value: "51000000000000" },
raw_input: "0x",
status: "ok",
};
}
function jsonResponse(route, body) {
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(body),
});
}
// Extract the address from a Blockscout /addresses/<addr>/... path.
function blockscoutAddress(pathname) {
const m = pathname.match(/\/addresses\/(0x[0-9a-fA-F]{40})\//);
return m ? m[1] : null;
}
function handleRpc(route, postData, report) {
let payload;
try {
payload = JSON.parse(postData || "null");
} catch {
report("unstubbed RPC: unparseable body " + String(postData));
return route.abort();
}
// ethers batches by default, so the body may be an array.
const batch = Array.isArray(payload) ? payload : [payload];
const replies = batch.map((req) => {
const result = RPC_RESULTS[req.method];
if (result === undefined) {
report("unstubbed RPC method: " + req.method);
return {
jsonrpc: "2.0",
id: req.id,
error: { code: -32601, message: "unstubbed in e2e harness" },
};
}
return { jsonrpc: "2.0", id: req.id, result };
});
return jsonResponse(route, Array.isArray(payload) ? replies : replies[0]);
}
/**
* Route every http(s) request through local fixtures.
*
* @param {import("playwright-core").BrowserContext} ctx
* @param {object} opts
* @param {(text: string) => void} opts.report called for unstubbed traffic
* @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.
*/
async function installNetworkStubs(ctx, opts) {
const report = opts.report;
// 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;
// JSON-RPC endpoint (any host): a POST with a JSON-RPC body.
if (req.method() === "POST") {
return handleRpc(route, req.postData(), report);
}
// Blockscout v2
if (p.includes("/api/v2/")) {
if (/\/addresses\/0x[0-9a-fA-F]{40}\/transactions$/.test(p)) {
return jsonResponse(route, { items: [] });
}
if (/\/addresses\/0x[0-9a-fA-F]{40}\/token-transfers$/.test(p)) {
const addr = blockscoutAddress(p);
return jsonResponse(route, {
items:
opts.seedTokenTransfer && addr
? tokenTransferItems(addr)
: [],
});
}
if (/\/addresses\/0x[0-9a-fA-F]{40}\/token-balances$/.test(p)) {
return jsonResponse(route, []);
}
if (p.endsWith("/transactions/" + STUB_TX_HASH)) {
return jsonResponse(route, transactionDetails());
}
}
// CoinDesk price tick
if (url.hostname.endsWith("coindesk.com")) {
return jsonResponse(route, { Data: {} });
}
// MetaMask phishing blocklist
if (
url.hostname === "raw.githubusercontent.com" ||
p.endsWith("/eth-phishing-detect/main/src/config.json")
) {
return jsonResponse(route, {
version: 2,
tolerance: 2,
fuzzylist: [],
whitelist: [],
blacklist: [],
});
}
// Best-effort Etherscan address labels: served as an empty page.
if (url.hostname.endsWith("etherscan.io")) {
return route.fulfill({
status: 200,
contentType: "text/html",
body: "<html><body></body></html>",
});
}
report("unstubbed request: " + req.method() + " " + req.url());
return route.abort();
});
}
module.exports = {
installNetworkStubs,
STUB_TOKEN,
STUB_COUNTERPARTY,
STUB_TX_HASH,
};

188
tests/e2e/run.js Normal file
View 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;
});

View File

@@ -2547,6 +2547,11 @@ pkg-dir@^4.2.0:
dependencies:
find-up "^4.0.0"
playwright-core@1.56.0:
version "1.56.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.0.tgz#14b40ea436551b0bcefe19c5bfb8d1804c83739c"
integrity sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==
pngjs@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz"