So you've decided to dive into end-to-end testing for your Web3 app? Excellent choice! Getting started with e2e testing is straightforward with the right tools, and once you get the hang of it, you'll wonder how you ever lived without it.
Before we jump into code, let's talk about why this combo works well. Playwright gives you a solid foundation for browser automation with great debugging tools, while Synpress adds Web3-specific features – MetaMask integration, wallet caching, and transaction handling.
First things first, let's get everything installed. It's surprisingly straightforward:
npm install @synthetixio/synpressNow, here's where it gets interesting. Your basic test setup looks something like this:
import { testWithSynpress } from '@synthetixio/synpress';
import { MetaMask, metaMaskFixtures } from '@synthetixio/synpress/playwright';
import basicSetup from './wallet-setup/metamask.setup';
const test = testWithSynpress(metaMaskFixtures(basicSetup));
let metamask: MetaMask;
const { expect } = test;This setup tells Synpress to use MetaMask in your tests with your specified configuration.

Let's start with something every Web3 app needs – connecting a wallet. Here's what a basic test looks like:
test.describe('My Awesome DApp Tests', () => {
test('should connect wallet successfully', async ({ page }) => {
// Navigate to your app
await page.goto('http://localhost:3000');
// Click that connect button
await page.click('[data-testid="connect-wallet"]');
// Synpress handles the MetaMask popup automatically
// Check if we're connected
await expect(page.locator('[data-testid="wallet-address"]')).toBeVisible();
});
});What I love about this is how clean it is. No wrestling with popup windows or iframe nightmares – Synpress handles all that for you.
Once you've mastered wallet connection, it's time for the main event – testing transactions:
test('should send tokens successfully', async ({ page }) => {
// Navigate to coin toss game
await page.goto("/coinToss.html")
// Connect wallet
const connectButton = page.getByTestId("ockConnectButton")
await connectButton.click()
// Find and click play button
const playButton = page.locator('button:has-text("Place Bet")')
// Click play button
await playButton.click()
await metamask.confirmTransaction()
// Verify success
const resultModal = page.locator('[role="dialog"]').filter({ hasText: /You (won|lost)/i })
const hasResultModal = await resultModal.isVisible({ timeout: 10000 }).catch(() => false)
});
Let's be real – tests won't work perfectly the first time. Here are essential debugging techniques:
DEBUG=synpress:* npm testThis gives you detailed logs about what's happening under the hood.
When a test fails and you need to see what's happening:
SLOW_MO=1000 npm testThis adds a 1-second delay between actions, making it easier to follow the test execution.
Add this to your test for automatic failure documentation:
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== 'passed') {
await page.screenshot({ path: `screenshots/${testInfo.title}.png` });
}
});Here's a fun one that had me pulling my hair out. When running pnpm test:e2e-setup, the process would just... hang. Forever. No errors, no timeout, just an eternal loading state staring back at me.
The setup script looked innocent enough:
test("Setup wallet", async ({ context, metamask }) => {
await metamask.connectToDapp()
await metamask.switchNetwork('base')
// ... rest of setup
})After what felt like hours of staring at a frozen terminal, I finally ran it with debug flags:
DEBUG=synpress:* pnpm test:e2e-setupAnd there it was! The debug logs revealed MetaMask was showing a network switch confirmation dialog that the script couldn't handle:
synpress:metamask Switching to network: base
synpress:metamask Waiting for network switch confirmation...
[hanging here forever]The solution? Remove the network switch from the setup entirely. The cached wallet doesn't need to be on a specific network – let each test handle its own network requirements. Sometimes the simplest solutions are the best ones.


Here's what separates good e2e tests from great ones:
Test user journeys, not implementation details – Think "user connects wallet and sends tokens" not "button click triggers function X"
Keep tests independent – Each test should work on its own. No test should depend on another test running first.
Use data-testid attributes – They're more stable than CSS selectors:
<button data-testid="connect-wallet">Connect</button>Be generous with assertions – But not too generous. Check what matters, ignore what doesn't.
E2E testing with Playwright and Synpress might seem daunting at first, but it's genuinely worth the investment. Start small – maybe just test your wallet connection flow. Then gradually add more complex scenarios.
Remember: done is better than perfect. Tests don't need to cover every edge case right away. They just need to catch the obvious breaks while you focus on building features.
The best part? Once you have a solid test suite, you can refactor with confidence, ship features faster, and actually enjoy your weekends without worrying about production issues.
Happy testing!

Share Dialog
ChainHacker
4 comments
wrote about E2E testing Web3 apps with Playwright + Synpress spoiler: DEBUG=synpress:* will save your sanity when tests hang forever
Interesting post ! The only thing that tickles me a bit is using "data-testid". It means you have to change stuff in production to let tests working. But I guess that's what you have to pay for stable, non-css-based tests.
Yeah, ARIA roles feel better. Migrating to locators using those. Could not wait with the article till total cleanup. `const resultModal = page.locator('[role="dialog"]').filter({ hasText: /You (won|lost)/i })`
Oh yeah why not aria-role. I didn't think about it