pocket
Build and contribute

Test tiers

What each tier tests, why live tests are opt-in, and the two harness properties that keep the suite from passing vacuously.

The split is by file extension, and it is load-bearing

Two runners look at tests/:

PatternRunner
*.spec.tsPlaywright, the real built extension in real Chromium
*.test.tsvitest
*.live.*hits real testnet and submits real transactions

Playwright's default match also claims *.test.ts, so without the split it would load vitest files and report vitest's own startup error as a suite failure.

Running them

cd extension

npm test                # vitest over src/**/*.test.ts
npm run test:suites     # vitest over tests/{auth,failure,edge,qa}
npm run test:pass       # build, then Playwright over tests/
npm run test:e2e        # build, then Playwright over the older e2e/ specs
npm run test:live       # src/**/*.live.test.ts, against real testnet
npm run test:e2e:live   # the live browser specs

A single file:

npx vitest run src/core/crypto/field.test.ts
npx vitest run --config vitest.suites.config.ts tests/auth/lock-lifecycle.test.ts
npx playwright test -c playwright.tests.config.ts tests/happy/onboarding.spec.ts
npx playwright test -c playwright.tests.config.ts -g "an imported phrase"

The tiers

TierAsks
src/**do the primitives hold: fields, curve, sponge, derivation, witnesses, submission taxonomy, history mapping
tests/auththe lock, the message boundary, handle binding, the prover's sender check, the SEP-43 surface
tests/edgeevery shape of address, amount, memo, password, phrase and injection payload a user can produce
tests/failuredependencies that are down, slow, rate-limited or lying
tests/happyonboarding, a public payment, the private pocket end to end, recovery, on live testnet
tests/ui-statesevery async surface in loading, empty, error and success
tests/viewportthe popup ceiling, narrow windows, and Chrome's 500% zoom
tests/a11yroles, names, announcements, keyboard-only flows, reduced motion
tests/perfcold start, long waits, and whether the popup keeps painting during a proof
tests/lifecycleworker death, concurrency, the idle-lock hold-off
tests/integritytwo writers on one opening store, and upgrading over an install a previous version made
tests/qarelease qualification, driven by a risk model rather than by module

Two tiers worth calling out

tests/integrity/migration runs code that is no longer in the tree. An older commit's src is archived, built into its own output directory, and swapped into the same extension path the current build then occupies, because Chrome derives the extension id from that path. It is the only way to test data written by a version that no longer exists.

tests/a11y/keyboard calls .click() nowhere at all. That restriction is the test: a flow driven by clicks proves the handlers work, and only a flow driven by Tab, Enter and typing proves a keyboard user can finish it.

Live tests are opt-in everywhere

*.live.* files hit real testnet and submit real transactions, so they are excluded from every default run and gated behind POCKET_LIVE_E2E.

A testnet outage, an offline build machine or a clean clone must not be reported as a code failure. "All tests pass" has to be a statement about the code rather than about one developer's network.

Two harness properties that keep the suite honest

The stub has to reach the service worker

Every chain call in this extension happens in the worker, not on the page. A stub scoped to the page would see none of it, so every failure-injection test would keep passing while injecting nothing, and the suite would report a wallet that degrades honestly without ever having made it degrade.

So there is a spec that tests the harness: it asserts the route handler saw the request and that the request came from a service worker, and separately that an injected failure actually changes what the wallet does. A stub that is seen but cannot alter behaviour is decoration.

Ambient assertions

Every test in every tier fails on a console error, an uncaught exception, or a request to a host the wallet is not supposed to talk to, whether or not that test was looking for one.

Most real defects announce themselves that way long before a targeted assertion catches them. A suite that only checks what each test thought to check is blind to all of it.

Two modes, because switching this on across a suite that never had it is a measurement before it is a gate:

QA_AMBIENT=report   collect and print, fail nothing
QA_AMBIENT=fail     collect and fail the test

Unset behaves as fail.

Deliberate choices in the Playwright config

No screenshot baselines. Playwright writes the baseline on first run and compares against it forever, so the expected value is produced by the same system that produces the actual value. The suite asserts on roles, accessible names and the words on a signing screen instead, which survive a restyle and still break on a change to what the product says.

No retries. A test that only passes on the second attempt is a finding to report, not a number to raise.

Three workers, not one per core. Every test launches its own Chromium with an 18 MB unpacked extension, and several launch a second one for the other side of a transfer. Raising it made browser cold start the slowest thing in the suite and produced launch timeouts that had nothing to do with the wallet.

A 60-second action timeout. Playwright's default is to wait for the whole test to time out, so a fill() on a field that never appeared blocked for fifteen minutes and then reported a test timeout rather than the missing field.

Every test launches its own browser with its own profile and funds its own account, so nothing is shared and there are no ordering dependencies.

Environment variables

VariableDoes
POCKET_EXT_PATHrun against a different build, which is how mutation testing stays safe in a shared checkout
POCKET_LIVE_E2Eopt into the live browser specs
POCKET_TESTNET_SECRETa funded account for the live tiers
POCKET_TEST_ARCHIVE_URLpoint a test at a running archive
QA_AMBIENTreport or fail
PW_WORKERSoverride the worker count

Mutation testing

The suite is also run against deliberately broken builds, to check that it fails when the product is wrong rather than merely passing when it is right.

POCKET_EXT_PATH is what makes that safe: build the broken version once, copy it aside, restore the source and the real output, then point the specs at the copy. Nobody else's run sees a deliberately broken extension.

On this page