chainwright
v0.9.12
Published
Playwright Web3 wallet testing framework for end-to-end dApp automation with MetaMask, Phantom, Solflare, Petra, Meteor, and Keplr
Downloads
2,907
Maintainers
Readme
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.
Features
- Wallet setup CLI to build reusable extension cache
- Playwright fixtures for wallet + Dapp testing
- Support for multiple wallet profiles per wallet
- Wallet action APIs for onboarding, account switching, transaction confirmation, and more
Supported Wallets
- Keplr
- MetaMask
- Meteor
- Petra
- Phantom
- Solflare
Requirements
- Node.js
>=22 @playwright/[email protected](peer dependency)
Operating Systems
Supports the following operating systems:
- MacOS
- Linux
- Windows
Installation
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/testQuick Start
1. Create wallet setup files
Create 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 default defineWalletSetup(...).
// tests/wallet-setup/metamask.setup.ts
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
// tests/wallet-setup/metamask.setup.ts
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 same profileName value.
Example:
main.setup.ts: uses the default profilemain-two.setup.ts: definesprofileName: "profile two"- Fixture usage:
metamaskFixture({ profileName: "profile two" })
// tests/wallet-setup/main-two.setup.ts
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"
},
);2. Build wallet cache
Run setup with the CLI (Supports npx, bun, pnpm, and yarn):
[!NOTE] By default, Chainwright looks for
tests/wallet-setupin 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>To specify a directory:
bun chainwright <directory path> <wallet> -f #Optional flagUseful 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)
3. Use wallet fixtures in Playwright tests
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
defaultwallet profile. If you specified aprofile-nameat 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
Worker-Scoped Fixture
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
4. Running in CI (GitHub Actions)
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.tsWallets By Module
Each 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)
Core APIs
defineWalletSetup
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 }- useful for setting up multiple wallet profiles and running the setup in slow motionslowMo.
testWithChainwright
testWithChainwright(customFixtures)Merges Playwright test with your Chainwright fixture extension.
Common Wallet Actions
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
License
MIT
Built by Tobechukwu. (github) Contributions are welcome: see CONTRIBUTING.md to get involved.
