Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions services/vault/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Vault E2E

Playwright suite for the vault dApp. The suite is **mock-first**: no
real Bitcoin regtest, no anvil, no live vault provider. Backend
responses are intercepted at the page-route layer with deterministic
payloads so tests stay fast and reproducible without provisioning
external services.

> This trade-off is deliberate. See issue #1589: full devnet harnessing
> blocks on the external contracts repo and a full VP implementation.
> Mock-first lets the per-flow tickets (#1590-#1602) land now and gives
> us coverage of the React surfaces, wallet wiring, and error paths.
> A future ticket can swap the route handlers for a real Anvil/bitcoind
> harness without rewriting the specs themselves.

## Layout

```
services/vault/e2e/
├── fixtures/
│ ├── mockBtcWallet.ts Deterministic IBTCProvider
│ ├── mockEthWallet.ts Deterministic viem WalletClient
│ ├── walletInjection.ts window.__BABYLON_E2E_WALLETS__ bridge
│ ├── seededWallets.ts Typed wrappers with declared balances
│ ├── networkRoutes.ts page.route() helpers (mempool, VP, eth)
│ ├── test.ts Playwright test.extend with fixtures
│ └── index.ts Public barrel - tests import from here
├── pages/ Page objects
│ ├── AppShell.ts
│ ├── Dashboard.ts
│ ├── DepositModal.ts
│ └── WithdrawModal.ts
└── *.spec.ts Specs
```

## Running tests

Playwright spins up the vault dev server itself (see
`playwright.config.ts` `webServer`). Tests intercept all network calls
via `page.route()`, so no separate backend is required.

```bash
pnpm --filter vault run test:e2e # headless
pnpm --filter vault run test:e2e:headed # with browser UI
pnpm --filter vault run test:e2e:ui # Playwright UI mode
pnpm --filter vault run test:e2e:report # open last HTML report
```

## Manual dev: standalone mock backends

Use this when you want to point a regular browser at the vault dApp
with deterministic backend responses (no Playwright).

```bash
pnpm --filter vault run e2e:env # starts the four mock listeners
NEXT_PUBLIC_E2E_MODE=1 pnpm --filter vault run dev
```

The stubs (defined in `services/vault/scripts/e2e-env.mjs`) listen on
the same ports `playwright.config.ts` uses:

| Port | Purpose |
| ---: | ------------------------------------ |
| 9996 | mempool API (`/mempool/...`) |
| 9997 | eth JSON-RPC (`POST /rpc`) |
| 9998 | vault-provider proxy (`/vp-health`, `/rpc/{vp}`) |
| 9999 | GraphQL (`POST /graphql`) |

The standalone stubs return generic defaults. Per-test customisation
goes through the route handlers in `networkRoutes.ts`, not the
standalone process.

## Writing a test

```ts
import {
test,
expect,
mockMempoolForSeededBtcWallet,
mockVpProxy,
} from "./fixtures";

test("dashboard reflects seeded BTC balance", async ({
page,
seededBtcWallet,
installWalletSentinel,
appShell,
dashboard,
}) => {
const wallet = seededBtcWallet({ amount: 250_000n });
await mockMempoolForSeededBtcWallet(page, wallet);
await mockVpProxy(page);
await installWalletSentinel({ btc: wallet });

await appShell.goto();
await expect(dashboard.collateralSectionHeading).toBeVisible();
});
```

## Conventions

- **One fixture, one concern.** `seededBtcWallet` only describes the
wallet; `mockMempoolForSeededBtcWallet` only wires its mempool
responses. Compose them at the test site so each test reads top to
bottom.
- **No magic defaults that hide failures.** A test that forgets to
install a route handler should fail loud, not silently fall through
to a default empty response. The route helpers prefer 501s over
silent 200s for unhandled paths.
- **Selectors prefer role + accessible name.** Where role is not
exposed, fall back to a stable `data-testid`. Adding new testids in
source (kebab-case, feature-specific) is preferred over CSS
selectors.
- **Each test starts clean.** Playwright tears down `page` between
tests; route handlers and `window.__BABYLON_E2E_WALLETS__` reset
automatically. Do not rely on prior test state.

## Override pattern

`page.route()` handlers register in order; the **last** matching
handler wins. Per-test overrides therefore go after fixture handlers:

```ts
await mockMempoolForSeededBtcWallet(page, wallet); // default
await page.route("**/v1/fees/recommended", (route) => // override
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ fastestFee: 50, halfHourFee: 40, hourFee: 30, economyFee: 20, minimumFee: 10 }),
}),
);
```

## CI

The suite runs under the standard playwright runner. CI parity is
trivial because there's nothing external to provision: GitHub Actions
runs `pnpm --filter vault run test:e2e` and the same in-test route
handlers fire. Sharding/parallelism lands with #1602.
85 changes: 85 additions & 0 deletions services/vault/e2e/fixtures-smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Smoke spec for the e2e fixture scaffold introduced in #1589.
*
* Verifies:
* - the typed seeded-wallet factories return values that satisfy the
* mempool wire shape the dApp's `useUTXOs` consumes;
* - `installWalletSentinel` writes a sentinel to
* `window.__BABYLON_E2E_WALLETS__` before navigation completes;
* - the page-object scaffold reaches the running app (AppShell on `/`).
*
* The deeper page interactions (clicking through the deposit modal,
* asserting a position appears) belong to the per-flow tickets.
*/

import {
E2E_WALLETS_GLOBAL,
expect,
mockMempoolForSeededBtcWallet,
mockVpProxy,
test,
} from "./fixtures";

test("seededBtcWallet exposes mempool-wire payloads that sum to the seeded amount", ({
seededBtcWallet,
}) => {
const wallet = seededBtcWallet({ amount: 250_000n });
expect(wallet.balanceSats).toBe(250_000n);
const totalValue = wallet.mempoolUtxos.reduce((s, u) => s + u.value, 0);
expect(BigInt(totalValue)).toBe(250_000n);
expect(wallet.mempoolAddressInfo.isvalid).toBe(true);
// Default address is signet P2WPKH (`tb1q...`), so the scriptPubKey
// is 0014 (OP_0 push-20) + 20-byte hash placeholder (40 hex).
expect(wallet.mempoolAddressInfo.scriptPubKey).toMatch(/^0014[0-9a-f]{40}$/);
});

test("seededBtcWallet rejects utxoSplit that doesn't sum to amount", ({
seededBtcWallet,
}) => {
expect(() =>
seededBtcWallet({ amount: 100n, utxoSplit: [40n, 50n] }),
).toThrow(/sum to 90n, expected 100n/);
});

test("seededEthWallet exposes balanceWeiHex as a valid quantity", ({
seededEthWallet,
}) => {
const wallet = seededEthWallet({ balanceWei: 5n * 10n ** 18n });
expect(wallet.balanceWeiHex).toBe(`0x${(5n * 10n ** 18n).toString(16)}`);
});

test("installWalletSentinel sets the e2e wallet global before navigation", async ({
page,
seededBtcWallet,
installWalletSentinel,
}) => {
const wallet = seededBtcWallet({ amount: 100_000n });
await mockMempoolForSeededBtcWallet(page, wallet);
await mockVpProxy(page);
await installWalletSentinel({ btc: wallet });
await page.goto("/");

const installed = await page.evaluate((name) => {
const w = window as unknown as Record<
string,
{ btc?: { kind?: string; address?: string } } | undefined
>;
return w[name];
}, E2E_WALLETS_GLOBAL);

expect(installed?.btc?.kind).toBe("seeded-btc");
expect(installed?.btc?.address).toBe(wallet.address);
});

test("AppShell page object reaches the app shell at /", async ({
page,
appShell,
}) => {
await mockVpProxy(page);
await appShell.goto();
// The shell renders the connect entry point even before a wallet is
// injected. We don't assert visibility (the unconnected state may
// show a different label depending on copy) - reaching the page
// without a navigation error is enough for the smoke check.
await expect(page).toHaveURL(/\/$/);
});
24 changes: 24 additions & 0 deletions services/vault/e2e/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,34 @@ export type {
MockEthWallet,
MockEthWalletOptions,
} from "./mockEthWallet";
export {
mockEthRpc,
mockEthRpcForSeededWallet,
mockGraphql,
mockHealthCheck,
mockMempoolForSeededBtcWallet,
mockVpProxy,
} from "./networkRoutes";
export { seededBtcWallet, seededEthWallet } from "./seededWallets";
export type {
SeededBtcWallet,
SeededBtcWalletOptions,
SeededEthWallet,
SeededEthWalletOptions,
SeededMempoolAddressInfo,
SeededMempoolUtxo,
} from "./seededWallets";
export { expect, test } from "./test";
export type { VaultE2EFixtures } from "./test";
export {
E2E_WALLETS_GLOBAL,
clearInjectedWallets,
getInjectedWallets,
injectWallets,
} from "./walletInjection";
export type { InjectedWallets } from "./walletInjection";
export {
btcWalletConfigFromSeeded,
injectBtcWalletProvider,
} from "./walletPageInjection";
export type { BtcWalletPageConfig } from "./walletPageInjection";
Loading