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.
189 lines
6.2 KiB
JavaScript
189 lines
6.2 KiB
JavaScript
// 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,
|
|
};
|