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/:
| Pattern | Runner |
|---|---|
*.spec.ts | Playwright, the real built extension in real Chromium |
*.test.ts | vitest |
*.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 specsA 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
| Tier | Asks |
|---|---|
src/** | do the primitives hold: fields, curve, sponge, derivation, witnesses, submission taxonomy, history mapping |
tests/auth | the lock, the message boundary, handle binding, the prover's sender check, the SEP-43 surface |
tests/edge | every shape of address, amount, memo, password, phrase and injection payload a user can produce |
tests/failure | dependencies that are down, slow, rate-limited or lying |
tests/happy | onboarding, a public payment, the private pocket end to end, recovery, on live testnet |
tests/ui-states | every async surface in loading, empty, error and success |
tests/viewport | the popup ceiling, narrow windows, and Chrome's 500% zoom |
tests/a11y | roles, names, announcements, keyboard-only flows, reduced motion |
tests/perf | cold start, long waits, and whether the popup keeps painting during a proof |
tests/lifecycle | worker death, concurrency, the idle-lock hold-off |
tests/integrity | two writers on one opening store, and upgrading over an install a previous version made |
tests/qa | release 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 testUnset 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
| Variable | Does |
|---|---|
POCKET_EXT_PATH | run against a different build, which is how mutation testing stays safe in a shared checkout |
POCKET_LIVE_E2E | opt into the live browser specs |
POCKET_TESTNET_SECRET | a funded account for the live tiers |
POCKET_TEST_ARCHIVE_URL | point a test at a running archive |
QA_AMBIENT | report or fail |
PW_WORKERS | override 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.