Mock the wallet and you leave connection prompts, chain switching, and signing dialogs untested. Rely on manual clicks and tests become slow and brittle. Automated E2E tests that drive a real MetaMask extension (the same one your users have) give you connect, sign, and reject coverage before you ship.
Here’s how to get that with Chroma: Playwright tests that control MetaMask Flask, import an account, authorize connections, and confirm or reject transactions, all in code.
Why test with a real MetaMask?
Real-wallet tests catch what mocks miss:
- Integration bugs: Connection prompts, chain switching, and signing dialogs behave differently than mocks.
- Real user flows: Same extension and UI your users see.
- Regression safety: Wallet or dApp UI changes show up as failing tests.
Chroma uses MetaMask Flask (v13.17.0), a stable, test-friendly build. Download it once via the CLI; your tests drive it like a real user.
Setup
Before installing Chroma, initialize Playwright in your project:
npm init playwright@latestThen install Chroma and prepare the MetaMask extension:
npm add -D @avalix/chroma
npx chroma download-extensionsCreate a test that attaches the MetaMask wallet to the browser context:
import { createWalletTest } from '@avalix/chroma'
const test = createWalletTest({
wallets: [{ type: 'metamask' }],
})
test('my metamask test', async ({ page, wallets }) => {
const metamask = wallets.metamask
// ...
})You get a metamask handle that can import an account, authorize connection requests, and confirm or reject transactions.
Import an account (seed phrase)
Use a test-only Secret Recovery Phrase so MetaMask has an account before your dApp runs. Chroma runs MetaMask’s onboarding for you (import wallet, set password, complete setup):
await metamask.importSeedPhrase({
seedPhrase: 'test test test test test test test test test test test junk',
})Never use real seed phrases or mainnet accounts in tests. Use test accounts with no real funds.
Import the account before navigating to your dApp so the wallet is ready when the dApp asks to connect.
Authorize the connection
When the dApp triggers “Connect wallet” (e.g. “Connect Wallet” or “Continue with a wallet”), MetaMask shows a connection popup. In your test: click the button, then approve in MetaMask:
await page.goto('https://your-dapp.example.com')
await page.getByRole('button', { name: 'Connect Wallet' }).click()
// MetaMask popup appears; approve it
await metamask.authorize()After authorize(), the dApp should see the connected account and update the UI. You can then assert on the connected state (e.g. address or “Connected” label).
Confirm or reject transactions
When the dApp sends a transaction or signing request, MetaMask shows a confirmation popup. Approve or reject it from the test:
// User flow that triggers a tx (e.g. send, approve, swap)
await page.getByRole('button', { name: 'Submit' }).click()
await metamask.confirm()To test rejection:
await metamask.reject()For MetaMask, Chroma uses confirm() and reject() instead of approveTx() / rejectTx() used for other wallets.
Full example: connect and sign on a live dApp
The snippet below follows a real flow used in Chroma’s own tests: open a dApp (e.g. Privy demo), connect with MetaMask Flask, authorize, then confirm a signing step. Adjust selectors and URL for your app.
import { createWalletTest, expect } from '@avalix/chroma'
const SEED_PHRASE = 'test test test test test test test test test test test junk'
const test = createWalletTest({
wallets: [{ type: 'metamask' }],
})
test.setTimeout(60_000)
test('connect with MetaMask and complete signing', async ({ page, wallets }) => {
const metamask = wallets.metamask
await metamask.importSeedPhrase({ seedPhrase: SEED_PHRASE })
await page.goto('https://demo.privy.io')
await page.waitForLoadState('domcontentloaded')
await page.bringToFront()
await page.getByRole('button', { name: 'REJECT ALL' }).click()
await page.waitForTimeout(2000)
await page.getByRole('button', { name: 'Continue with a wallet' }).click()
await page.getByPlaceholder('Search wallets').fill('metamask flask')
await page.getByRole('button', { name: 'MetaMask Flask' }).first().click()
await metamask.authorize()
await metamask.confirm()
await expect(page.getByText(/0x[a-fA-F0-9]{4}\.\.\.[a-fA-F0-9]{4}/).first()).toBeVisible()
})Flow in short:
- Import the test account into MetaMask.
- Open the dApp and accept cookies/consent if needed.
- Start “Continue with a wallet” and choose MetaMask Flask.
authorize()to accept the connection;confirm()to accept the subsequent signing step.- Assert that the UI shows the connected address (or another success state).
Swap the URL and selectors for your dApp and run the same flow.
Troubleshooting
Extension not loading: Ensure extensions are installed: npx chroma download-extensions. Chroma installs MetaMask Flask under .chroma/metamask-extension-13.17.0.
Popup or step timing out: Increase the test timeout, e.g. test.setTimeout(60000).
Account not found / wrong account: Call importSeedPhrase before page.goto(...) so MetaMask is ready when the dApp requests connection.
Flaky connection or confirm: Add short waits after navigation or after opening modals so the extension and dApp are in sync; avoid hard-coded long sleeps where possible.
If you run into issues when using MetaMask with Chroma, please report them on the Chroma GitHub repository. Your feedback helps improve the integration for everyone.
What’s next
- MetaMask wallet guide: API reference and more examples.
- Testing dApps: Page Object Model, test data, and waiting for blockchain state.
- Multi-chain guide: Run MetaMask alongside other Chroma-supported wallets in the same project.
With Chroma, your E2E tests run against a real MetaMask in CI and locally. No mocks, just real clicks and coverage you can ship on.
This post was drafted with AI assistance and reviewed by the Chroma team.