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
62 changes: 62 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,64 @@ jobs:
env:
REQUIRE_WASM: "1"

mock-e2e:
name: Mock host browser E2E
# Runs on every PR for signal, but is not yet part of the required `ci-status`
# gate (see below) — promote it once it has a green baseline in CI.
runs-on: ubuntu-latest
needs: codegen
timeout-minutes: 30
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # stable
with:
toolchain: stable
targets: wasm32-unknown-unknown

- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22

- name: Download codegen output
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: codegen-output

- name: Install workspace deps
run: npm ci --ignore-scripts

- name: Build @parity/truapi
run: npm run build --prefix js/packages/truapi

- name: Install wasm-pack
run: cargo install wasm-pack --version 0.15.0 --locked

- name: Build WASM
run: npm run build:wasm --prefix js/packages/truapi-host-wasm

- name: Build @parity/truapi-host-wasm
run: npm run build --prefix js/packages/truapi-host-wasm

- name: Install Playwright browser
working-directory: js/packages/truapi-mock-e2e
run: npx playwright install --with-deps chromium

- name: Run browser E2E
run: npm run test:e2e --prefix js/packages/truapi-mock-e2e

- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: mock-e2e-report
path: js/packages/truapi-mock-e2e/playwright-report
retention-days: 14

playground:
name: Playground (build + lint)
runs-on: ubuntu-latest
Expand Down Expand Up @@ -327,6 +385,8 @@ jobs:
name: CI Status
if: always()
runs-on: ubuntu-latest
# mock-e2e is intentionally NOT listed: it runs for signal but neither gates
# merges nor blocks this aggregate status on its (up to 30-min) run.
needs: [rust, licenses, codegen, ts-client, host-wasm, playground, explorer, e2e]
steps:
- name: Check all jobs
Expand All @@ -337,6 +397,8 @@ jobs:
"${{ needs.codegen.result }}"
"${{ needs.ts-client.result }}"
"${{ needs.host-wasm.result }}"
# mock-e2e intentionally omitted: it runs for signal but does not gate
# merges until it has a green baseline in CI.
"${{ needs.playground.result }}"
"${{ needs.explorer.result }}"
"${{ needs.e2e.result }}"
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ js/packages/
`.` (shared host types), `/web` (iframe + Web
Worker), `/worker-runtime` (Worker entry).
WASM bundle (gitignored) under dist/wasm/web/, built via `make wasm`
truapi-mock-e2e/ @parity/truapi-mock-e2e: browser E2E harness (private; real WASM core + product-in-iframe vs the mock host)
playground/ Next.js interactive playground; deploys to truapi-playground.dot
hosts/dotli/ dotli submodule
docs/ design docs, RFCs, feature proposals
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ js/packages/
truapi/ @parity/truapi TypeScript client
truapi-host-wasm/ @parity/truapi-host-wasm: WASM-backed host runtime; entries `.`
(shared host types), `/web` (iframe + Web Worker),
`/worker-runtime`
`/worker-runtime`, `/testing` (createMockClient)
truapi-mock-e2e/ @parity/truapi-mock-e2e: browser E2E harness (real WASM core +
product-in-iframe, driven against the mock host); private
playground/ Interactive Next.js playground (truapi-playground.dot)
hosts/dotli/ dotli host, vendored as a submodule
docs/ Design docs, RFCs, feature proposals
Expand All @@ -80,6 +82,8 @@ a single package with tree-shakeable subpath entries:
MessageChannel handshake (`createIframeHost`) plus `createWebWorkerProvider`.
- `@parity/truapi-host-wasm/worker-runtime` is the Web Worker entrypoint so the WASM core can
run off the page main thread.
- `@parity/truapi-host-wasm/testing` exposes `createMockClient`, which wires a product client to
the real core running against the mock host in one call (for tests and product "mock mode").

## How it works

Expand Down
22 changes: 22 additions & 0 deletions js/packages/truapi-host-wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,28 @@ check until the mock covers it. `wasm-bridge.test.ts` drives the real WASM core
mock headlessly (no browser, no worker). Public API: `createMockHost`, `mockRuntimeConfig`,
`MockHost`, `MockHostConfig`, `PermissionPolicy`.

### One call — `createMockClient`

`@parity/truapi-host-wasm/testing` exports `createMockClient`, which collapses
`createMockHost` + `createWebWorkerProvider` + `createClient` into a single call. It returns
the exact product client a product uses in production, plus the mock for assertions. Pass the
core Worker so your bundler owns how it is produced.

```ts
// `?worker` is a Vite/bundler-specific suffix; other bundlers use their own form.
import HostWorker from "@parity/truapi-host-wasm/worker-runtime?worker";
import { createMockClient } from "@parity/truapi-host-wasm/testing";

const { client, mock } = await createMockClient(new HostWorker(), {
devicePermissions: "allow-all",
});
await client.system.handshake();
// ... drive the product client; assert via mock.navigations(), etc.
```

The full browser end-to-end harness that exercises this across a Web Worker and an iframe
lives in the `@parity/truapi-mock-e2e` package.

## Publishing

The npm publish workflow is not wired yet. A release-process discussion is needed before adding a
Expand Down
4 changes: 4 additions & 0 deletions js/packages/truapi-host-wasm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
"types": "./dist/web/index.d.ts",
"import": "./dist/web/index.js"
},
"./testing": {
"types": "./dist/testing.d.ts",
"import": "./dist/testing.js"
},
"./worker-runtime": {
"types": "./dist/worker-runtime.d.ts",
"import": "./dist/worker-runtime.js"
Expand Down
35 changes: 35 additions & 0 deletions js/packages/truapi-host-wasm/src/testing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// One-call setup for driving a product against the mock host. It collapses
// `createMockHost` + `createWebWorkerProvider` + `createClient` into a single
// call, so a test (or a product's "mock mode") gets a ready-to-use client in one
// line. Experimental: the shape may change as more product test-cases adopt it.
import { createClient, createTransport, type TrUApiClient } from "@parity/truapi";
import { createMockHost, createWebWorkerProvider, mockRuntimeConfig } from "./web/index.js";
import type { MockHost, MockHostConfig, WebWorkerHostCallbacks } from "./web/index.js";

/** A product client wired to the mock host, plus the mock for assertions. */
export interface MockClient {
/** The product SDK client — the exact object a product uses in production. */
client: TrUApiClient;
/** The mock host, exposing recorded oracles (navigations, confirmations, …). */
mock: MockHost;
}

/**
* Wire a product {@link TrUApiClient} to the real WASM core running against the
* mock host, in a single call.
*
* Pass the core Worker — e.g. `new HostWorker()` from
* `@parity/truapi-host-wasm/worker-runtime?worker` — so the caller's bundler owns
* how the worker is produced. The returned `client` talks to the real dispatcher
* over the mocked platform seam; `mock` lets a test assert on what the core asked
* the device to do.
*/
export async function createMockClient(worker: Worker, config?: MockHostConfig): Promise<MockClient> {
const mock = createMockHost(config);
// createMockHost implements every callback (its impl is `Required<HostCallbacks>`),
// so it satisfies the provider's stricter callback surface.
const provider = await createWebWorkerProvider(worker, mock.callbacks as WebWorkerHostCallbacks, {
runtimeConfig: mockRuntimeConfig(),
});
return { client: createClient(createTransport(provider)), mock };
}
5 changes: 5 additions & 0 deletions js/packages/truapi-mock-e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
*.tsbuildinfo
test-results/
playwright-report/
52 changes: 52 additions & 0 deletions js/packages/truapi-mock-e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# @parity/truapi-mock-e2e

Browser E2E harness for the TrUAPI mock host. It proves, **in a real browser**,
that a product runs against the mocked host with the **real Rust core (WASM)** —
no device, no wallet, no phone. Private workspace package; not published.

Unlike the headless `wasm-bridge.test.ts` in `@parity/truapi-host-wasm`, this
exercises the full production transport: the real core in a Web Worker, the
product in an **iframe** connecting via the SDK's real `getClientSync()` sandbox
path over a real `MessageChannel`. Nothing hand-rolls the wire protocol.

## Topologies

- **Single page** (`/` → `src/main.tsx`): host + product in one page; the whole
setup is a single `createMockClient(new HostWorker())` call
(`@parity/truapi-host-wasm/testing`).
- **Iframe** (`/host.html` + `/product.html`): mirrors production embedding. The
host boots the core + `createIframeHost`; the product runs in an iframe with
**no mock code**, connecting through `getClientSync()`.

## Prerequisites

The workspace packages must be built first (their `dist/` present, including the
WASM bundle):

```bash
# from the repo root
make wasm
( cd js/packages/truapi && npm run build )
( cd js/packages/truapi-host-wasm && npm run build )
```

## Run

```bash
cd js/packages/truapi-mock-e2e
npm run dev # http://localhost:4319/ (single page)
# http://localhost:4319/host.html (iframe topology)
npm run test:e2e # headless Playwright: iframe topology, asserts each call
```

## What "mock mode" means

The product never talks to a device. Its transport points at the mock host
instead of the real one — the product's own call code is unchanged. Every call
flows through the real dispatcher; the mock only decides what "the device" says.
This browser harness exercises the platform-seam calls (storage, permissions,
features, navigation, notifications, preimage, theme). Signing/login are **not**
driven here — that coverage lives in the Rust through-core tests (PR #258), where
the mock wallet completes login and signing in-process (deterministic and valid
in-process, but **not chain-valid** — a real on-chain signature needs a genuine
signer / signer-bot).
12 changes: 12 additions & 0 deletions js/packages/truapi-mock-e2e/host.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>TrUAPI mock host (iframe topology)</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/host.tsx"></script>
</body>
</html>
12 changes: 12 additions & 0 deletions js/packages/truapi-mock-e2e/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>TrUAPI mock host — browser E2E</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
29 changes: 29 additions & 0 deletions js/packages/truapi-mock-e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@parity/truapi-mock-e2e",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Browser E2E harness: the real truapi-server WASM core in a Web Worker with a product in an iframe, driven against the mock host. Not published.",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview --port 4319 --strictPort",
"test:e2e": "playwright test",
"typecheck": "tsc -b"
},
"dependencies": {
"@parity/truapi": "file:../truapi",
"@parity/truapi-host-wasm": "file:../truapi-host-wasm",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@playwright/test": "^1.49.1",
"@types/node": "^20.11.0",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7",
"vite": "^5.4.11"
}
}
21 changes: 21 additions & 0 deletions js/packages/truapi-mock-e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineConfig } from "@playwright/test";

// Local-mode browser E2E: the product runs inside the mock host (the real
// truapi-server WASM core in a Worker), driven headless. The dev server serves
// the pre-built workspace packages; build them first (see README / CI).
export default defineConfig({
testDir: "./tests",
timeout: 60_000,
expect: { timeout: 40_000 },
reporter: [["list"]],
use: {
baseURL: "http://localhost:4319",
headless: true,
},
webServer: {
command: "npm run dev",
url: "http://localhost:4319/host.html",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
11 changes: 11 additions & 0 deletions js/packages/truapi-mock-e2e/product.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TrUAPI product (mock mode)</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/product.tsx"></script>
</body>
</html>
Loading