walletqa
v1.0.0-beta.19
Published
Playwright-based multi-chain wallet E2E testing library
Downloads
1,325
Maintainers
Readme
walletqa
Playwright-based E2E testing library for web apps that integrate with browser extension wallets.
Test your dApp against real wallet extensions (Trust Wallet, MetaMask, Phantom, Rabby, OKX, Rainbow, TONkeeper) across EVM, Solana, and TON chains.
Why walletqa?
- Real extension testing — headed browser with actual wallet extensions, not mocks or stubs
- 7 wallets — Trust Wallet, Phantom, Rabby, OKX, Rainbow, TONkeeper, MetaMask
- Cached profiles — onboard once (~35s), reuse the cached profile in every test (~7s)
- Config-driven engine — declarative wallet definitions replace hundreds of lines of imperative code
- Playwright-native — uses
test.extend()fixtures, works with your existing Playwright setup
Quick Start
npm install -D walletqa @playwright/test
npx walletqa init --wallet trust-wallet
npx walletqa cache tests/wallet-setup
npx playwright test --project=extensionHow It Works
walletqa loads real Chrome extensions into a Playwright browser context, restores a cached profile (so the wallet is already set up), and provides fixtures to approve/reject wallet popups from your test code.
1. defineWalletSetup() → onboard wallet once, snapshot browser profile
2. walletqa cache → build cached profiles for all wallets
3. Test runs → restore profile → auto-unlock → run dApp test
4. approve/reject → detect popup → click confirm/cancel → close popupSetup Guide
1. Define a wallet setup (one-time onboarding)
Create tests/wallet-setup/trust-wallet.setup.ts:
import { defineWalletSetup } from 'walletqa/cache';
import { TrustWalletPageObject } from 'walletqa/wallets';
const SEED = 'test test test test test test test test test test test junk';
const PASSWORD = 'TestPassword123!';
export default defineWalletSetup('trust-wallet', async (_context, walletPage) => {
const tw = walletPage as TrustWalletPageObject;
await tw.importWallet(SEED, PASSWORD);
});2. Build the cache
npx walletqa cache tests/wallet-setupThis launches a headed browser, runs the onboarding flow, and snapshots the browser profile. You only need to do this once — or when the setup function changes.
3. Write a test
Create tests/connect.extension.test.ts:
import { expect } from '@playwright/test';
import { createWalletFixture } from 'walletqa/fixtures';
import trustWalletSetup from './wallet-setup/trust-wallet.setup.js';
const test = createWalletFixture({
wallet: 'trust-wallet',
password: 'TestPassword123!',
cacheHash: trustWalletSetup.hash,
});
test('connect wallet to dApp', async ({
walletPage,
approveConnection,
}) => {
await walletPage.goto('http://localhost:3000');
await walletPage.click('[data-testid="connect-btn"]');
// Trust Wallet popup appears — approve it
await approveConnection();
// Assert your dApp received the wallet address
const address = walletPage.locator('[data-testid="wallet-address"]');
await expect(address).toBeVisible();
await expect(address).toHaveText(/^0x[a-fA-F0-9]{40}$/);
});4. Configure Playwright
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 60_000,
projects: [
{
name: 'extension',
use: { ...devices['Desktop Chrome'], headless: false },
testMatch: '**/*.extension.test.ts',
fullyParallel: false,
workers: 1, // Extension tests must run sequentially
},
],
});5. Run
npx playwright test --project=extensionConfig-Driven Engine
walletqa includes a declarative engine that replaces imperative page objects with data-driven wallet definitions. Instead of writing 300+ lines of TypeScript per wallet, you write ~100 lines of config.
Using the engine directly
import { ApprovalEngine, getWalletDefinition } from 'walletqa/engine';
const def = getWalletDefinition('trust-wallet');
const engine = new ApprovalEngine({
context: walletContext,
definition: def,
extensionId,
walletPassword: 'TestPassword123!',
screenshotMode: 'always', // capture all pages before + after every action
});
// Approve a connection popup
await engine.approve('connect');
// Reject a transaction popup
await engine.reject('transaction');
// Attach screenshots to Playwright HTML report
import { attachCaptures } from 'walletqa/ci';
await attachCaptures(testInfo, engine.getScreenshots());Screenshot modes: 'off' (zero overhead), 'on-failure' (default — only on timeout), 'always' (before + after every action). When enabled, captures every open page — dApp, extension popup, and extension home — for visual debugging and AI-assisted selector updates.
Wallet definitions
A WalletDefinition fully describes how to interact with a wallet as pure data:
export const trustWalletDef: WalletDefinition = {
brand: 'trust-wallet',
extensionStoreId: 'egjidjbpglichdcondbcbdnbeeppgdph',
chains: ['evm', 'ton', 'solana'],
protocol: 'eip-6963',
pages: {
home: 'home.html',
onboarding: 'home.html',
notification: 'notification.html',
},
mnemonic: {
wordCount: 12,
type: 'bip39',
inputStyle: 'textarea',
inputSelector: 'textarea',
},
onboarding: {
steps: [
{ label: 'Accept terms', action: 'check', selector: 'input[type="checkbox"]' },
{ label: 'I already have a wallet', action: 'click',
selector: 'button:has-text("I already have a wallet")' },
// ... more steps
],
readySelector: '[class*="Balance"], [class*="balance"]',
},
approval: {
connect: { confirm: 'button:has-text("Connect")', reject: 'button:has-text("Cancel")' },
transaction: { confirm: 'button:has-text("Confirm")', reject: 'button:has-text("Cancel")' },
signature: { confirm: 'button:has-text("Confirm")', reject: 'button:has-text("Cancel")' },
},
quirks: {
popupStyle: 'new-page',
autoOpensOnboarding: true,
},
};Three wallet definitions are included: Tonkeeper, Trust Wallet, and OKX. See src/engine/wallet-defs/ for the full configs.
Resilience features
- SES iframe support —
frameproperty onApprovalActiontargets buttons inside sandboxed iframes (OKX SES). Theses-pointer-dispatchclick strategy dispatches the full pointer event sequence React 18+ needs inside iframes - Post-click verification —
quirks.postClickVerificationchecks the button actually disappeared after clicking, and retries with escalating strategies (evaluate-click→ses-pointer-dispatch) if it didn't - Custom click strategies —
registerClickStrategy(name, fn)lets you define your own click handler for wallets with unique sandbox architectures - Smart fallbacks — when all configured selectors fail, the engine tries common button text patterns ("Confirm", "Approve", "Connect", "Sign") as a recovery layer. Enabled per wallet via
quirks.enableSmartFallbacks - Auto-screenshot on failure — when approval detection fails, screenshots of all open pages are saved to
test-results/approval-failures/for debugging - Structured error diagnostics —
ApprovalTimeoutErrorincludes which detection phases were tried, which selectors failed, and which pages were open isClosed()guards — all approval operations check page state before interacting, preventing crashes when popups close mid-interaction- Extension version pinning —
--pin-versionaccepts exact versions (5.0.0) and semver ranges (^4.0.0,~3.2.0,>=5.0.0,4.x) - Post-action hooks —
registerPostActionHook(name, fn)runs custom logic (logging, metrics, screenshots) after every approve/reject action - dApp iframe widget support —
DappIframeContexthelper for testing wallet interactions triggered from embedded iframes (payment widgets, bridges, DEX aggregators) - Nightly selector validation — CI workflow runs
walletqa validate-selectorsnightly and auto-opens GitHub issues when selectors break - Pre-flight health check —
runPreflightChecks()validates cache, extension, and version compatibility before browser launch. Fails fast with actionable messages instead of cryptic timeouts - Retry-aware flake detection — tests that pass on retry are automatically annotated as
walletqa:flakein the HTML report, making it easy to identify unreliable tests - Video recording on failure —
video: 'retain-on-failure'records video of every test but only keeps it for failures, providing visual debugging without retry overhead - LLM fallback — when all selectors and smart fallbacks fail, optionally queries an LLM (OpenAI, Google Generative AI, or Ollama) to find the button via accessibility tree analysis. Entirely opt-in via
llmFallbackconfig onApprovalEngine. Logs a warning when used so you know to update your selectors - Failure root cause classification —
classifyFailure()auto-categorizes test errors into actionable classes:selector-miss,popup-timeout,cache-stale,network-error,extension-crash, orunknown. Used byFlakeReporterandwalletqa doctor --flakesfor automated triage - Flakiness tracking —
FlakeReporter(a Playwright Reporter) persists pass/fail/retry results to.walletqa/history.jsonand computes per-test flake scores with trend analysis. Tests below 70% pass rate are flagged for quarantine. See Flake Tracking below - Visual change detection —
compareScreenshot()diffs wallet popup screenshots against stored baselines usingpixelmatch. Detects UI regressions from extension updates. Requirespixelmatchandpngjsas optional peer deps
For a deep dive into the engine architecture, see docs/wallet-e2e-engine.md.
Flake Tracking
Add the FlakeReporter to your playwright.config.ts to automatically record test results and track flakiness over time:
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
['html'],
['walletqa/ci/flake-reporter'], // Records results to .walletqa/history.json
],
// ...
});The reporter extracts wallet and action from test tags (@trust-wallet, @connect, etc.), classifies failures by root cause, and persists results to .walletqa/history.json. View flake scores with:
npx walletqa doctor --flakes # Top flaky tests with pass rates, trends, and quarantine suggestionsEach test gets a flake score based on pass rate, retry-pass frequency, and trend direction (improving/stable/degrading). Tests below 70% pass rate are flagged for quarantine.
Fixture API
createWalletFixture(options) returns a Playwright test object with these fixtures:
| Fixture | Type | Description |
|---------|------|-------------|
| walletPage | Page | The dApp page (not the extension) |
| walletContext | BrowserContext | Browser context with the extension loaded |
| extensionId | string | The Chrome extension ID |
| walletPageObject | WalletPageObject | Direct access to wallet page object methods |
| approveConnection() | () => Promise<void> | Approve a wallet connection popup |
| rejectConnection() | () => Promise<void> | Reject a wallet connection popup |
| approveTransaction() | () => Promise<void> | Approve a transaction popup |
| rejectTransaction() | () => Promise<void> | Reject a transaction popup |
| approveSignature() | () => Promise<void> | Approve a signature popup |
| rejectSignature() | () => Promise<void> | Reject a signature popup |
| captureState() | (label) => Promise | Capture screenshots of all open pages |
| walletEvents | WalletEventLogger | Structured event log for debugging |
| preflightResult | PreflightResult | Pre-flight health check results (cache, extension, version) |
Options
| Option | Type | Description |
|--------|------|-------------|
| wallet | WalletBrand | Which wallet to use |
| password | string | Wallet password for auto-unlock |
| mnemonic | string | Seed phrase for onboarding |
| cacheHash | string | Cache key from defineWalletSetup() |
| extensionVersion | string | Specific extension version to download |
| extensionPath | string | Path to local extension directory |
| pinnedVersion | string | Pin extension to a version — fails on mismatch |
| launchArgs | string[] | Additional Chrome launch arguments |
Multi-Wallet Parametric Tests
Run the same test across multiple wallets with one definition:
import { createMultiWalletTest } from 'walletqa/fixtures';
const test = createMultiWalletTest(['trust-wallet', 'okx', 'phantom'], {
walletPassword: 'TestPassword123!',
});
test('connect wallet', async ({ walletPage, walletBrand }) => {
await walletPage.goto('http://localhost:3000');
await walletPage.click('[data-testid="connect-btn"]');
console.log(`Testing with ${walletBrand}`);
});This generates three test variants: [trust-wallet] connect wallet, [okx] connect wallet, [phantom] connect wallet.
Supported Wallets
| Wallet | Chains | dApp E2E | Engine Def | |--------|--------|----------|------------| | Trust Wallet | EVM, TON, Solana | connect + sign + reject | Yes | | TONkeeper | TON | connect + sign + reject + tx | Yes | | OKX | EVM, TON, Solana | connect + sign + reject | Yes | | MetaMask | EVM | — (use Synpress) | — | | Phantom | Solana, EVM | connect + sign + reject | Yes | | Rabby | EVM | connect + sign + reject | Yes | | Rainbow | EVM | onboarding only | — |
dApp E2E = tests that connect to a real test dApp, approve/reject in the wallet popup, and verify the dApp receives the result.
Engine Def = declarative WalletDefinition config available in src/engine/wallet-defs/.
CLI
npx walletqa init # Generate project scaffolding
npx walletqa cache [setupDir] # Build wallet setup caches
npx walletqa cache [setupDir] --pin-version 4.2.0 # Pin to specific extension version
npx walletqa cache:list # List cached setups with hash and age
npx walletqa cache:clean # Remove all caches
npx walletqa doctor # Check environment: Node, Playwright, extensions
npx walletqa doctor --json # Machine-readable diagnostic output
npx walletqa test [testGlob] # Build cache + run extension tests
npx walletqa test --wallet trust-wallet # Filter by wallet
npx walletqa test --chain ton # Filter by chain
npx walletqa test --action sign # Filter by action
npx walletqa test --wallet okx --action connect # Combined filters
npx walletqa test --affected # Auto-select from git diff
npx walletqa test --list # Show available test matrix
npx walletqa validate-selectors # Validate engine selectors against live extensions
npx walletqa validate-selectors --wallet tonkeeper --json # Validate one wallet, JSON output
npx walletqa check-updates # Check all wallets for new extension versions
npx walletqa check-updates --wallet okx # Check a single wallet
npx walletqa check-updates --json # Machine-readable output
npx walletqa generate --wallet trust-wallet --actions connect,sign,reject # Scaffold test + setup files
npx walletqa generate --wallet okx --actions connect --output src/tests # Custom output directory
npx walletqa generate --wallet tonkeeper --actions send-tx --dapp-url http://localhost:5173 # Custom dApp URL
npx walletqa doctor --flakes # Show flaky test report with pass rates and trends
npx walletqa open --wallet metamask # Open a browser with MetaMask (latest version)
npx walletqa open --wallet phantom --ext-version 25.0.0 # Open with a specific version
npx walletqa open --wallet trust-wallet --url http://localhost:3000 # Open and navigate to a dApp
npx walletqa open --wallet metamask --list-versions # List downloadable versions
npx walletqa open --wallet trust-wallet --seed-phrase "your twelve word seed phrase here" # Open with wallet ready to use
npx walletqa open --wallet metamask --seed-phrase "..." --password "MyPass123!" # Custom password
npx walletqa open --wallet okx --seed-phrase "..." --url http://localhost:3000 # Configured + navigate to dAppSemantic Test Filtering
All example tests use Playwright tags for semantic filtering:
# "Test all TON wallet connections"
npx walletqa test --chain ton --action connect
# "Run sign tests for Trust Wallet"
npx walletqa test --wallet trust-wallet --action sign
# "Test what my PR changed"
npx walletqa test --affectedTags: @trust-wallet, @tonkeeper, @okx, @metamask, @phantom, @rabby, @rainbow | @evm, @ton, @solana | @connect, @sign, @send-tx, @reject, @onboard | @engine
The --affected flag reads git diff and maps changed wallet files to the relevant test tags automatically.
How Caching Works
defineWalletSetup()hashes the setup function body (via esbuild minification) to create a stable cache keywalletqa cachelaunches a real browser, runs the onboarding flow, waits for Chrome to flush writes, then snapshots the browser profile- At test time, the fixture restores the cached profile, patches Chrome's internal extension paths, and auto-unlocks the wallet
- The extension ID stays stable across machines via
keyinjection into the extension'smanifest.json
Cache handles: LevelDB LOCK files, Secure Preferences HMAC (via --use-mock-keychain), extension path rewriting, and Extension State cleanup.
Integration with Existing Projects
walletqa is a Playwright fixture — it works alongside your existing tests with zero migration.
Add to an existing Playwright project
npm install -D walletqaAdd a wallet-e2e project to your playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 60_000,
projects: [
// Your existing tests — unchanged
{
name: 'web',
use: { ...devices['Desktop Chrome'] },
testMatch: '**/*.test.ts',
testIgnore: '**/*.extension.test.ts', // Exclude wallet tests
},
// walletqa wallet tests
{
name: 'wallet-e2e',
use: {
...devices['Desktop Chrome'],
headless: false, // Required — Chrome extensions don't work headless
},
testMatch: '**/*.extension.test.ts',
workers: 1, // Extension tests run serially
},
],
});Your existing tests run unchanged. Wallet tests live in separate *.extension.test.ts files:
your-project/
├── playwright.config.ts # Two projects: "web" + "wallet-e2e"
├── tests/
│ ├── homepage.test.ts # Existing test (unchanged)
│ ├── wallet-setup/
│ │ └── trust-wallet.setup.ts # walletqa setup file
│ └── connect.extension.test.ts # walletqa E2E test
└── .walletqa-cache/ # Auto-created by walletqa cacheCopy-paste starter: See
examples/getting-started/for a minimal working example.
Using alongside Synpress
walletqa complements Synpress — keep Synpress for MetaMask, use walletqa for everything else. Both use Playwright test.extend() fixtures, so they coexist without conflicts.
| | Synpress | walletqa | |--|---------|---------| | MetaMask | Mature, production-ready | Basic (deprioritized) | | Trust Wallet | Not supported | Full dApp E2E | | OKX | Not supported | Full dApp E2E | | TONkeeper | Not supported | Full dApp E2E + TonConnect | | Phantom | Not supported | Full dApp E2E | | Rabby | Not supported | Full dApp E2E | | Rainbow | Not supported | Onboarding verified |
// playwright.config.ts — three projects coexisting
projects: [
{ name: 'web', testMatch: '**/*.test.ts',
testIgnore: ['**/*.extension.test.ts', '**/metamask/**'] },
{ name: 'metamask', testMatch: '**/metamask/**/*.test.ts', // Synpress
use: { headless: false } },
{ name: 'wallet-e2e', testMatch: '**/*.extension.test.ts', // walletqa
use: { headless: false }, workers: 1 },
]Full example: See
examples/synpress-coexistence/for a complete config with shared test helpers.
For detailed step-by-step instructions, see the Integration Guide.
CI Setup
Extension tests require a display server and a cache build step:
# GitHub Actions
- run: xvfb-run npx walletqa cache tests/wallet-setup # Build cache
- run: xvfb-run npx playwright test --project=wallet-e2e # Run testsSee docs/ci-setup.md for full CI configuration, cache artifacts, and secrets management.
Extending walletqa
Add a custom wallet (no fork required)
import { registerWallet, createWalletFixture, BaseWalletPageObject } from 'walletqa';
// 1. Create a page object
class MyWalletPageObject extends BaseWalletPageObject {
readonly brand = 'my-wallet';
async importWallet(mnemonic: string, password: string) {
const page = await this.getExtensionPage('popup.html');
// ... your onboarding logic
}
async approveConnection() {
const popup = await this.waitForPopup(); // 3-phase detection built-in
await popup.locator('button:has-text("Connect")').click();
await this.closePopup(popup);
}
async rejectConnection() { /* ... */ }
async approveTransaction() { /* ... */ }
async rejectTransaction() { /* ... */ }
async approveSignature() { /* ... */ }
async rejectSignature() { /* ... */ }
}
// 2. Register it
registerWallet('my-wallet', MyWalletPageObject, {
readySelector: '#root', // CSS selector for "extension is loaded"
entryPage: 'popup.html', // extension entry page path
});
// 3. Use it like any built-in wallet
const test = createWalletFixture({
wallet: 'my-wallet',
password: 'TestPassword123!',
});
test('connect', async ({ approveConnection }) => {
// works exactly like built-in wallets
});Add a custom engine definition
For declarative wallet configs (~50 lines instead of a full page object class):
import { registerWalletDefinition, ApprovalEngine } from 'walletqa';
registerWalletDefinition('my-wallet', {
brand: 'my-wallet',
extensionStoreId: 'abc123',
chains: ['evm'],
protocol: 'eip-6963',
pages: { home: 'popup.html', onboarding: 'popup.html', notification: 'popup.html' },
mnemonic: { wordCount: 12, type: 'bip39', inputStyle: 'textarea', inputSelector: 'textarea' },
onboarding: {
steps: [
{ label: 'Import', action: 'click', selector: 'button:has-text("Import")' },
{ label: 'Fill seed', action: 'fill-mnemonic', selector: '' },
{ label: 'Set password', action: 'fill', selector: '#password', value: '{{password}}' },
{ label: 'Submit', action: 'click', selector: 'button[type="submit"]' },
],
readySelector: '[data-testid="balance"]',
},
approval: {
connect: { confirm: 'button:has-text("Connect")', reject: 'button:has-text("Cancel")' },
transaction: { confirm: 'button:has-text("Confirm")', reject: 'button:has-text("Reject")' },
signature: { confirm: 'button:has-text("Sign")', reject: 'button:has-text("Cancel")' },
},
unlock: { passwordInput: 'input[type="password"]', submitButton: 'button[type="submit"]' },
quirks: { popupStyle: 'new-page', enableSmartFallbacks: true },
meta: { lastVerified: '2026-03-08' },
});Override selectors without forking
import { patchDefinition, getWalletDefinition } from 'walletqa/engine';
const base = getWalletDefinition('trust-wallet');
const patched = patchDefinition(base, {
approval: {
connect: { confirm: '.my-custom-connect-button' },
},
quirks: { approvalExtraTimeout: 20_000 },
});Version-specific selector overrides
When an extension updates and selectors change, add version overrides without breaking older versions:
const def = getWalletDefinition('tonkeeper', '5.1.0');
// Any versions[] entries matching '5.1.0' are automatically deep-mergedDocumentation
- Integration Guide — adding walletqa to existing projects and Synpress coexistence
- Architecture — module structure, data flow, popup detection, caching internals
- CI Setup — GitHub Actions, secrets, cache artifacts, xvfb, nightly selector validation
- Troubleshooting — common issues, error diagnostics, selector validation
- Engine Design — config-driven wallet definitions, smart fallbacks, version pinning
- Wallet References — UI selectors, architecture, and debugging for all 7 wallets
- Parallel Execution — per-wallet parallel safety,
workersconfig, CI guidance - Backlog — planned features and roadmap
Requirements
- Node.js >= 18
@playwright/test>= 1.39 (peer dependency)- Extension tests require headed mode —
headless: trueis not supported by Chrome extensions - For CI: use
xvfb-runon Linux or a hosted runner with a display
License
MIT
