Why This Chapter Exists
Early changes to a live site shipped without automated tests, and three of them broke the build. A TypeScript type error, a BigInt syntax incompatibility, and a DNS failure in a price API — any one of which would have been caught by a 5-second test run. This chapter is the fix: a test suite that proves the framework works before code ships.
Run the Tests
$ cd x402-demo $ npx vitest run ✓ __tests__/x402-client.test.js (10 tests) ✓ __tests__/adapters/solana.test.js (13 tests) ✓ __tests__/adapters/evm.test.js (7 tests) ✓ __tests__/adapters/walletconnect.test.js (9 tests) Test Files 4 passed (4) Tests 39 passed (39) Duration 1.32s
What's Covered
Client Core (10 tests)
Creation with valid/invalid config, adapter registry (register, list chains, reject incomplete adapters, chaining), event emitter (subscribe, emit, unsubscribe), wallet detection aggregation across multiple chains, pay-path error handling for unsupported chains and missing amounts.
Solana Adapter (13 tests)
Wallet detection (Phantom installed, multiple wallets, no wallet). USDC verification with 8 attack scenarios from our security specialist's threat model:
| Test | Attack Blocked |
|---|---|
| Non-existent txHash | Fake payment submission |
| Failed transaction | Reverted tx used as proof |
| No transfer to recipient | Payment sent to wrong wallet |
| Partial payment | Underpayment fraud ($0.50 for $12 item) |
| Correct amount (positive) | Proves legitimate payments work |
| Net-delta refund attack | Jupiter multi-hop: credit $10, claw back $9.50 |
| Wrong token | Scam token impersonation |
| Stale transaction | Reusing an ancient unrelated payment |
Ethereum Adapter (7 tests)
Same attack coverage for Base/Polygon/Arbitrum USDC: valid transfer, not-found, failed, wrong recipient, partial payment, wrong token contract, and the Ethereum net-delta refund attack (two Transfer events — credit then debit in one receipt).
WalletConnect Adapter (9 tests)
Deep link URL generation (Phantom, Solflare, query params, localhost). Mobile vs desktop detection. Project ID handling. Deep-link redirect throw behavior. WalletConnect error messaging.
How It Works
Wallet Mocking
Reusable factory functions simulate wallet providers without real browser extensions:
// From __tests__/helpers/mock-providers.js
function mockPhantomProvider(opts = {}) {
return {
isPhantom: true,
connect: async () => ({
publicKey: { toString: () => opts.pubkey || 'FakePubkey...' }
}),
signTransaction: async (tx) => {
if (opts.rejectSign) throw new Error('User rejected')
return { serialize: () => new Uint8Array(256) }
},
}
}
Transaction Fixtures
Verification tests use real RPC JSON responses captured from the Solana network; a real $0.02 USDC payment is one of the fixture files. Attack scenarios are crafted variants. Because fixtures ARE the RPC protocol spec, they only break if the blockchain protocol itself changes.
Why Vitest, Not Playwright
The adapters are pure ES modules with injected dependencies. Mocking window.phantom.solana in jsdom is identical to mocking it in a real browser — the API surface is the same JavaScript object. Playwright adds CI complexity (browser binaries, flake) for zero additional coverage. Reserved for one future smoke test: "page loads, Buffer polyfill present, no console errors."
What This Prevents
If we'd had this suite before our three broken PRs:
- BigInt literal (
0n) → test run fails before push - TypeScript strict-cast error → type-check step catches it
- Jupiter price API down → price-fallback test validates CoinGecko works
Rule going forward: npx vitest run passes before any PR ships. If it doesn't pass, it doesn't ship.