Compare commits
1 Commits
a6885a0d62
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f7f141a757 |
13
Makefile
13
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: bootstrap setup install test test-e2e lint fmt fmt-check check docker hooks build clean dev
|
.PHONY: bootstrap setup install test lint fmt fmt-check check docker hooks build build-debug clean dev
|
||||||
|
|
||||||
# Standard targets are thin shims; the implementations live in script/
|
# Standard targets are thin shims; the implementations live in script/
|
||||||
# per the scripts-to-rule-them-all pattern (see the Entrypoints section
|
# per the scripts-to-rule-them-all pattern (see the Entrypoints section
|
||||||
@@ -16,10 +16,6 @@ install:
|
|||||||
test:
|
test:
|
||||||
@script/test
|
@script/test
|
||||||
|
|
||||||
# Browser end-to-end suite. Requires docker; not part of check.
|
|
||||||
test-e2e:
|
|
||||||
@script/test-e2e
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@script/lint
|
@script/lint
|
||||||
|
|
||||||
@@ -42,6 +38,13 @@ build:
|
|||||||
@echo "Building extension..."
|
@echo "Building extension..."
|
||||||
@yarn run build 2>&1
|
@yarn run build 2>&1
|
||||||
|
|
||||||
|
# Development-only build: enables the red DEBUG / INSECURE banner and makes
|
||||||
|
# the hardcoded test recovery phrase the output of wallet creation. Never
|
||||||
|
# distribute the artifacts this produces.
|
||||||
|
build-debug:
|
||||||
|
@echo "Building extension (DEBUG)..."
|
||||||
|
@AUTISTMASK_DEBUG=1 yarn run build 2>&1
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@rm -rf dist/
|
@rm -rf dist/
|
||||||
|
|
||||||
|
|||||||
61
README.md
61
README.md
@@ -42,6 +42,23 @@ Load the extension:
|
|||||||
- **Firefox**: Navigate to `about:debugging#/runtime/this-firefox`, click "Load
|
- **Firefox**: Navigate to `about:debugging#/runtime/this-firefox`, click "Load
|
||||||
Temporary Add-on", and select `dist/firefox/manifest.json`.
|
Temporary Add-on", and select `dist/firefox/manifest.json`.
|
||||||
|
|
||||||
|
### Debug Builds
|
||||||
|
|
||||||
|
`make build` always produces a release build: the build-time `DEBUG` constant is
|
||||||
|
`false`, so wallet creation uses real entropy and the red banner is off. To
|
||||||
|
produce a debug build instead, set `AUTISTMASK_DEBUG=1` in the environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build-debug # or: AUTISTMASK_DEBUG=1 make build
|
||||||
|
```
|
||||||
|
|
||||||
|
Only the exact value `1` enables it; any other value (including unset, empty, or
|
||||||
|
`true`) yields a release build, so a typo cannot accidentally ship the debug
|
||||||
|
behavior. The build prints which mode it used. See the
|
||||||
|
[DEBUG Mode Policy](#debug-mode-policy) for what the flag changes. **Never
|
||||||
|
distribute a debug build** — every wallet it creates gets the same publicly
|
||||||
|
known test recovery phrase.
|
||||||
|
|
||||||
## Entrypoints
|
## Entrypoints
|
||||||
|
|
||||||
This repository adheres to the
|
This repository adheres to the
|
||||||
@@ -56,8 +73,6 @@ provide:
|
|||||||
git pre-commit hook
|
git pre-commit hook
|
||||||
- `script/projectname` — print the project name (used for the Docker image tag)
|
- `script/projectname` — print the project name (used for the Docker image tag)
|
||||||
- `script/test` — run the test suite (jest)
|
- `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/lint` — run the linter
|
||||||
- `script/fmt` — format all files (writes)
|
- `script/fmt` — format all files (writes)
|
||||||
- `script/fmt-check` — check formatting (read-only)
|
- `script/fmt-check` — check formatting (read-only)
|
||||||
@@ -67,35 +82,6 @@ provide:
|
|||||||
- `script/precommit` — run by the git pre-commit hook; runs `script/check`
|
- `script/precommit` — run by the git pre-commit hook; runs `script/check`
|
||||||
- `script/install-precommit` — install the git pre-commit hook
|
- `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
|
## Rationale
|
||||||
|
|
||||||
Common popular EVM wallets have become bloated with swap UIs, portfolio
|
Common popular EVM wallets have become bloated with swap UIs, portfolio
|
||||||
@@ -699,6 +685,19 @@ flows, or alter program behavior beyond the banner and the hardcoded mnemonic.
|
|||||||
Adding new DEBUG-conditional branches requires explicit approval from the
|
Adding new DEBUG-conditional branches requires explicit approval from the
|
||||||
project owner.
|
project owner.
|
||||||
|
|
||||||
|
`DEBUG` is a build-time constant, not a runtime setting. `build.js` injects it
|
||||||
|
into the bundle as the `__BUILD_DEBUG__` define — `false` unless the build was
|
||||||
|
run with `AUTISTMASK_DEBUG=1` (see [Debug Builds](#debug-builds)) — and
|
||||||
|
`src/shared/constants.js` reads it. It cannot be changed after the bundle is
|
||||||
|
produced.
|
||||||
|
|
||||||
|
The debug-mode toggle in settings is a separate, runtime-only flag. It raises
|
||||||
|
the log level and turns the banner on, and that is all it may ever do: it feeds
|
||||||
|
`isDebug()` in `src/shared/log.js`, which is deliberately not what
|
||||||
|
`generateMnemonic()` consults. Mnemonic generation reads the build-time `DEBUG`
|
||||||
|
constant directly, so no runtime toggle in a release build can reach the
|
||||||
|
hardcoded test phrase.
|
||||||
|
|
||||||
### Key Decisions
|
### Key Decisions
|
||||||
|
|
||||||
- **No framework**: The popup UI is vanilla JS and HTML. The extension is small
|
- **No framework**: The popup UI is vanilla JS and HTML. The extension is small
|
||||||
|
|||||||
74
TODO.md
74
TODO.md
@@ -10,30 +10,31 @@
|
|||||||
|
|
||||||
# Status
|
# Status
|
||||||
|
|
||||||
pre-1.0. Tagged v0.1.0 on 2026-02-27. Working toward the 1.0.0 milestone. Full
|
pre-1.0, working towards the 1.0.0 milestone. Tagged v0.1.0 on 2026-02-27. No
|
||||||
policy file set present. `make check` is green on this branch; a real browser
|
other branch is in flight: the settings About well landed as #145 on 2026-07-26
|
||||||
end-to-end suite (`make test-e2e`) now exists alongside it, because `make check`
|
and scripts-to-rule-them-all landed as #148, so the `scripts/` directory
|
||||||
cannot see a runtime `ReferenceError` in a popup view.
|
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.
|
||||||
|
|
||||||
# Next Step
|
# Next Step
|
||||||
|
|
||||||
Verify `main` passes `make check` after this branch merges and fix anything red;
|
Land #149: make `DEBUG` a build-time constant that defaults to off, injected as
|
||||||
`main` must always be green.
|
the `__BUILD_DEBUG__` esbuild define from `AUTISTMASK_DEBUG=1`, so a plain
|
||||||
|
`make build` stops handing every newly created wallet the publicly committed
|
||||||
|
test recovery phrase. Branch `fix/issue-149-debug-build-flag`; PR open, awaiting
|
||||||
|
review.
|
||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- 2026-08-09 Containerized Chrome end-to-end harness (`make test-e2e` /
|
- 2026-08-09: Reviewed the repo end to end and filed the 1.0.0 backlog
|
||||||
`script/test-e2e`) driving the real popup with all network intercepted, plus
|
(#149-#168).
|
||||||
the two used-but-not-imported crashes it caught: AddToken unreachable (#150)
|
- 2026-07-26: About well in settings with build info, repo link and the version
|
||||||
and TransactionDetail broken for every ERC-20 transfer (#151). Harness
|
click easter egg (#145); proper view navigation stack (#146).
|
||||||
demonstrated failing before the fixes and passing after (#181).
|
|
||||||
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile
|
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile
|
||||||
shims, README Entrypoints section
|
shims, README Entrypoints section (#148)
|
||||||
- 2026-08-09 Landed the settings About well (build info, app name and repo link,
|
- 2026-03-01: USD display suppressed on testnets (#142); estimated USD for ETH
|
||||||
release date, version click easter egg) as `23aeae4` (#145).
|
in approve-tx view (#141).
|
||||||
- 2026-03-01: About well in settings with build info and easter egg (in flight
|
|
||||||
on feature branch); USD display suppressed on testnets (#142); estimated USD
|
|
||||||
for ETH in approve-tx view (#141).
|
|
||||||
- Sepolia testnet support (#137); etherscan links go to token-specific URLs
|
- Sepolia testnet support (#137); etherscan links go to token-specific URLs
|
||||||
(#136).
|
(#136).
|
||||||
- Transaction detail improvements: Type field and on-chain details (#130),
|
- Transaction detail improvements: Type field and on-chain details (#130),
|
||||||
@@ -51,15 +52,32 @@ Verify `main` passes `make check` after this branch merges and fix anything red;
|
|||||||
|
|
||||||
# Future Steps
|
# Future Steps
|
||||||
|
|
||||||
- Decide the libsodium backend that actually ships (#182) and delete the single
|
- Fix the two `ReferenceError` crashes that make whole screens unreachable:
|
||||||
allowlist entry it owns in `tests/e2e/harness.js`.
|
AddToken (#150) and TransactionDetail for every ERC-20 transfer (#151).
|
||||||
- Extend the end-to-end suite to the dApp approval signing path (EIP-1193
|
- Add ESLint to `script/lint` (#152). `make check` is `prettier --check` only
|
||||||
through the real content script, background worker and approval popup), and
|
and cannot catch undefined identifiers, which is how #150 and #151 shipped.
|
||||||
decide separately whether docker-in-docker makes `make test-e2e` runnable in
|
- Make the Firefox target functional: Chrome callback APIs are used against the
|
||||||
the Gitea workflow.
|
promise-only `browser` namespace (#153).
|
||||||
- Prune stale branches: dozens of merged local and remote feature branches
|
- Send and transaction-flow correctness: gas fee excluded from the
|
||||||
remain (fix/_, feature/_, tx-\*); delete merged ones locally and on origin.
|
insufficient-balance check (#154), WaitTx 60s timeout overwriting a rendered
|
||||||
- Continue the issue backlog toward a feature-complete wallet, then cut further
|
success screen (#155), last-wallet deletion leaving inconsistent state (#156).
|
||||||
tags as milestones land.
|
- Security: plaintext password crossing the extension messaging boundary during
|
||||||
|
dApp approvals (#157); MV3 service worker termination killing the background
|
||||||
|
refresh and the 24h phishing list update (#158).
|
||||||
|
- Test the crypto core — `wallet.js` derivation and `vault.js` encryption (#159)
|
||||||
|
— and the address-poisoning defense in `transactions.js` (#160).
|
||||||
|
- Wallet features for 1.0: show a wallet's recovery phrase behind the password
|
||||||
|
(#161), delete an address from an HD wallet (#162).
|
||||||
|
- Docs: `docs/README.md` contradicts the code on external services and names
|
||||||
|
competitors (#163); README Screen Map omits three shipped screens (#164).
|
||||||
|
- Owner decisions: Sepolia support versus "Non-Goals for 1.0", and `isMetaMask`
|
||||||
|
naming a competitor in shipped code (#165).
|
||||||
|
- Repo policy compliance sweep: test rerun pattern, `yarn`/`npx`, frozen
|
||||||
|
lockfile, undocumented Makefile targets (#166).
|
||||||
|
- Prune the 24 stale remote feature branches (#167).
|
||||||
|
- Remove dead exports and de-duplicate copy-pasted view helpers (#168).
|
||||||
- Pre-1.0 security review of the extension (key handling, DEBUG mode policy, RPC
|
- Pre-1.0 security review of the extension (key handling, DEBUG mode policy, RPC
|
||||||
input validation) before any 1.0rc tag.
|
input validation) before any 1.0rc tag; #149 and #157 are parts of it, but the
|
||||||
|
review is broader than either.
|
||||||
|
- Cut 1.0.0 once the milestone is empty, then continue tagging as milestones
|
||||||
|
land.
|
||||||
|
|||||||
16
build.js
16
build.js
@@ -11,6 +11,14 @@ function ensureDir(dir) {
|
|||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DEBUG is a build-time flag, off unless explicitly requested. It is the only
|
||||||
|
// thing that makes the hardcoded test mnemonic reachable, so the opt-in must be
|
||||||
|
// exact: anything other than the literal "1" (unset, empty, "true", a typo)
|
||||||
|
// produces a release build. Failing towards the safe mode is deliberate.
|
||||||
|
function isDebugBuild() {
|
||||||
|
return process.env.AUTISTMASK_DEBUG === "1";
|
||||||
|
}
|
||||||
|
|
||||||
function getBuildInfo() {
|
function getBuildInfo() {
|
||||||
const pkg = JSON.parse(
|
const pkg = JSON.parse(
|
||||||
fs.readFileSync(path.join(__dirname, "package.json"), "utf8"),
|
fs.readFileSync(path.join(__dirname, "package.json"), "utf8"),
|
||||||
@@ -47,7 +55,15 @@ async function build() {
|
|||||||
const buildInfo = getBuildInfo();
|
const buildInfo = getBuildInfo();
|
||||||
console.log("Build info:", buildInfo);
|
console.log("Build info:", buildInfo);
|
||||||
|
|
||||||
|
const debugBuild = isDebugBuild();
|
||||||
|
console.log(
|
||||||
|
debugBuild
|
||||||
|
? "Build mode: DEBUG (INSECURE - hardcoded test mnemonic, do not ship)"
|
||||||
|
: "Build mode: release (DEBUG off)",
|
||||||
|
);
|
||||||
|
|
||||||
const define = {
|
const define = {
|
||||||
|
__BUILD_DEBUG__: JSON.stringify(debugBuild),
|
||||||
__BUILD_VERSION__: JSON.stringify(buildInfo.version),
|
__BUILD_VERSION__: JSON.stringify(buildInfo.version),
|
||||||
__BUILD_LICENSE__: JSON.stringify(buildInfo.license),
|
__BUILD_LICENSE__: JSON.stringify(buildInfo.license),
|
||||||
__BUILD_AUTHOR__: JSON.stringify(buildInfo.author),
|
__BUILD_AUTHOR__: JSON.stringify(buildInfo.author),
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
"@tailwindcss/cli": "^4.2.1",
|
"@tailwindcss/cli": "^4.2.1",
|
||||||
"esbuild": "^0.27.3",
|
"esbuild": "^0.27.3",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
"playwright-core": "1.56.0",
|
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"tailwindcss": "^4.2.1"
|
"tailwindcss": "^4.2.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
#!/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 "$@"
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const { $, showView, showFlash, goBack } = require("./helpers");
|
const { $, showFlash, goBack } = require("./helpers");
|
||||||
const { getTopTokens } = require("../../shared/tokenList");
|
const { getTopTokens } = require("../../shared/tokenList");
|
||||||
const { state, saveState } = require("../../shared/state");
|
const { state, saveState } = require("../../shared/state");
|
||||||
const { lookupTokenInfo } = require("../../shared/balances");
|
const { lookupTokenInfo } = require("../../shared/balances");
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const {
|
|||||||
showFlash,
|
showFlash,
|
||||||
flashCopyFeedback,
|
flashCopyFeedback,
|
||||||
addressTitle,
|
addressTitle,
|
||||||
addressDotHtml,
|
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
isoDate,
|
isoDate,
|
||||||
timeAgo,
|
timeAgo,
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
const DEBUG = true;
|
// DEBUG is a build-time constant injected by esbuild's define in build.js
|
||||||
|
// (see src/shared/buildInfo.js for the same pattern). It is false unless the
|
||||||
|
// bundle was produced with AUTISTMASK_DEBUG=1, and it is false whenever the
|
||||||
|
// module is loaded outside a bundle (tests, plain require). It must never be
|
||||||
|
// derived from anything the user can change at runtime: it is what gates the
|
||||||
|
// hardcoded test mnemonic below.
|
||||||
|
/* global __BUILD_DEBUG__ */
|
||||||
|
const DEBUG = typeof __BUILD_DEBUG__ !== "undefined" ? __BUILD_DEBUG__ : false;
|
||||||
|
|
||||||
const DEBUG_MNEMONIC =
|
const DEBUG_MNEMONIC =
|
||||||
"cube evolve unfold result inch risk jealous skill hotel bulb night wreck";
|
"cube evolve unfold result inch risk jealous skill hotel bulb night wreck";
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ const { Mnemonic, HDNodeWallet, Wallet } = require("ethers");
|
|||||||
const { DEBUG, DEBUG_MNEMONIC, BIP44_ETH_PATH } = require("./constants");
|
const { DEBUG, DEBUG_MNEMONIC, BIP44_ETH_PATH } = require("./constants");
|
||||||
|
|
||||||
function generateMnemonic() {
|
function generateMnemonic() {
|
||||||
|
// This must stay the compile-time DEBUG constant. Do NOT switch it to
|
||||||
|
// isDebug() from log.js: that also ORs in the runtime debugMode flag the
|
||||||
|
// settings toggle drives, which would let a user of a release build turn
|
||||||
|
// the hardcoded, publicly known test phrase back on for real wallets.
|
||||||
if (DEBUG) return DEBUG_MNEMONIC;
|
if (DEBUG) return DEBUG_MNEMONIC;
|
||||||
const m = Mnemonic.fromEntropy(
|
const m = Mnemonic.fromEntropy(
|
||||||
globalThis.crypto.getRandomValues(new Uint8Array(16)),
|
globalThis.crypto.getRandomValues(new Uint8Array(16)),
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
// 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,
|
|
||||||
};
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
// 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
188
tests/e2e/run.js
@@ -1,188 +0,0 @@
|
|||||||
// 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;
|
|
||||||
});
|
|
||||||
94
tests/wallet.test.js
Normal file
94
tests/wallet.test.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// Tests for the DEBUG build flag as it gates mnemonic generation.
|
||||||
|
//
|
||||||
|
// The modules read the __BUILD_DEBUG__ global that esbuild replaces at bundle
|
||||||
|
// time. Under jest the global is absent, which is exactly the release-build
|
||||||
|
// case; the debug-build case is exercised by defining the global and
|
||||||
|
// re-requiring the modules with a fresh registry.
|
||||||
|
|
||||||
|
const WORDS_IN_12_WORD_PHRASE = 12;
|
||||||
|
|
||||||
|
function loadWallet() {
|
||||||
|
const constants = require("../src/shared/constants");
|
||||||
|
const wallet = require("../src/shared/wallet");
|
||||||
|
const log = require("../src/shared/log");
|
||||||
|
return { constants, wallet, log };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("generateMnemonic in a release build", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
delete globalThis.__BUILD_DEBUG__;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("DEBUG defaults to false when the build define is absent", () => {
|
||||||
|
const { constants } = loadWallet();
|
||||||
|
expect(constants.DEBUG).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns fresh, valid 12-word phrases that are not the test phrase", () => {
|
||||||
|
const { constants, wallet } = loadWallet();
|
||||||
|
|
||||||
|
const first = wallet.generateMnemonic();
|
||||||
|
const second = wallet.generateMnemonic();
|
||||||
|
|
||||||
|
expect(first).not.toBe(second);
|
||||||
|
for (const phrase of [first, second]) {
|
||||||
|
expect(wallet.isValidMnemonic(phrase)).toBe(true);
|
||||||
|
expect(phrase.split(" ")).toHaveLength(WORDS_IN_12_WORD_PHRASE);
|
||||||
|
expect(phrase).not.toBe(constants.DEBUG_MNEMONIC);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("derives a usable HD wallet from the generated phrase", () => {
|
||||||
|
const { wallet } = loadWallet();
|
||||||
|
|
||||||
|
const { xpub, firstAddress } = wallet.hdWalletFromMnemonic(
|
||||||
|
wallet.generateMnemonic(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(xpub.startsWith("xpub")).toBe(true);
|
||||||
|
expect(firstAddress).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the runtime debug toggle cannot re-enable the test phrase", () => {
|
||||||
|
const { constants, wallet, log } = loadWallet();
|
||||||
|
|
||||||
|
// What the settings easter-egg toggle does at runtime.
|
||||||
|
log.setRuntimeDebug(true);
|
||||||
|
expect(log.isDebug()).toBe(true);
|
||||||
|
|
||||||
|
const phrase = wallet.generateMnemonic();
|
||||||
|
expect(phrase).not.toBe(constants.DEBUG_MNEMONIC);
|
||||||
|
expect(wallet.isValidMnemonic(phrase)).toBe(true);
|
||||||
|
expect(phrase).not.toBe(wallet.generateMnemonic());
|
||||||
|
|
||||||
|
log.setRuntimeDebug(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generateMnemonic in a debug build", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
globalThis.__BUILD_DEBUG__ = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete globalThis.__BUILD_DEBUG__;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("DEBUG is true and the test phrase is returned", () => {
|
||||||
|
const { constants, wallet } = loadWallet();
|
||||||
|
|
||||||
|
expect(constants.DEBUG).toBe(true);
|
||||||
|
expect(wallet.generateMnemonic()).toBe(constants.DEBUG_MNEMONIC);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the test phrase is itself a valid 12-word BIP-39 phrase", () => {
|
||||||
|
const { constants, wallet } = loadWallet();
|
||||||
|
|
||||||
|
expect(wallet.isValidMnemonic(constants.DEBUG_MNEMONIC)).toBe(true);
|
||||||
|
expect(constants.DEBUG_MNEMONIC.split(" ")).toHaveLength(
|
||||||
|
WORDS_IN_12_WORD_PHRASE,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2547,11 +2547,6 @@ pkg-dir@^4.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
find-up "^4.0.0"
|
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:
|
pngjs@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz"
|
resolved "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user