Chainwright is an end-to-end testing toolkit for Web3 dapps built on Playwright. It helps you build and cache browser wallet extension state, then reuse it in your end-to-end tests through ready-made fixtures.
- Wallet setup CLI to build reusable extension cache
- Custom wallet extension source.
- Playwright fixtures for wallet + Dapp testing
- Support for multiple wallet profiles per wallet
- Wallet action APIs for onboarding, account switching, transaction confirmation, and more
- Keplr
- MetaMask
- Meteor
- Petra
- Phantom
- Solflare
- Node.js
>=22 @playwright/test@1.61.1(peer dependency)
Supports the following operating systems:
- MacOS
- Linux
- Windows
Before installing Chainwright, ensure to install Playwright's browser using the command below.
npx playwright install chromiumbunx playwright install chromiumAfter Installing Playwright's browser, install Chainwright and @playwright/test
pnpm add -D chainwright @playwright/testbun add -D chainwright @playwright/testnpm install --save-dev chainwright @playwright/testyarn add -D chainwright @playwright/testCreate a setup directory (default: tests/wallet-setup) and add *.setup.ts files with a wallet name in the filename, for example:
base.setup.tsbase-two.setup.tspetra.setup.tsphantom-team-a.setup.ts
Each file must export defineWalletSetup(...).
Wallet setup examples: Setup examples link
import { defineWalletSetup } from "chainwright/core";
import { Metamask } from "chainwright/metamask";
const PASSWORD = "test1234"; // For Petra wallet, you have to use a strong password. e.g. PlayerPetra45!!
const SEED_PHRASE = "test test test test test test test test test test test test test";
export default defineWalletSetup(
PASSWORD,
async ({ walletPage }) => {
const metamask = new Metamask(walletPage);
await metamask.onboard({
mode: "import",
secretRecoveryPhrase: SEED_PHRASE,
mainAccountName: "Main",
});
},
{
...//Optional prarmeters here
},
);For Wallets with additional accounts
import { defineWalletSetup } from "chainwright/core";
import { Petra } from "chainwright/petra";
const PASSWORD = "PlayerPetra45!!";
export default defineWalletSetup(
PASSWORD,
async ({ walletPage }) => {
const petra = new Petra(walletPage);
await petra.onboard({
mode: "importMnemonic",
accountName: "default",
network: "Testnet",
secretRecoveryPhrase: "test test test...", // Seed phrase for the main account
additionalAccounts: [
{
accountName: "nw-account",
mode: "mnemonic",
mnemonicPhrase: "test test test..." // Seed Phrase for this account
},
]
});
},
{
...//Optional prarmeters here
}
);To support multiple profiles in a single wallet (for example, MetaMask), only setup files from the second profile onward need an explicit, distinct profile name.
main.setup.ts can use the default profile, while main-two.setup.ts (and any additional setup files) should declare a unique profile name. Then, in any fixture that should use that profile, pass the exact profileName value.
Example:
main.setup.ts: uses the default profilemain-two.setup.ts: definesprofileName: "profile two"- Fixture usage:
metamaskFixture({ profileName: "profile two" })
import { defineWalletSetup } from "chainwright/core";
import { Metamask } from "chainwright/metamask";
const PASSWORD = "test1234"; // For Petra wallet, you have to use a strong password. e.g. PlayerPetra45!!
const SEED_PHRASE = "test test test test test test test test test test test test test";
export default defineWalletSetup(
PASSWORD,
async ({ walletPage }) => {
const metamask = new Metamask(walletPage);
await metamask.onboard({
mode: "import",
secretRecoveryPhrase: SEED_PHRASE,
mainAccountName: "Main",
});
},
{
profileName: "profile two"
},
);For custom wallet extension download URL and local path
For security reasons, you might choose not to use the same wallet extension source that Chainwright uses internally. To fix this, you can provide your own wallet extension source via a download URL or a locally stored wallet extension.
Note
You must ensure that the version of the extension you provide matches the one Chainwright uses internally.
These are Chainwright's wallet extensions and their versions:
- MetaMask: v13.33.0 Source
- Petra: v2.4.8 Source
- Phantom: v26.10.0 Source
- Solflare: v2.19.1 Source
- Meteor: v0.7.0 Source
- Keplr: v0.13.39 Source
Example:
import { defineWalletSetup } from "chainwright/core";
import { Metamask } from "chainwright/metamask";
const PASSWORD = "test1234"; // For Petra wallet, you have to use a strong password. e.g. PlayerPetra45!!
const SEED_PHRASE = "test test test test test test test test test test test test test";
export default defineWalletSetup(
PASSWORD,
async ({ walletPage }) => {
const metamask = new Metamask(walletPage);
await metamask.onboard({
mode: "import",
secretRecoveryPhrase: SEED_PHRASE,
mainAccountName: "Main",
});
},
{
extensionSource: {
localPath: "Your local path here",
// OR
downloadUrl: "Download URL here",
sha256: "Expectd SHA-256 hash here"
},
},
);Run setup with the CLI (Supports npx, bun, pnpm, and yarn):
Note
By default, Chainwright looks for tests/wallet-setup in your base directory. However, you can specify the directory you want Chainwright to get your setup files from.
bun chainwright --wallets <Wallets you want to support>
Examples:
# Multiple wallet setup command
bun chainwright --wallets metamask phantom solflare
# Single wallet setup command
bun chainwright --metamask
# Overriding existing wallet cache during setup
bun chainwright --metamask --force
# Overriding multiple existing wallet cache during setup
bun chainwright --wallets metamask phantom petra --force
# NOTE: These command will work with other package managers. npm, pnpm, and yarn.To specify a directory:
bun chainwright <directory path> <wallet>
Example:
bun chainwright ./src/e2e/your-tests --metamaskUseful flags:
-h, --helpshows you all the commands-f, --forceoverwrite existing cache--wallets <wallets...>select wallets (metamask,solflare,petra,phantom,meteor,keplr). Setup multiple wallets at the same time.-a, --allsetup all wallets--kp, --keplrsetup keplr wallet-m, --metamasksetup metamask wallet--mt, --meteorsetup the meteor wallet--pt, --petrasetup petra wallet--ph, --phantomsetup phantom wallet-s, --solflaresetup solflare wallet
Wallet profile cache is stored under:
.wallet-cache/<wallet>/wallet-data(default profile).wallet-cache/<wallet>/<profileName>(custom profile)
import { expect, type Page } from "@playwright/test";
import { testWithChainwright } from "chainwright/core";
import { metamaskFixture } from "chainwright/metamask";
// Chainwright's Fixture
export const testWithMetamask = testWithChainwright(metamaskFixture());
// Extend Chainwright's metamaskFixture to suit your need
export const testDappFixture = testWithMetamask.extend<{dAppPage: Page}>({
dappPage: async ({ page, baseURL }, use) => {
await page.goto(`https://your-dapp.example`);
await use(page);
},
});
// Then in your tests do:
const test = testDappFixture;
test.describe("Example tests", () => {
test("connect wallet to dapp", async ({ dappPage, metamask }) => {
const connectButton = dappPage.getByRole("button", { name: /Connect/i})
await connectButton.click();
await metamask.connectToApp("Account 1");
await expect(dappPage.getByText("Connected")).toBeVisible();
});
})Note
The wallet fixture will make use of the default wallet profile. If you specified a profile-name at the point of setting up, make sure to include it in the fixture.
// No profile name is specified at setup time
const testWithFixture = testWithChainwright(fixture())
// If a profile name is specified at setup time.
const testWithFixture = testWithChainwright(fixture({ profileName: "profile name" }))Wallet fixture parameters:
profileName?: string,slowMo?: number
Use worker-scoped fixtures when tests in a test.describe() block can safely share a wallet context. Setup and teardown run once per worker instead of per test, which speeds up CI runs and reduces flakiness caused by repeated wallet initialization.
import { type Page } from "@playwright/test";
import { testWithChainwright } from "chainwright/core";
import { metamaskWorkerScopeFixture } from "chainwright/metamask";
// Chainwright's worker scoped fixture
export const testWithFixture = testWithChainwright(metamaskWorkerScopeFixture());
// Your worker scoped fixture that extends Chainwright's worker scoped fixture
export const workerScopedFixture = testWithFixture.extend<{dAppPage: Page}>({
dappPage: [
async ({ workerScopeContents }, use) => {
const { context, wallet, walletPage } = workerScopeContents;
/** N.B:
* wallet represents -> metamask, phantom, keplr, etc...
* walletPage represents -> metamask wallet page, phantom wallet page, keplr wallet page, etc...
*/
const _dappPage = await context.newPage();
await _dappPage.goto(`http://example-site.com`);
await use(_dappPage);
},
{ scope: "worker" },
],
})
// Then in your tests do:
const test = workerScopedFixture;
test.describe("Example test", () => {
test("Should confirm transaction", ({ dappPage, workerScopeContents}) => {
const { wallet: metamask } = workerScopeContents
await dappPage.getByRole("button", { name: "Send Tx" }).click();
await metamask.confirmTransaction();
});
test("Should reject transaction", async ({ dappPage, workerScopeContents })=> {
const { wallet: metamask } = workerScopeContents
await dappPage.getByRole("button", { name: "Send Tx" }).click();
await metamask.rejectTransaction();
});
})Worker scoped fixture parameters:
profileName?: stringslowMo?: number
Running Chainwright in CI is very similar to running Playwright in CI. The only additional requirement is a cache-build step before executing tests, as shown below:
Why we make use of xvfb:
Important
Browser extensions don't load in headless Chromium, so the tests have to run in headed mode. CI machines have no display, so launching a headed browser fails. xvfb provides a fake virtual display, letting Chromium run headed in CI as if a screen were attached.
name: CI
on:
workflow_dispatch:
pull_request:
branches: ["main"]
jobs:
test:
runs-on: ubuntu-22.04
timeout-minutes: 60
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
submodules: "recursive"
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v5
with:
version: 10
- name: Use Node LTS
uses: actions/setup-node@v6
with:
node-version: 24.x
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Install XVFB
run: sudo apt-get install -y xvfb
- name: Install Playwright browsers
run: pnpx playwright install chromium
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Build cache
run: xvfb-run pnpm run setup-wallets
- name: Run end-to-end tests (Headful)
run: xvfb-run pnpm playwright test --config=tests/playwright.config.tsEach wallet module exports:
<wallet>Fixture(...)<wallet>WorkerScopeFixture(...)<WalletClass>
Examples:
metamaskFixture,metamaskWorkerScopeFixture,MetamaskphantomFixture,phantomWorkerScopeFixture,PhantompetraFixture,petraWorkerScopeFixture,PetrasolflareFixture,solflareWorkerScopeFixture,SolflaremeteorFixture,meteorWorkerScopeFixture,MeteorkeplrFixture,keplrWorkerScopeFixture,Keplr
Extra MetaMask fixtures:
createAnvilNode(options?)connectToAnvil()
Extra Phantom/Solflare fixtures:
autoCloseNotification(auto fixture)
defineWalletSetup(password, setupFn, config?)password: string- wallet unlock password saved in cache metadatasetupFn: ({ context, walletPage }) => Promise<void>- runs onboarding/import flowconfig?: { profileName?: string; slowMo?: number, extensionSource?: {...} }- useful for setting up multiple wallet profiles, running the setup in slow motionslowMoand using a custom extension source.
testWithChainwright(customFixtures)Merges Playwright test with your Chainwright fixture extension.
Depending on wallet module, wallet class methods include:
onboard(...)unlock()lock()switchAccount(...)renameAccount(...)getAccountAddress(...)addAccount(...)connectToApp(...)confirmTransaction()rejectTransaction()
Additional wallet-specific actions are available, for example:
- MetaMask:
switchNetwork,toggleShowTestnetNetwork,addCustomNetwork - Phantom:
switchNetwork,toggleOptionalChains - Petra/Solflare/Meteor:
switchNetwork - Meteor:
openSettings
MIT
Built by Tobechukwu. (github) Contributions are welcome: see CONTRIBUTING.md to get involved.