MM

Mocking window.ethereum in Playwright for end-to-end dApp testing

web3-mock supports Cypress and JSDOM test environments out of the box. However, I prefer Playwright to those as it provides a compelling set of benefits: familiar Jest matchers, a fixture-based runner, and execution in multiple real browser environments. Getting web3-mock to work with Playwright requires setup that is minimal but not obvious, hence this quick note.

The test definitions configures the mock and then interact with the page:

import { test } from '@playwright/test'

test('test wallet connection', async ({ page }) => {
await mockWeb3(page, () => {
// @ts-ignore
Web3Mock.mock({
blockchain: 'ethereum',
accounts: { return: ['0xd73b04b0e696b0945283defa3eee453814758f1a'] },
})
})

await page.goto('http://localhost:3000/')
await page.locator('text=Sign in').click()
await page.locator('text=Connected').waitFor()
})

The mockWeb3() call causes the web3-mock library plus the mock configuration to be loaded whenever a page is navigated to, but before the page’s own scripts are run:

import { Page } from '@playwright/test'
import { readFileSync } from 'fs'

const mockWeb3 = async (page: Page, fn: Function) => {
await page.addInitScript({
content:
readFileSync(
require.resolve('@depay/web3-mock/dist/umd/index.bundle.js'),
'utf-8',
) +
'\n' +
`(${fn.toString()})();`,
})
}

Why the ugly string concatenation, when we could call addInitScript twice, once for injecting the library, the next for injecting the mock configuration? Because the order of execution of scripts added with addInitScript is not defined, so the library might get loaded after (the attempt at) using it.