npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

walletqa

v1.0.0-beta.19

Published

Playwright-based multi-chain wallet E2E testing library

Downloads

1,325

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=extension

How 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 popup

Setup 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-setup

This 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=extension

Config-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 supportframe property on ApprovalAction targets buttons inside sandboxed iframes (OKX SES). The ses-pointer-dispatch click strategy dispatches the full pointer event sequence React 18+ needs inside iframes
  • Post-click verificationquirks.postClickVerification checks the button actually disappeared after clicking, and retries with escalating strategies (evaluate-clickses-pointer-dispatch) if it didn't
  • Custom click strategiesregisterClickStrategy(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 diagnosticsApprovalTimeoutError includes 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-version accepts exact versions (5.0.0) and semver ranges (^4.0.0, ~3.2.0, >=5.0.0, 4.x)
  • Post-action hooksregisterPostActionHook(name, fn) runs custom logic (logging, metrics, screenshots) after every approve/reject action
  • dApp iframe widget supportDappIframeContext helper for testing wallet interactions triggered from embedded iframes (payment widgets, bridges, DEX aggregators)
  • Nightly selector validation — CI workflow runs walletqa validate-selectors nightly and auto-opens GitHub issues when selectors break
  • Pre-flight health checkrunPreflightChecks() 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:flake in the HTML report, making it easy to identify unreliable tests
  • Video recording on failurevideo: '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 llmFallback config on ApprovalEngine. Logs a warning when used so you know to update your selectors
  • Failure root cause classificationclassifyFailure() auto-categorizes test errors into actionable classes: selector-miss, popup-timeout, cache-stale, network-error, extension-crash, or unknown. Used by FlakeReporter and walletqa doctor --flakes for automated triage
  • Flakiness trackingFlakeReporter (a Playwright Reporter) persists pass/fail/retry results to .walletqa/history.json and computes per-test flake scores with trend analysis. Tests below 70% pass rate are flagged for quarantine. See Flake Tracking below
  • Visual change detectioncompareScreenshot() diffs wallet popup screenshots against stored baselines using pixelmatch. Detects UI regressions from extension updates. Requires pixelmatch and pngjs as 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 suggestions

Each 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 dApp

Semantic 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 --affected

Tags: @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

  1. defineWalletSetup() hashes the setup function body (via esbuild minification) to create a stable cache key
  2. walletqa cache launches a real browser, runs the onboarding flow, waits for Chrome to flush writes, then snapshots the browser profile
  3. At test time, the fixture restores the cached profile, patches Chrome's internal extension paths, and auto-unlocks the wallet
  4. The extension ID stays stable across machines via key injection into the extension's manifest.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 walletqa

Add 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 cache

Copy-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 tests

See 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-merged

Documentation

  • 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, workers config, CI guidance
  • Backlog — planned features and roadmap

Requirements

  • Node.js >= 18
  • @playwright/test >= 1.39 (peer dependency)
  • Extension tests require headed modeheadless: true is not supported by Chrome extensions
  • For CI: use xvfb-run on Linux or a hosted runner with a display

License

MIT