|
| 1 | +--- |
| 2 | +title: "Writing Tests" |
| 3 | +description: "Learn how to write comprehensive blockchain tests with OnchainTestKit" |
| 4 | +--- |
| 5 | + |
| 6 | +This guide covers everything you need to know about writing tests with OnchainTestKit, from basic wallet connections to complex transaction scenarios. NOTE that some of these examples may be different from what you implement depending on your frontend code. |
| 7 | + |
| 8 | +## Available Fixtures |
| 9 | + |
| 10 | +OnchainTestKit provides several fixtures for your tests: |
| 11 | + |
| 12 | +<Info> |
| 13 | +Fixtures are automatically injected into your test functions and handle setup/teardown. |
| 14 | +</Info> |
| 15 | + |
| 16 | +| Fixture | Type | Description | |
| 17 | +|---------|------|-------------| |
| 18 | +| `page` | `Page` | Playwright page object for browser automation | |
| 19 | +| `metamask` | `MetaMask` | MetaMask wallet automation interface | |
| 20 | +| `coinbase` | `CoinbaseWallet` | Coinbase wallet automation interface | |
| 21 | +| `node` | `LocalNodeManager` | Local blockchain node manager | |
| 22 | +| `smartContractManager` | `SmartContractManager` | Smart contract deployment and interaction | |
| 23 | + |
| 24 | +## Basic Wallet Operations |
| 25 | + |
| 26 | +### Connecting a Wallet |
| 27 | + |
| 28 | +<Tabs> |
| 29 | +<Tab title="MetaMask"> |
| 30 | +```typescript |
| 31 | +test("connect MetaMask", async ({ page, metamask }) => { |
| 32 | + if (!metamask) throw new Error("MetaMask not initialized") |
| 33 | + |
| 34 | + // Open wallet connect modal |
| 35 | + await page.getByTestId("ockConnectButton").first().click() |
| 36 | + |
| 37 | + // Select MetaMask from wallet options |
| 38 | + await page |
| 39 | + .getByTestId("ockModalOverlay") |
| 40 | + .first() |
| 41 | + .getByRole("button", { name: "MetaMask" }) |
| 42 | + .click() |
| 43 | + |
| 44 | + // Handle MetaMask connection request |
| 45 | + await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP) |
| 46 | +}) |
| 47 | +``` |
| 48 | +</Tab> |
| 49 | + |
| 50 | +<Tab title="Coinbase Wallet"> |
| 51 | +```typescript |
| 52 | +test("connect Coinbase Wallet", async ({ page, coinbase }) => { |
| 53 | + if (!coinbase) throw new Error("Coinbase not initialized") |
| 54 | + |
| 55 | + // Open wallet connect modal |
| 56 | + await page.getByTestId("ockConnectButton").first().click() |
| 57 | + |
| 58 | + // Select Coinbase from wallet options |
| 59 | + await page |
| 60 | + .getByTestId("ockModalOverlay") |
| 61 | + .first() |
| 62 | + .getByRole("button", { name: "Coinbase" }) |
| 63 | + .click() |
| 64 | + |
| 65 | + // Handle Coinbase connection request |
| 66 | + await coinbase.handleAction(BaseActionType.CONNECT_TO_DAPP) |
| 67 | +}) |
| 68 | +``` |
| 69 | +</Tab> |
| 70 | +</Tabs> |
| 71 | + |
| 72 | +### Network Switching |
| 73 | + |
| 74 | +```typescript |
| 75 | +test("switch networks", async ({ page, metamask }) => { |
| 76 | + // Connect wallet first |
| 77 | + await connectWallet(page, metamask) |
| 78 | + |
| 79 | + // Switch to Base Sepolia |
| 80 | + await page.getByTestId("switch-to-base-sepolia").click() |
| 81 | + |
| 82 | + // Handle network switch in wallet |
| 83 | + await metamask.handleAction(BaseActionType.SWITCH_NETWORK) |
| 84 | +}) |
| 85 | +``` |
| 86 | + |
| 87 | +## Transaction Testing |
| 88 | + |
| 89 | +### Basic Transaction |
| 90 | + |
| 91 | +```typescript |
| 92 | +test("send transaction", async ({ page, metamask }) => { |
| 93 | + // Connect wallet |
| 94 | + await connectWallet(page, metamask) |
| 95 | + |
| 96 | + // Ideally, you have some purchase button |
| 97 | + |
| 98 | + // Submit transaction |
| 99 | + await page.getByTestId("purchase-button").click() |
| 100 | + |
| 101 | + // Approve transaction in wallet |
| 102 | + await metamask.handleAction(BaseActionType.HANDLE_TRANSACTION, { |
| 103 | + approvalType: ActionApprovalType.APPROVE, |
| 104 | + }) |
| 105 | + |
| 106 | + // Wait for confirmation |
| 107 | + await expect(page.getByText("Transaction confirmed!")).toBeVisible() |
| 108 | +}) |
| 109 | +``` |
| 110 | + |
| 111 | +### Rejecting Transactions |
| 112 | + |
| 113 | +```typescript |
| 114 | +test("reject transaction", async ({ page, metamask }) => { |
| 115 | + await connectWallet(page, metamask) |
| 116 | + |
| 117 | + // Trigger transaction |
| 118 | + await page.getByTestId("purchase-button").click() |
| 119 | + |
| 120 | + // Reject in wallet |
| 121 | + await metamask.handleAction(BaseActionType.HANDLE_TRANSACTION, { |
| 122 | + approvalType: ActionApprovalType.REJECT, |
| 123 | + }) |
| 124 | + |
| 125 | + // Verify rejection handled |
| 126 | + await expect(page.getByText("Transaction rejected")).toBeVisible() |
| 127 | +}) |
| 128 | +``` |
| 129 | + |
| 130 | +## Advanced Testing Patterns |
| 131 | + |
| 132 | +### Parallel Test Execution |
| 133 | + |
| 134 | +```typescript |
| 135 | +test.describe.parallel("Parallel tests", () => { |
| 136 | + test("test 1", async ({ page, metamask, node }) => { |
| 137 | + console.log(`Test 1 using port: ${node?.port}`) |
| 138 | + // Each test gets its own isolated node |
| 139 | + }) |
| 140 | + |
| 141 | + test("test 2", async ({ page, metamask, node }) => { |
| 142 | + console.log(`Test 2 using port: ${node?.port}`) |
| 143 | + // Different port, isolated environment |
| 144 | + }) |
| 145 | +}) |
| 146 | +``` |
| 147 | + |
| 148 | +## Best Practices |
| 149 | + |
| 150 | +<Steps> |
| 151 | + |
| 152 | +<Step title="Wait for state changes"> |
| 153 | +Always wait for UI updates after wallet actions: |
| 154 | + |
| 155 | +```typescript |
| 156 | +// Good |
| 157 | +await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP) |
| 158 | +await page.waitForSelector('[data-testid="wallet-connected"]') |
| 159 | + |
| 160 | +// Bad - might be flaky |
| 161 | +await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP) |
| 162 | +expect(page.getByText("Connected")).toBeVisible() // Might fail |
| 163 | +``` |
| 164 | +</Step> |
| 165 | + |
| 166 | +<Step title="Handle errors gracefully"> |
| 167 | +Always include error scenarios in your tests: |
| 168 | + |
| 169 | +```typescript |
| 170 | +test("handle wallet rejection", async ({ page, metamask }) => { |
| 171 | + try { |
| 172 | + await metamask.handleAction(BaseActionType.CONNECT_TO_DAPP, { |
| 173 | + approvalType: ActionApprovalType.REJECT, |
| 174 | + }) |
| 175 | + } catch (error) { |
| 176 | + // Verify error is handled in UI |
| 177 | + await expect(page.getByText("Connection rejected")).toBeVisible() |
| 178 | + } |
| 179 | +}) |
| 180 | +``` |
| 181 | +</Step> |
| 182 | +</Steps> |
| 183 | + |
| 184 | +## Debugging Tests |
| 185 | + |
| 186 | +### Visual Debugging |
| 187 | + |
| 188 | +```bash |
| 189 | +# Run tests in headed mode |
| 190 | +yarn playwright test --headed |
| 191 | + |
| 192 | +# Use Playwright Inspector |
| 193 | +yarn playwright test --debug |
| 194 | + |
| 195 | +# Slow down execution |
| 196 | +yarn playwright test --slow-mo=1000 |
| 197 | +``` |
| 198 | + |
| 199 | +### Console Logs |
| 200 | + |
| 201 | +```typescript |
| 202 | +test("debug test", async ({ page, metamask }) => { |
| 203 | + // Log page errors |
| 204 | + page.on('pageerror', error => { |
| 205 | + console.error('Page error:', error) |
| 206 | + }) |
| 207 | + |
| 208 | + // Log console messages |
| 209 | + page.on('console', msg => { |
| 210 | + console.log('Console:', msg.text()) |
| 211 | + }) |
| 212 | + |
| 213 | + // Your test code |
| 214 | +}) |
| 215 | +``` |
| 216 | + |
| 217 | +## CI/CD Integration |
| 218 | + |
| 219 | +### GitHub Actions Example |
| 220 | + |
| 221 | +```yaml |
| 222 | +name: E2E Tests |
| 223 | + |
| 224 | +on: |
| 225 | + push: |
| 226 | + branches: [main, develop] |
| 227 | + pull_request: |
| 228 | + branches: [main] |
| 229 | + |
| 230 | +jobs: |
| 231 | + e2e: |
| 232 | + runs-on: ubuntu-latest |
| 233 | + timeout-minutes: 30 |
| 234 | + |
| 235 | + steps: |
| 236 | + - name: Checkout code |
| 237 | + uses: actions/checkout@v4 |
| 238 | + |
| 239 | + - name: Setup Node.js |
| 240 | + uses: actions/setup-node@v4 |
| 241 | + with: |
| 242 | + node-version: '18' |
| 243 | + cache: 'npm' |
| 244 | + |
| 245 | + - name: Set up Corepack + yarn |
| 246 | + run: | |
| 247 | + npm install -g corepack |
| 248 | + yarn set version 4.9.2 |
| 249 | +
|
| 250 | + - name: Install root dependencies |
| 251 | + run: yarn |
| 252 | + |
| 253 | + - name: Install Foundry |
| 254 | + uses: foundry-rs/foundry-toolchain@v1 |
| 255 | + |
| 256 | + - name: Build contracts |
| 257 | + run: | |
| 258 | + cd smart-contracts |
| 259 | + forge install foundry-rs/forge-std |
| 260 | + forge install OpenZeppelin/openzeppelin-contracts |
| 261 | + forge build |
| 262 | +
|
| 263 | + - name: Install Playwright browsers |
| 264 | + run: yarn playwright install --with-deps |
| 265 | + |
| 266 | + - name: Prepare wallet extensions |
| 267 | + run: | |
| 268 | + yarn prepare-metamask |
| 269 | + yarn prepare-coinbase |
| 270 | +
|
| 271 | + - name: Build application |
| 272 | + run: | |
| 273 | + echo "E2E_TEST_SEED_PHRASE=${{ secrets.E2E_TEST_SEED_PHRASE }}" > .env |
| 274 | + echo "E2E_CONTRACT_PROJECT_ROOT=../smart-contracts" >> .env |
| 275 | + yarn build |
| 276 | +
|
| 277 | + - name: Install xvfb |
| 278 | + run: sudo apt-get update && sudo apt-get install -y xvfb |
| 279 | + |
| 280 | + - name: Run E2E tests |
| 281 | + env: |
| 282 | + NODE_OPTIONS: '--dns-result-order=ipv4first' |
| 283 | + run: xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" yarn test:e2e |
| 284 | +``` |
| 285 | +
|
| 286 | +## Next Steps |
| 287 | +
|
| 288 | +Start testing your onchain application today: |
| 289 | +
|
| 290 | +1. Install OnchainTestKit in your project |
| 291 | +2. Write your first test following the examples above |
| 292 | +3. Integrate tests into your CI/CD pipeline |
| 293 | +4. Expand test coverage as you build new features |
| 294 | +5. See [example tests](https://github.com/coinbase/onchaintestkit/tree/master/example/frontend/e2e) |
| 295 | +6. Access the [full docs here](https://onchaintestkit.xyz/) |
| 296 | +
|
| 297 | +Remember: comprehensive testing leads to more reliable onchain applications and better user experiences. |
0 commit comments