Web3 Testing Guide

How to Test WalletConnect Flow with Playwright: Complete 2026 Guide

A scenario-by-scenario walkthrough for testing WalletConnect in Playwright. QR code bypass via pairing URI, session proposals, chain switching, transaction approval, error handling, and graceful disconnect, all without a physical phone.

40M+

WalletConnect has facilitated over 40 million pairings across 600+ dApps and 300+ wallets, making it the dominant protocol for connecting mobile wallets to desktop applications in the Web3 ecosystem.

WalletConnect 2024 Year in Review

0Protocol round trips per pairing
0Scenarios covered
0Chain types tested
0%Fewer lines with Assrt

WalletConnect v2 Pairing and Session Flow

dApp (Browser)WalletConnect RelayWallet (Signer)Generate pairing URIReturn wc: URI (QR content)Publish session proposalDeliver proposal to walletApprove session (chains, methods)Session establishedeth_sendTransaction requestForward signing requestReturn signed txTransaction hash

1. Why Testing WalletConnect Is Harder Than It Looks

WalletConnect v2 connects a browser-based dApp to a mobile wallet through a relay server. The standard user experience is: your dApp renders a QR code containing a pairing URI, the user scans it with their mobile wallet, the wallet and dApp negotiate a session over the relay, and then signing requests flow back and forth through that persistent session. The QR code handoff is the fundamental obstacle for automated testing. There is no phone in your CI environment, no camera to scan with, and no way to interact with a mobile wallet from a Playwright script running in a headless browser.

The complexity goes deeper than the QR code. WalletConnect v2 uses a topic-based publish/subscribe model over a WebSocket relay. The pairing URI contains a symmetric key that both sides use to encrypt messages. The session proposal specifies which chains, JSON-RPC methods, and events the dApp requires, and the wallet must approve each one explicitly. Chain switching sends a separate wallet_switchEthereumChain request that the wallet can reject. Disconnection can originate from either side, and both sides must handle the session_delete event gracefully.

There are five structural reasons this flow resists test automation. First, the QR code is a visual artifact that cannot be clicked or filled like a form field. Second, the relay is an external service that introduces network latency and occasional connectivity drops. Third, session proposals involve cryptographic key exchange that must happen in the correct order. Fourth, each signing request requires explicit wallet-side approval, which in production means tapping a button on a phone. Fifth, chain switching may trigger additional prompts on the wallet side that are invisible to the dApp.

WalletConnect v2 QR Pairing Flow

🌐

dApp

User clicks Connect Wallet

💳

QR Modal

Renders wc: pairing URI

⚙️

Relay Server

wss://relay.walletconnect.org

🔒

Wallet Scans

Parses URI, derives key

↪️

Session Negotiation

Chains, methods, events

Session Active

Bidirectional signing

Why the QR Handoff Breaks Automation

🌐

Headless Browser

No camera, no phone

QR Code Rendered

Visual-only artifact

No Wallet App

CI has no mobile device

Solution

Extract pairing URI directly

The solution is to bypass the QR code entirely. Instead of scanning, you extract the raw pairing URI from the dApp, feed it to a programmatic wallet (a Node.js script using the WalletConnect SDK), and simulate the wallet side of every interaction. The sections below show you exactly how.

2. Setting Up a Reliable Test Environment

Testing WalletConnect requires three components running in parallel: your dApp in a browser (driven by Playwright), a programmatic wallet that acts as the signer, and access to the WalletConnect relay (either the public relay or a local one). The programmatic wallet is a Node.js process that uses the @walletconnect/web3wallet SDK to pair, approve sessions, and sign transactions without any UI.

WalletConnect Test Environment Checklist

  • Install @walletconnect/web3wallet and @walletconnect/utils as dev dependencies
  • Obtain a free WalletConnect Project ID from cloud.walletconnect.com
  • Generate a test Ethereum private key (never use a key holding real funds)
  • Configure your dApp to use the public relay (relay.walletconnect.org)
  • Set up a Playwright globalSetup that spawns the programmatic wallet process
  • Create a shared communication channel (file, IPC, or in-memory) between Playwright and the wallet
  • Add ethers.js or viem for transaction signing in the wallet helper
  • Pin WalletConnect SDK versions to avoid breaking changes between test runs

Environment Variables

.env.test

Programmatic Wallet Helper

The core of your test infrastructure is a programmatic wallet that simulates what a real mobile wallet does. It connects to the WalletConnect relay, listens for session proposals, and responds to signing requests. This helper runs as a background process during your test suite.

test/helpers/walletconnect-wallet.ts

Playwright Configuration

playwright.config.ts
Install WalletConnect Test Dependencies

3. Scenario: Bypassing the QR Code via Pairing URI

The first and most critical scenario is establishing a WalletConnect session without scanning a QR code. Every dApp that implements WalletConnect generates a pairing URI (starting with wc:) and encodes it into a QR code for the user to scan. That same URI is available in the DOM or via JavaScript evaluation on the page. Your test extracts it, passes it to the programmatic wallet, and the pairing completes over the relay just as it would with a real phone.

Most dApps that use the WalletConnect Web3Modal also provide a “Copy to clipboard” button next to the QR code, or store the URI in a data attribute on the QR container element. Some dApps expose it through the window.walletconnect object or emit it as a custom DOM event. The extraction method depends on the dApp, but one of these approaches will work for virtually every implementation.

1

QR Code Bypass via Pairing URI Extraction

Complex

Goal

Click the “Connect Wallet” button in the dApp, extract the WalletConnect pairing URI from the QR modal without scanning, and establish a session programmatically.

Preconditions

  • dApp running at DAPP_URL with WalletConnect integration
  • Programmatic wallet helper initialized and connected to relay
  • Valid WalletConnect Project ID configured

Playwright Implementation

walletconnect-pairing.spec.ts

What to Assert Beyond the UI

  • The extracted URI starts with wc: and contains the @2 version marker
  • The session topic is a valid hex string returned by the wallet SDK
  • The dApp UI transitions from “Connect Wallet” to showing the connected address
  • No console errors related to WebSocket disconnection appear during pairing

QR Bypass: Playwright vs Assrt

import { test, expect } from '@playwright/test';
import { createTestWallet, pairAndApprove } from '../helpers/walletconnect-wallet';

test('bypass QR code and pair', async ({ page }) => {
  const wallet = await createTestWallet();
  await page.goto('/');
  await page.getByRole('button', { name: /connect wallet/i }).click();

  await expect(page.locator('[class*="walletconnect"]'))
    .toBeVisible({ timeout: 10_000 });

  const uri = await page.evaluate(() => {
    const qr = document.querySelector('[data-uri]');
    if (qr) return qr.getAttribute('data-uri');
    const input = document.querySelector('input[value^="wc:"]');
    return input?.value || null;
  });

  expect(uri).toMatch(/^wc:[a-f0-9]+@2/);
  const topic = await pairAndApprove(wallet, uri!, ['eip155:1']);
  expect(topic).toBeTruthy();

  await expect(page.getByText(/connected|0xf39F/i))
    .toBeVisible({ timeout: 15_000 });
});
57% fewer lines

4. Scenario: Session Proposal Approval

After pairing, the dApp sends a session proposal that specifies which blockchain namespaces, chains, JSON-RPC methods, and events it requires. The wallet must review this proposal and approve or reject it. In a real wallet app, this is a confirmation screen the user taps. In your test, the programmatic wallet evaluates the proposal and approves it with the correct accounts and chains.

Testing the session proposal is important because a mismatch between what the dApp requests and what the wallet approves will cause silent failures later. If the dApp requires eth_signTypedData_v4 but the wallet only approves personal_sign, the dApp will not discover the problem until it tries to request a typed signature and gets an error. Your test should verify that the proposal includes all required methods and that the approval response matches.

2

Session Proposal Validation and Approval

Moderate

Playwright Implementation

walletconnect-session.spec.ts

Assrt Equivalent

# scenarios/walletconnect-session-proposal.assrt
describe: Validate session proposal and approve with correct namespaces

given:
  - the dApp is running
  - a programmatic test wallet is ready

steps:
  - connect to the dApp via QR bypass
  - inspect the session proposal

expect:
  - the proposal requires the eip155 namespace
  - required chains include eip155:1
  - required methods include eth_sendTransaction and personal_sign
  - required events include chainChanged and accountsChanged
  - after approval the dApp shows connected state

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

5. Scenario: Chain Switching (Ethereum to Polygon)

Many dApps support multiple EVM chains and let users switch between them. When a user selects a different chain in the dApp UI, the dApp sends a wallet_switchEthereumChain request through the WalletConnect session. The wallet receives this request, switches its active chain, and emits a chainChanged event back to the dApp. Testing this flow verifies that chain switching propagates correctly through the relay and that both the dApp and wallet end up on the same chain.

If the wallet does not support the requested chain, it should return a 4902 error code (chain not added). Your dApp should then attempt to add the chain via wallet_addEthereumChain before retrying the switch. Both the success and failure paths need test coverage.

3

Chain Switching: Ethereum to Polygon

Moderate

Playwright Implementation

walletconnect-chain-switch.spec.ts

Chain Switching: Playwright vs Assrt

test('chain switch: Ethereum to Polygon', async ({ page }) => {
  const wallet = await createTestWallet();
  await page.goto('/');
  // ... connect and pair ...
  const sessionTopic = await pairAndApprove(wallet, uri!, ['eip155:1', 'eip155:137']);

  wallet.on('session_request', async (event) => {
    if (event.params.request.method === 'wallet_switchEthereumChain') {
      await wallet.respondSessionRequest({
        topic: event.topic,
        response: { id: event.id, jsonrpc: '2.0', result: null },
      });
      await wallet.emitSessionEvent({
        topic: event.topic,
        event: { name: 'chainChanged', data: 137 },
        chainId: 'eip155:137',
      });
    }
  });

  await page.getByRole('button', { name: /network/i }).click();
  await page.getByRole('option', { name: /polygon/i }).click();
  await expect(page.getByText(/polygon/i)).toBeVisible();
});
60% fewer lines

6. Scenario: Transaction Signing and Confirmation

The core value of WalletConnect is enabling a dApp to request transaction signatures from a remote wallet. When the user initiates an on-chain action (minting an NFT, swapping tokens, sending ETH), the dApp constructs a transaction object and sends an eth_sendTransaction request through the WalletConnect session. The wallet receives the request, shows the user a confirmation screen, signs the transaction with the private key, broadcasts it to the network, and returns the transaction hash.

In your test environment, the programmatic wallet skips the confirmation screen and signs immediately. For a complete test, you also want to verify that the dApp correctly handles the returned transaction hash: showing a pending state, polling for confirmation, and updating the UI once the transaction is mined. If you are using a local Hardhat or Anvil node, the transaction confirms instantly. On a testnet, you may need to wait.

4

Transaction Signing via WalletConnect

Complex

Playwright Implementation

walletconnect-tx.spec.ts

What to Assert Beyond the UI

  • The eth_sendTransaction request arrived at the wallet with correct params (to, value, data)
  • The wallet signed the transaction with the test private key
  • The dApp received a valid transaction hash in the response
  • If using a local node, the transaction is present in the block

7. Scenario: Wallet Rejection and Error Handling

Users reject transactions. They open their wallet, see the gas fee, and decide not to proceed. The wallet sends back a JSON-RPC error with code 4001(User Rejected Request). Your dApp must handle this gracefully: dismiss the pending state, show an appropriate error message, and allow the user to retry. Many dApps fail this scenario silently, leaving the user stuck in a “waiting for wallet” state forever.

Beyond user rejection, your test suite should cover other error scenarios: the relay connection dropping mid-request, the session expiring, and the wallet returning an insufficient funds error. Each of these produces a different error code and message that your dApp should handle distinctly.

5

Wallet Rejection: User Declines Transaction

Moderate

Playwright Implementation

walletconnect-rejection.spec.ts

Assrt Equivalent

# scenarios/walletconnect-rejection.assrt
describe: Wallet rejects a transaction and dApp handles it gracefully

given:
  - wallet is connected to the dApp
  - wallet is configured to reject all signing requests

steps:
  - initiate a send transaction in the dApp
  - fill in recipient and amount
  - click confirm

expect:
  - the dApp shows a rejection or error message
  - the dApp is not stuck in a pending state
  - the send button is enabled again for retry

8. Scenario: Graceful Disconnect Flow

WalletConnect sessions can be terminated by either the dApp or the wallet. When the user clicks “Disconnect” in the dApp, it sends a session_deleteevent through the relay, and both sides clean up their local session state. When the wallet disconnects (the user removes the dApp from their wallet's session list), the dApp receives the session_delete event and should revert to an unauthenticated state. Both directions need test coverage because the failure mode is different for each.

A common bug is the dApp failing to clean up its local state after a wallet-initiated disconnect. The user's wallet shows no active session, but the dApp still shows a connected address and attempts to send signing requests to a dead session. Testing both disconnect directions catches this class of bug.

6

Graceful Disconnect: dApp and Wallet Initiated

Straightforward

Playwright Implementation

walletconnect-disconnect.spec.ts

Disconnect Flow: Playwright vs Assrt

test('wallet-initiated disconnect', async ({ page }) => {
  const wallet = await createTestWallet();
  // ... connect and pair ...
  const sessionTopic = await pairAndApprove(wallet, uri!, ['eip155:1']);

  await expect(page.getByText(/connected|0xf39F/i))
    .toBeVisible({ timeout: 15_000 });

  // Wallet initiates disconnect
  await wallet.disconnectSession({
    topic: sessionTopic,
    reason: { code: 6000, message: 'User disconnected' },
  });

  // dApp should revert to disconnected state
  await expect(page.getByRole('button', { name: /connect wallet/i }))
    .toBeVisible({ timeout: 15_000 });
  await expect(page.getByText(/0xf39F/i))
    .not.toBeVisible({ timeout: 5_000 });
});
47% fewer lines

9. Common Pitfalls That Break WalletConnect Test Suites

Relay Connectivity Timeouts

The WalletConnect relay (relay.walletconnect.org) is an external service. In CI environments with restricted network egress or behind corporate firewalls, the WebSocket connection may fail silently or time out. The symptom is your test hanging indefinitely at the pairing step. Always set explicit timeouts on the pairing operation and add retry logic. Consider running a local relay (the WalletConnect team publishes a Docker image) for fully isolated CI pipelines.

Stale Session State Between Test Runs

WalletConnect SDKs persist session data in local storage (browser side) and a local database (wallet side). If your tests do not clean up sessions between runs, leftover sessions from previous tests can cause pairing failures or unexpected “already connected” states. Clear the browser's local storage in your test setup and call wallet.getActiveSessions() to disconnect any lingering sessions before each test.

Project ID Rate Limits

Every WalletConnect Project ID has rate limits on the relay. Free tier projects allow a limited number of pairings per day. If you run your test suite frequently or in parallel across multiple CI runners, you will exhaust this limit. The relay returns a 429 status on the WebSocket upgrade, and the SDK silently fails to connect. Use a dedicated Project ID for CI with a higher tier, or batch your tests to minimize pairing operations by reusing sessions across related test cases.

Namespace Mismatch Between dApp and Wallet

The session proposal specifies required namespaces (chains, methods, events), and the wallet must approve namespaces that satisfy all requirements. If your programmatic wallet approves eip155:1 but the dApp also requires eip155:137, the session will be established but chain switching to Polygon will fail. Always inspect the proposal's requiredNamespaces and build your approval response dynamically from it, rather than hardcoding chains.

WebSocket Connection Racing

The WalletConnect relay uses WebSocket connections, and both the dApp and wallet need active connections before messages can flow. If your test extracts the pairing URI and immediately feeds it to the programmatic wallet, the wallet's WebSocket connection may not be established yet. The pairing message gets lost, and the test times out. Add an explicit wait for the wallet's relay connection to be ready before attempting to pair. The core.relayer.subscriber.on('created') event signals that the relay connection is active.

WalletConnect Test Anti-Patterns

  • Hardcoding chain IDs instead of reading from the session proposal
  • Not cleaning up sessions between test runs
  • Missing timeout on the pairing step (hangs forever in CI)
  • Using a production Project ID for CI tests
  • Assuming the relay WebSocket is connected before it actually is
  • Forgetting to handle the 4001 user rejection error code
  • Not testing wallet-initiated disconnect (only testing dApp disconnect)
  • Ignoring chainChanged events after wallet_switchEthereumChain
WalletConnect Test Suite Run

10. Writing These Scenarios in Plain English with Assrt

The Playwright code in this guide is substantial. The QR bypass scenario alone is over 40 lines of TypeScript that deeply couples your test to DOM selectors, the WalletConnect SDK API, and the specific element structure of your dApp's connect modal. When your dApp upgrades from Web3Modal v3 to v4, or switches from WalletConnect to a different wallet connection library, every selector breaks. Assrt lets you describe the intent of each scenario in plain English, generates the Playwright implementation, and regenerates it when the underlying page structure changes.

The QR bypass scenario from Section 3 illustrates this well. In Playwright, you need to know the exact CSS selector for the QR container, the data attribute name, and the fallback extraction strategies. In Assrt, you describe what you want: “extract the pairing URI from the QR modal.” Assrt resolves the correct extraction method at runtime by analyzing the live DOM.

scenarios/walletconnect-full-suite.assrt

Assrt compiles each scenario into the same Playwright TypeScript you saw in the preceding sections, committed to your repo as real tests. When your dApp switches from @web3modal/react to @reown/appkit, Assrt detects the selector failures, analyzes the new DOM structure, and opens a pull request with updated locators. Your scenario files remain untouched.

Start with the QR bypass scenario. Once it passes in CI, add session proposal validation, then chain switching, then transaction signing, then rejection handling, then disconnect. Within an afternoon you will have complete WalletConnect coverage that most Web3 dApps never achieve manually.

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk