Skip to content

Commit f9d3ea3

Browse files
authored
adding e2e tests for basenames registration flow (base#2489)
adding e2e tests for basenames registration flow using the new onchaintestkit npm package
1 parent 785810f commit f9d3ea3

19 files changed

Lines changed: 1486 additions & 9 deletions

.github/workflows/e2e-tests.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: E2E Tests
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
e2e:
14+
runs-on: ubuntu-latest
15+
strategy:
16+
matrix:
17+
node-version: [18.x]
18+
19+
steps:
20+
- uses: actions/checkout@v3
21+
22+
- name: Use Node.js ${{ matrix.node-version }}
23+
uses: actions/setup-node@v3
24+
with:
25+
node-version: ${{ matrix.node-version }}
26+
cache: 'yarn'
27+
28+
- name: Set E2E env variables
29+
working-directory: apps/web
30+
run: |
31+
echo "NODE_ENV=development" > .env
32+
echo "E2E_TEST_SEED_PHRASE=test test test test test test test test test test test junk" >> .env
33+
echo "E2E_TEST_FORK_URL=https://mainnet.base.org" >> .env
34+
echo "E2E_TEST_FORK_BLOCK_NUMBER=31397553" >> .env
35+
echo "E2E_TEST=true" >> .env
36+
echo "TEST_BASENAME=testbasename123" >> .env
37+
echo "NEXT_PUBLIC_CDP_BASE_RPC_ENDPOINT=http://localhost:8545/" >> .env
38+
39+
- name: Install dependencies
40+
run: yarn
41+
42+
- name: Install Foundry
43+
uses: foundry-rs/[email protected]
44+
45+
- name: Prepare MetaMask extension
46+
run: yarn e2e:prepare-metamask
47+
48+
- name: Install Playwright browsers
49+
run: yarn playwright install --with-deps
50+
51+
- name: Install xvfb
52+
run: sudo apt-get update && sudo apt-get install -y xvfb
53+
54+
- name: Build application
55+
run: yarn build
56+
57+
- name: Run E2E tests
58+
env:
59+
NODE_OPTIONS: '--dns-result-order=ipv4first'
60+
run: xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" yarn test:e2e

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ out/
6060
# prevent people from accidentally committing a package-lock
6161
package-lock.json
6262

63+
# E2E
64+
**/playwright-report
65+
**/test-results
66+
apps/web/e2e/.cache
67+
6368
# Env files
6469
.env.local
6570
.env.development.local

apps/web/e2e/E2Ereadme.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# End-to-End Tests for `apps/web`
2+
3+
This folder contains Playwright tests for the Base Web project against a local Next.js dev server...
4+
5+
## Contents
6+
7+
- [`testFixture.ts`](./testFixture.ts) – extends Playwright's fixture system with the On-chain Test Kit wallets.
8+
- [`appSession.ts`](./appSession.ts) – helper functions for the common onboarding & registration steps.
9+
- [`tests/*.spec.ts`](./tests) – individual test cases (successful registration, rejection flow, …).
10+
11+
---
12+
13+
## Prerequisites
14+
15+
run yarn add -D @playwright/test @coinbase/onchaintestkit
16+
17+
> The scripts have been tested on macOS and Linux. Windows users should run the commands inside WSL 2.
18+
19+
---
20+
21+
## Environment variables
22+
23+
Create a file called **`.env`** in `apps/web/` (it is listed in `.gitignore`).
24+
25+
```dotenv
26+
# E2E .env example ───────────────────────────────────────────
27+
28+
# the basename that the test will try to register
29+
TEST_BASENAME=mytestname123
30+
31+
# 12-word mnemonic that gets imported into MetaMask; **DO NOT USE A REAL WALLET**
32+
E2E_TEST_SEED_PHRASE="test test test test test test test test test test test junk"
33+
34+
# RPC endpoint that Anvil will fork from (Base mainnet in this example)
35+
E2E_TEST_FORK_URL=https://mainnet.base.org
36+
37+
# The block number to fork at. Omitting gives you the latest block.
38+
E2E_TEST_FORK_BLOCK_NUMBER=31397553
39+
40+
NEXT_PUBLIC_CDP_BASE_RPC_ENDPOINT="http://localhost:8545/"
41+
E2E_TEST="true"
42+
```
43+
44+
- **`TEST_BASENAME`** must be at least 3 alphanumeric lower-case characters.
45+
- Any ETH the flow spends comes from Anvil's default funded developer accounts, so there is **no cost**.
46+
47+
---
48+
49+
## Running the tests locally
50+
51+
1. **Install dependencies + build**
52+
53+
```bash
54+
# from repo root
55+
yarn install
56+
yarn prepare-metamask
57+
yarn playwright install --with-deps
58+
yarn build
59+
```
60+
61+
2. **Run the E2E tests**
62+
63+
```bash
64+
yarn test:e2e
65+
```
66+
67+
The first time this runs it will download a MetaMask release (~60 MB) and start Anvil; subsequent runs are faster.
68+
69+
### CI parity
70+
71+
The GitHub Actions workflow (`.github/workflows/e2e.yml`) follows exactly the same steps – if it passes locally it should pass in CI.
72+
73+
---
74+
75+
## Running tests in headed mode (see the browser)
76+
77+
By default, Playwright runs tests in headless mode. To watch the tests execute:
78+
79+
```bash
80+
# Run all tests with visible browser
81+
yarn test:e2e --headed
82+
83+
# Run a specific test file
84+
yarn test:e2e registration-success.spec.ts --headed
85+
86+
# Debug mode: opens Playwright Inspector, lets you step through each action
87+
PWDEBUG=1 yarn test:e2e registration-success.spec.ts
88+
```
89+
90+
**Note:** In headed mode, you'll see the MetaMask extension window pop up alongside your app. The test automation will click through it automatically—don't interfere or it may fail!
91+
92+
---
93+
94+
Feel free to extend this README if you run into anything else that others might find useful. Happy testing! 🎉

apps/web/e2e/appSession.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { expect, Page } from '@playwright/test';
2+
import {
3+
BaseActionType,
4+
ActionApprovalType,
5+
CoinbaseWallet,
6+
MetaMask,
7+
} from '@coinbase/onchaintestkit';
8+
import {
9+
validateBasename,
10+
navigateToBasenameRegistration,
11+
searchForBasename,
12+
selectBasenameFromResults,
13+
SELECTORS,
14+
} from './basenameHelpers';
15+
16+
/**
17+
* Connects MetaMask wallet to the app and accepts Terms of Service
18+
* This represents the standard onboarding flow for first-time users
19+
*
20+
* @param page - The Playwright page object
21+
* @param metamask - The MetaMask wallet instance
22+
*/
23+
export async function connectWallet(page: Page, metamask: MetaMask): Promise<void> {
24+
console.log('[connectWallet] Current URL before connect:', page.url());
25+
// Open wallet connect modal
26+
await page.getByTestId('ockConnectButton').first().click();
27+
console.log('[connectWallet] Wallet connect modal opened');
28+
29+
// Select MetaMask from wallet options
30+
await page
31+
.getByTestId('ockModalOverlay')
32+
.first()
33+
.getByRole('button', { name: 'MetaMask' })
34+
.click();
35+
console.log('[connectWallet] MetaMask option clicked');
36+
37+
// Handle MetaMask connection request
38+
await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP);
39+
console.log('[connectWallet] MetaMask handleAction finished, URL after connect:', page.url());
40+
}
41+
42+
/**
43+
* Connects Coinbase wallet to the app
44+
* This represents the standard onboarding flow for first-time users
45+
*
46+
* @param page - The Playwright page object
47+
* @param coinbase - The Coinbase wallet instance
48+
*/
49+
export async function connectCoinbaseWallet(page: Page, coinbase: CoinbaseWallet): Promise<void> {
50+
console.log('[connectCoinbaseWallet] Current URL before connect:', page.url());
51+
// Open wallet connect modal
52+
await page.getByTestId('ockConnectButton').first().click();
53+
console.log('[connectCoinbaseWallet] Wallet connect modal opened');
54+
55+
// Select Coinbase Wallet from wallet options
56+
await page
57+
.getByTestId('ockModalOverlay')
58+
.first()
59+
.getByRole('button', { name: 'Coinbase Wallet' })
60+
.click();
61+
console.log('[connectCoinbaseWallet] Coinbase Wallet option clicked');
62+
63+
// Handle Coinbase wallet connection request
64+
await coinbase.handleAction(BaseActionType.CONNECT_TO_DAPP);
65+
console.log(
66+
'[connectCoinbaseWallet] Coinbase handleAction finished, URL after connect:',
67+
page.url(),
68+
);
69+
}
70+
71+
/**
72+
* Handles a MetaMask transaction approval
73+
*
74+
* @param metamask - The MetaMask wallet instance
75+
* @param approvalType - The type of approval (default: APPROVE)
76+
*/
77+
export async function handleTransaction(
78+
metamask: MetaMask,
79+
approvalType: ActionApprovalType = ActionApprovalType.APPROVE,
80+
): Promise<void> {
81+
console.log('[handleTransaction] Handling transaction with approvalType:', approvalType);
82+
await metamask.handleAction(BaseActionType.HANDLE_TRANSACTION, { approvalType });
83+
console.log('[handleTransaction] Transaction handled');
84+
}
85+
86+
/**
87+
* Switches to Base network if not already connected
88+
*
89+
* @param page - The Playwright page object
90+
* @returns true if network switch was needed, false otherwise
91+
*/
92+
export async function switchToBaseNetworkIfNeeded(page: Page): Promise<boolean> {
93+
console.log(
94+
'[switchToBaseNetworkIfNeeded] Checking if network switch is needed. Current URL:',
95+
page.url(),
96+
);
97+
await page.waitForLoadState('networkidle');
98+
await page.waitForTimeout(5000);
99+
// Prefer the explicit "Connect to Base" button if present
100+
const explicitSelector = 'button:has-text("Connect to Base")';
101+
102+
const hasExplicit = await page
103+
.locator(explicitSelector)
104+
.isVisible()
105+
.catch(() => false);
106+
107+
if (hasExplicit) {
108+
await page.locator(explicitSelector).click();
109+
await page.waitForLoadState('networkidle');
110+
console.log('[switchToBaseNetworkIfNeeded] Clicked "Connect to Base" button');
111+
return true;
112+
}
113+
114+
console.log('[switchToBaseNetworkIfNeeded] Already on Base network');
115+
return false;
116+
}
117+
118+
/**
119+
* Gets the main page from a context, excluding extension pages
120+
*
121+
* @param page - The Playwright page object
122+
* @returns The main application page
123+
*/
124+
export async function getMainPage(page: Page): Promise<Page> {
125+
const pages = page.context().pages();
126+
console.log('[getMainPage] Context currently has', pages.length, 'pages');
127+
const mainPage = pages.find((p) => !p.url().includes('chrome-extension://')) || page;
128+
console.log('[getMainPage] Selected main page with URL:', mainPage.url());
129+
await mainPage.bringToFront();
130+
await mainPage.waitForLoadState('networkidle');
131+
return mainPage;
132+
}
133+
134+
/**
135+
* Performs the common steps for basename registration tests:
136+
* 1. Validates Metamask fixture
137+
* 2. Navigates to the app and waits for network idle
138+
* 3. Connects the wallet and switches to the Base network if needed
139+
* 4. Navigates to the basename registration flow and selects the desired basename
140+
*
141+
* @param page - Playwright page
142+
* @param metamask - MetaMask wallet fixture
143+
* @returns The mainPage after completing the initial flow
144+
*/
145+
export async function prepareBasenameFlow(
146+
page: Page,
147+
metamask: MetaMask,
148+
): Promise<{ mainPage: Page; basename: string }> {
149+
if (!metamask) {
150+
throw new Error('MetaMask is not defined');
151+
}
152+
153+
const basename = validateBasename(process.env.TEST_BASENAME);
154+
console.log('[prepareBasenameFlow] Starting flow for basename:', basename);
155+
156+
// Navigate to application root
157+
await page.goto('/');
158+
await page.waitForLoadState('networkidle');
159+
console.log('[prepareBasenameFlow] Navigated to root. Current URL:', page.url());
160+
161+
// Connect wallet and switch network
162+
await connectWallet(page, metamask);
163+
const mainPage = await getMainPage(page);
164+
await switchToBaseNetworkIfNeeded(mainPage);
165+
166+
// Wait until the app is fully hydrated
167+
await mainPage.waitForLoadState('networkidle');
168+
169+
// Ensure wallet address is visible (wallet connected)
170+
console.log('[prepareBasenameFlow] Checking wallet address visibility');
171+
await expect(mainPage.getByText(SELECTORS.WALLET_ADDRESS)).toBeVisible();
172+
console.log('[prepareBasenameFlow] Wallet address visible');
173+
174+
// Begin registration flow
175+
await navigateToBasenameRegistration(mainPage);
176+
console.log('[prepareBasenameFlow] Navigated to basename registration');
177+
await searchForBasename(mainPage, basename);
178+
console.log('[prepareBasenameFlow] Searched for basename');
179+
await selectBasenameFromResults(mainPage, basename);
180+
console.log('[prepareBasenameFlow] Selected basename from results');
181+
182+
return { mainPage, basename };
183+
}

0 commit comments

Comments
 (0)