diff --git a/.gitignore b/.gitignore index 431d416..c9b4d80 100644 --- a/.gitignore +++ b/.gitignore @@ -32,5 +32,10 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts +# playwright + e2e test reports +e2e/playwright-report +test-results +e2e/.cache + #env .env diff --git a/bun.lockb b/bun.lockb index ec99129..0d3fd62 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/e2e/E2E.md b/e2e/E2E.md new file mode 100644 index 0000000..ff3996d --- /dev/null +++ b/e2e/E2E.md @@ -0,0 +1,94 @@ +# End-to-End Test Guide + +This project includes a full Playwright / OnchainTestKit setup that automatically: + +1. Spins up an **Anvil** fork of Base Mainnet (via OnchainTestKit) +2. Downloads & loads the **MetaMask** browser extension +3. Starts the local **Next.js** app (`bun run dev`) +4. Connects MetaMask to the app and clicks the **Fund** button + +Follow the steps below to run the tests on your machine or in CI. + +--- + +## 1 Prerequisites + +| Tool | Version | Notes | +|------|---------|-------| +| **Bun** | ≥ 1.2 | | +| **Node ≥ 18** | for Next.js/Playwright | +| **Git** | clone the repo | +| **Mac/Windows/Linux** | Chrome is bundled by Playwright | + +Install project deps: + +```bash +bun i +``` + +--- + +## 2 One-time MetaMask download (local & CI) + +```bash +bunx prepare-metamask # provided by @coinbase/onchaintestkit +``` + +This downloads `metamask-12.8.1` into `e2e/.cache/` so every future test run re-uses the same extension. + +--- + +## 3 Environment variables + +Create **`.env.local`** in the project root and add: + +```dotenv +# OnchainKit (public) – get these from the CDP dashboard +NEXT_PUBLIC_CDP_API_KEY=your_client_api_key +NEXT_PUBLIC_CDP_PROJECT_ID=your_project_id +NEXT_PUBLIC_WC_PROJECT_ID=your_walletconnect_id + +# Only needed if you enable Secure-Init (session-token flow) +# CDP_SECRET_KEY="-----BEGIN PRIVATE KEY-----…" +# CDP_SECRET_KEY_ID=key_id +# CDP_ORG_ID=org_id +``` + +Feel free to add other env variables here related to E2E_TEST_FORK_URL, or E2E_TEST_FORK_BLOCK_NUMBER, or anything else otherwise they will use the default values. + +--- + +## 4 Running the tests + +Headless (CI-style) +```bash +bun run e2e # alias for: bunx playwright test +``` + +Interactive debugging +```bash +bunx playwright test --headed --debug +``` + +Playwright will: +* start `next dev` on port 3000 +* launch Chromium with the MetaMask extension +* execute the spec at `e2e/fund-flow.spec.ts` + +--- + +## 5 What the template currently does + +Currently, it sets up a metamask wallet, uses that as the wallet to login to, and then the fund button becomes available. + +Feel free to extend the spec file or duplicate it for additional user journeys. + +--- + +## 6 Handy scripts + +| Script | Description | +|--------|-------------| +| `bun run e2e` | Run Playwright tests (headless) | +| `bun run e2e:headed` | Run Playwright tests in headed mode | +| `bunx prepare-metamask` | Download MetaMask extension | diff --git a/e2e/appSession.ts b/e2e/appSession.ts new file mode 100644 index 0000000..a4b6a32 --- /dev/null +++ b/e2e/appSession.ts @@ -0,0 +1,15 @@ +import { BaseActionType, MetaMask } from "@coinbase/onchaintestkit"; +import { Page } from "@playwright/test"; + +/** + * Connects MetaMask wallet to the app and accepts Terms of Service + * This represents the standard onboarding flow for first-time users + * + * @param page - The Playwright page object + * @param metamask - The MetaMask wallet instance + */ +export async function connectWallet(page: Page, metamask: MetaMask): Promise { + await page.getByTestId('ockConnectButton').nth(1).click(); + await page.getByText(/MetaMask/i).first().click(); + await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP); + } \ No newline at end of file diff --git a/e2e/fund-flow.spec.ts b/e2e/fund-flow.spec.ts new file mode 100644 index 0000000..18f1687 --- /dev/null +++ b/e2e/fund-flow.spec.ts @@ -0,0 +1,27 @@ +import { connectWallet } from './appSession'; +import { test, expect } from './onchainTest'; + +// Adjust selectors to match your UI; this is a starter example. +test('user funds wallet via FundButton', async ({ page, metamask }) => { + // ensure wallet automation fixture is present + if (!metamask) throw new Error('MetaMask fixture is required'); + + await page.goto('/'); + + await connectWallet(page, metamask); + + // Now click the Fund button - looking for button containing "Fund" text + const fundButton = page.getByRole('button', { name: /Fund/i }); + await expect(fundButton).toBeEnabled(); + + // Handle fund flow trigger – wait for popup/tab after clicking fund + const [popup] = await Promise.all([ + page.waitForEvent('popup'), + fundButton.click(), + ]); + + await expect(popup).not.toBeNull(); + + // Depending on your success criteria, expand on this test case + // You will need to get things like session tokens, etc.... +}); \ No newline at end of file diff --git a/e2e/onchainTest.ts b/e2e/onchainTest.ts new file mode 100644 index 0000000..1d9bb77 --- /dev/null +++ b/e2e/onchainTest.ts @@ -0,0 +1,5 @@ +import { createOnchainTest } from '@coinbase/onchaintestkit'; +import { metamaskWalletConfig } from './walletConfig/metamaskWalletConfig'; + +export const test = createOnchainTest(metamaskWalletConfig); +export const { expect } = test; \ No newline at end of file diff --git a/e2e/walletConfig/metamaskWalletConfig.ts b/e2e/walletConfig/metamaskWalletConfig.ts new file mode 100644 index 0000000..0908c38 --- /dev/null +++ b/e2e/walletConfig/metamaskWalletConfig.ts @@ -0,0 +1,32 @@ +import { base } from 'viem/chains'; +import { configure } from '@coinbase/onchaintestkit'; + +export const DEFAULT_PASSWORD = 'PASSWORD'; +export const DEFAULT_SEED_PHRASE = process.env.E2E_TEST_SEED_PHRASE; + +// Configure the test with MetaMask setup without adding network yet +const baseConfig = configure() + .withLocalNode({ + chainId: base.id, + forkUrl: process.env.E2E_TEST_FORK_URL ?? 'https://mainnet.base.org', + forkBlockNumber: BigInt(process.env.E2E_TEST_FORK_BLOCK_NUMBER ?? '31397553'), + hardfork: 'cancun', + }) + .withMetaMask() + .withSeedPhrase({ + seedPhrase: DEFAULT_SEED_PHRASE ?? 'test test test test test test test test test test test junk', + password: DEFAULT_PASSWORD, + }) + // Add the network with the actual port in a custom setup + .withNetwork({ + name: 'Base Mainnet', + chainId: base.id, + symbol: 'ETH', + // placeholder for the actual rpcUrl, which is auto injected by the node fixture + rpcUrl: 'http://localhost:8545', + }); + +// Build the config +const config = baseConfig.build(); + +export const metamaskWalletConfig = config; \ No newline at end of file diff --git a/package.json b/package.json index 92a0180..0da3703 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,10 @@ "lint:unsafe": "biome lint --write --unsafe .", "start": "next start", "test": "vitest run", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "e2e": "bunx playwright test", + "e2e:headed": "bunx playwright test --headed --debug", + "e2e:ci": "bunx playwright install --with-deps && bunx playwright test --reporter dot" }, "dependencies": { "@coinbase/onchainkit": "^0.33.4", @@ -30,6 +33,8 @@ }, "devDependencies": { "@biomejs/biome": "^1.8.0", + "@coinbase/onchaintestkit": "^1.1.0", + "@playwright/test": "^1.53.1", "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^14.2.0", "@types/node": "^20.11.8", @@ -49,6 +54,7 @@ "tailwindcss": "^3.4.0", "typescript": "5.6.2", "utf-8-validate": "^6.0.3", + "viem": "^2.31.4", "vitest": "^2.0.1" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..9bfcbcc --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,51 @@ +import { defineConfig, devices } from '@playwright/test'; +import * as dotenv from 'dotenv'; + +// Load environment variables from .env (if present) +// Load multiple environment files in order of precedence +dotenv.config({ path: './.env.local' }); +dotenv.config({ path: './.env' }); + +// Default port for the Next.js dev server +const PORT = Number(process.env.PORT ?? 3000); +const baseURL = `http://localhost:${PORT}`; + +/** + * Playwright configuration + * See https://playwright.dev/docs/test-configuration for all available options. + */ +export default defineConfig({ + testDir: './e2e', + /* Global timeouts */ + timeout: 5 * 60 * 1000, // 5 minutes per test + expect: { + timeout: 120_000, // 2 minutes for expect assertions + }, + /* CI behaviour */ + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 2 : undefined, + + /* Shared settings for all the projects below */ + use: { + baseURL, + trace: 'on-first-retry', + video: 'retain-on-failure', + }, + + /* Projects */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + /* Automatically start the Next.js server before running the tests */ + webServer: { + command: process.env.CI ? 'bun run start' : 'bun run dev', + url: baseURL, + timeout: 120 * 1000, // wait up to 2 minutes for the server + reuseExistingServer: !process.env.CI, // local dev: don\'t restart if already running + }, +}); diff --git a/src/components/OnchainProviders.tsx b/src/components/OnchainProviders.tsx index 4401885..14fc671 100644 --- a/src/components/OnchainProviders.tsx +++ b/src/components/OnchainProviders.tsx @@ -18,7 +18,7 @@ function OnchainProviders({ children }: Props) { return ( - + {children}