NYM-1199: Node Families E2E with mock app shell & native webview tests
Implements the new Node Families E2E testing strategy, shifting the primary Playwright suite from Storybook iframes to a dedicated mock-wired app shell (`main.mock.html`). This provides higher-fidelity testing against the actual wallet UI and router without a Tauri runtime or login. Also introduces an optional WebdriverIO + `tauri-driver` suite for native WebKitGTK webview validation, configured as a non-blocking CI job for Linux. A critical fix for `NymCard`'s `data-testid` prop is included to enable robust UI selectors across all E2E tiers. Updates CI workflows, adds new scripts for mock builds and native tests, and provides comprehensive documentation in `e2e/README.md` for the tiered testing approach.
This commit is contained in:
@@ -45,6 +45,14 @@ jobs:
|
||||
- name: Unit tests (nym-wallet)
|
||||
run: pnpm --filter @nymproject/nym-wallet-app test
|
||||
|
||||
- name: Install Playwright browser (chromium)
|
||||
run: pnpm --filter @nymproject/nym-wallet-app exec playwright install --with-deps chromium
|
||||
|
||||
- name: e2e — Playwright vs mock-wired dev server (primary, design D1)
|
||||
# Launches the mock-wired dev server (webpack:dev:mock, WALLET_MOCK_FAMILIES=on)
|
||||
# via Playwright's webServer and replays the owner/operator journeys. Fails the job.
|
||||
run: pnpm --filter @nymproject/nym-wallet-app test:e2e
|
||||
|
||||
- name: Prepare build output directory
|
||||
shell: bash
|
||||
env:
|
||||
@@ -75,3 +83,54 @@ jobs:
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
# Optional native-webview validation (design D4/D8): drives the packaged Tauri binary
|
||||
# through tauri-driver + WebdriverIO under xvfb. Linux-only; non-blocking until stabilised.
|
||||
# NOTE: the final wiring (Tauri window → main.mock.html in the mock build) is a documented
|
||||
# TODO in wdio.conf.ts; this job is the scaffold that runs once that lands.
|
||||
e2e-tauri:
|
||||
runs-on: ubuntu-22.04
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install Tauri + WebDriver system deps
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev webkit2gtk-driver xvfb \
|
||||
libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev \
|
||||
build-essential curl wget file libssl-dev libsoup-3.0-dev
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 11.1.2
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: nym-wallet/.nvmrc
|
||||
cache: pnpm
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: nym-wallet/src-tauri
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build TypeScript packages
|
||||
run: pnpm build:types && pnpm build:packages
|
||||
|
||||
- name: Install tauri-driver
|
||||
run: cargo install tauri-driver --locked
|
||||
|
||||
- name: Build mock-wired Tauri binary
|
||||
# TODO (native-leg wiring): build the Tauri app from the WALLET_MOCK_FAMILIES=on
|
||||
# frontend with the window pointed at main.mock.html. See wdio.conf.ts.
|
||||
run: pnpm --filter @nymproject/nym-wallet-app tauri:build:mock
|
||||
|
||||
- name: Run WebdriverIO native-webview suite (under xvfb)
|
||||
run: xvfb-run -a pnpm --filter @nymproject/nym-wallet-app test:e2e:tauri
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
test-results/
|
||||
@@ -0,0 +1,60 @@
|
||||
import { FAMILY_NODES, TID } from '../e2e/shared/families';
|
||||
|
||||
/**
|
||||
* Native-webview replay of the owner + operator journeys (design D4), sharing selectors +
|
||||
* fixtures with the primary Playwright suite via `e2e/shared/families` (parity requirement).
|
||||
*
|
||||
* SCAFFOLD: runnable once the native-leg wiring lands (see wdio.conf.ts) — the mock-wired
|
||||
* binary must launch the Family page per persona. Each `describe` below assumes the binary
|
||||
* was started in the matching persona (owner / operator); per-persona launch is the TODO.
|
||||
*/
|
||||
|
||||
const byId = (id: string) => $(`[data-testid="${id}"]`);
|
||||
const inGroup = (node: number, id: string) => byId(TID.inviteGroup(node)).$(`[data-testid="${id}"]`);
|
||||
|
||||
const { ownerFlow, operatorAccept, operatorReject } = FAMILY_NODES;
|
||||
|
||||
describe('Families flows — native webview (owner persona)', () => {
|
||||
it('owner lifecycle: create → invite → accept → kick → disband', async () => {
|
||||
await byId(TID.createFamilyName).waitForDisplayed({ timeout: 30_000 });
|
||||
await byId(TID.createFamilyName).setValue('Flow Family');
|
||||
await byId(TID.createFamilyDescription).setValue('A family created in a flow test.');
|
||||
await byId(TID.createFamilySubmit).click();
|
||||
await byId(TID.ownerManagementPage).waitForDisplayed();
|
||||
|
||||
await byId(TID.inviteNodeId).setValue(String(ownerFlow));
|
||||
await byId(TID.inviteNodeSubmit).click();
|
||||
await byId(TID.inviteNodeConfirm).click();
|
||||
await byId(TID.pendingInvite(ownerFlow)).waitForDisplayed();
|
||||
|
||||
await byId(TID.tabOperator).click();
|
||||
await inGroup(ownerFlow, TID.acceptCard).click();
|
||||
await byId(TID.acceptConfirm).click();
|
||||
await byId(TID.operatorNodeFamily(ownerFlow)).waitForDisplayed();
|
||||
|
||||
await byId(TID.tabOwner).click();
|
||||
await byId(TID.memberJoinedKick(ownerFlow)).click();
|
||||
await byId(TID.memberJoinedKickConfirm(ownerFlow)).click();
|
||||
await byId(TID.memberJoined(ownerFlow)).waitForExist({ reverse: true });
|
||||
|
||||
await byId(TID.deleteButton).click();
|
||||
await byId(TID.deleteConfirm).click();
|
||||
await byId(TID.createFamilyName).waitForDisplayed();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Families flows — native webview (operator persona)', () => {
|
||||
it('operator lifecycle: accept → leave, then reject', async () => {
|
||||
await byId(TID.tabOperator).click();
|
||||
await inGroup(operatorAccept, TID.acceptCard).click();
|
||||
await byId(TID.acceptConfirm).click();
|
||||
await byId(TID.operatorNodeFamily(operatorAccept)).waitForDisplayed();
|
||||
|
||||
await byId(TID.leaveButton).click();
|
||||
await byId(TID.leaveConfirm).click();
|
||||
|
||||
await inGroup(operatorReject, TID.rejectCard).click();
|
||||
await byId(TID.rejectConfirm).click();
|
||||
await byId(TID.inviteGroupEmpty(operatorReject)).waitForDisplayed();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
// Skip-not-fail launcher for the optional native-webview leg (design D5).
|
||||
// tauri-driver only works on Linux/Windows; on macOS (no WKWebView driver) or when the
|
||||
// required drivers are missing, this exits 0 with a clear message instead of failing.
|
||||
import { execSync } from 'node:child_process';
|
||||
import { platform } from 'node:os';
|
||||
|
||||
const skip = (why) => {
|
||||
console.log(`[e2e:tauri] skipped — ${why}`);
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
const has = (bin) => {
|
||||
try {
|
||||
execSync(`command -v ${bin}`, { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (platform() === 'darwin') skip('macOS has no WKWebView driver (tauri-driver unsupported) — use the Playwright suite locally.');
|
||||
if (platform() === 'win32' ? !has('msedgedriver') : !(has('WebKitWebDriver') || has('webkit2gtk-driver')))
|
||||
skip('platform webdriver not found (install webkit2gtk-driver on Linux / msedgedriver on Windows).');
|
||||
if (!has('tauri-driver')) skip('tauri-driver not found — run `cargo install tauri-driver --locked`.');
|
||||
|
||||
execSync('wdio run ./wdio.conf.ts', { stdio: 'inherit' });
|
||||
@@ -0,0 +1,57 @@
|
||||
# Node Families e2e
|
||||
|
||||
Three tiers validate the Family page journeys. They share selectors + fixtures via
|
||||
[`e2e/shared/families.ts`](./shared/families.ts) so they cannot drift apart.
|
||||
|
||||
## Mock-wired build (`WALLET_MOCK_FAMILIES`)
|
||||
|
||||
The real wallet bootstrap (`AppProvider`) is Tauri-coupled and login-gated, so it can't run
|
||||
in a plain browser. A dedicated, flag-gated mock entry solves this (design D2):
|
||||
|
||||
- `WALLET_MOCK_FAMILIES=on` adds a `mainMock` webpack entry + `main.mock.html` that mounts
|
||||
the real router + `ApplicationLayout` + Family page, but with the Storybook mocks
|
||||
(`MockMainContextProvider` + `MockFamiliesContextProvider`) — no Tauri, no login.
|
||||
- Off (default, and always in production) the mock entry/HTML are never built.
|
||||
- Persona is chosen at runtime: `main.mock.html?persona=owner|operator|operator-seeded`.
|
||||
|
||||
Run it: `pnpm webpack:dev:mock` → http://localhost:9000/main.mock.html?persona=owner#/family
|
||||
|
||||
## Tier 1 — Playwright vs dev server (primary, cross-platform)
|
||||
|
||||
The main suite. Runs everywhere incl. macOS; this is what gates CI.
|
||||
|
||||
```
|
||||
pnpm --dir .. run build # once: build workspace packages (@nymproject/types, etc.)
|
||||
npx playwright install chromium # once
|
||||
pnpm test:e2e # launches webpack:dev:mock + replays the journeys
|
||||
```
|
||||
|
||||
Config: [`playwright.config.ts`](../playwright.config.ts). Specs: [`families.spec.ts`](./families.spec.ts).
|
||||
|
||||
## Tier 2 — WebdriverIO + tauri-driver (optional, Linux CI only)
|
||||
|
||||
Drives the **packaged Tauri binary** in the native WebKitGTK webview. macOS is unsupported
|
||||
(no WKWebView driver), so `pnpm test:e2e:tauri` **skips** there (design D5). Runs in the
|
||||
non-blocking `e2e-tauri` CI job under `xvfb`.
|
||||
|
||||
- Config: [`../wdio.conf.ts`](../wdio.conf.ts) · Spec: [`../e2e-tauri/families.tauri.ts`](../e2e-tauri/families.tauri.ts)
|
||||
- Prereqs (Linux): `webkit2gtk-driver`, `cargo install tauri-driver --locked`.
|
||||
- **TODO (native-leg wiring):** point the mock binary's window at `main.mock.html?persona=…`
|
||||
and confirm the release binary path in `wdio.conf.ts`. Until then this tier is a scaffold.
|
||||
|
||||
## Tier 3 — Sandbox real-IPC read smoke (optional, manual)
|
||||
|
||||
The node-families contract is deployed to **sandbox** (currently one family, one member).
|
||||
This smoke validates the *real* `FamiliesContextProvider` + `requests/families.ts` wiring
|
||||
that the mock stands in for — separate from, and non-blocking relative to, the mock tiers.
|
||||
|
||||
Manual procedure (design D9):
|
||||
|
||||
1. Launch the real wallet (`pnpm dev`) and sign into a **sandbox** account (network `SANDBOX`).
|
||||
2. Open the Family page and confirm it renders the known sandbox family/member via real IPC.
|
||||
3. **Read-only** — do not create/invite/kick/disband against the shared sandbox.
|
||||
4. Assert render/shape (or pin the known family id), not exact contents, so a contract
|
||||
redeploy doesn't hard-fail.
|
||||
|
||||
Promote to a non-blocking CI job only once a sandbox test account can be provisioned
|
||||
headlessly (mnemonic in CI secrets) — a follow-up, not a blocker.
|
||||
@@ -1,29 +1,84 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
import { FAMILY_IDS, FAMILY_NODES, familyMockUrl, TID } from './shared/families';
|
||||
|
||||
/**
|
||||
* e2e coverage of the owner + operator flows (tasks.md §8.4), driven against the
|
||||
* Storybook flow stories. Each flow story's `play` function runs automatically when
|
||||
* the story iframe loads, so we navigate to the story and assert the post-flow DOM.
|
||||
* Primary e2e of the owner + operator Node Families journeys (design D1), driven against
|
||||
* the mock-wired app shell (`main.mock.html`, design D2) on the dev server. Selectors are
|
||||
* shared with the optional WebdriverIO leg via `./shared/families` (parity requirement)
|
||||
* and target the ids that actually render (see the note in that file).
|
||||
*
|
||||
* Confirmation dialogs portal to the document body; in a single browser DOM they're reached
|
||||
* at page scope. Per-node invite content is scoped via the `operator-node-<n>` wrapper, and
|
||||
* invite buttons are keyed by family id.
|
||||
*/
|
||||
|
||||
const storyUrl = (id: string) => `/iframe.html?id=${id}&viewMode=story`;
|
||||
const { ownerFlow, operatorAccept, operatorReject, operatorNone } = FAMILY_NODES;
|
||||
|
||||
test.describe('Families flows', () => {
|
||||
const openOperatorTab = (page: Page) => page.getByTestId(TID.tabOperator).click();
|
||||
|
||||
test.describe('Families flows (mock-wired app shell)', () => {
|
||||
test('owner lifecycle: create → invite → accept → kick → disband', async ({ page }) => {
|
||||
await page.goto(storyUrl('families-flows--owner-lifecycle'));
|
||||
// After disband the family is gone, so the create entry point returns.
|
||||
await expect(page.getByTestId('create-family-name')).toBeVisible({ timeout: 30_000 });
|
||||
const fid = FAMILY_IDS.ownerFlow;
|
||||
await page.goto(familyMockUrl('owner'));
|
||||
|
||||
// create
|
||||
await expect(page.getByTestId(TID.createFamilyName)).toBeVisible({ timeout: 30_000 });
|
||||
await page.getByTestId(TID.createFamilyName).fill('Flow Family');
|
||||
await page.getByTestId(TID.createFamilyDescription).fill('A family created in a flow test.');
|
||||
await page.getByTestId(TID.createFamilySubmit).click();
|
||||
await expect(page.getByTestId(TID.ownerManagementPage)).toBeVisible();
|
||||
|
||||
// invite the self-controlled node
|
||||
await page.getByTestId(TID.inviteNodeId).fill(String(ownerFlow));
|
||||
await page.getByTestId(TID.inviteNodeSubmit).click();
|
||||
await page.getByTestId(TID.inviteNodeConfirm).click();
|
||||
await expect(page.getByTestId(TID.pendingInvite(ownerFlow))).toBeVisible();
|
||||
|
||||
// accept it from the operator tab (same account controls the node)
|
||||
await openOperatorTab(page);
|
||||
await page.getByTestId(TID.operatorNodeSection(ownerFlow)).getByTestId(TID.acceptCard(fid)).click();
|
||||
await page.getByTestId(TID.acceptConfirm(fid)).click();
|
||||
|
||||
// kick it from the owner tab — the joined member appears, then is removed
|
||||
await page.getByTestId(TID.tabOwner).click();
|
||||
await expect(page.getByTestId(TID.memberJoined(ownerFlow))).toBeVisible();
|
||||
await page.getByTestId(TID.memberJoinedKick(ownerFlow)).click();
|
||||
await page.getByTestId(TID.memberJoinedKickConfirm(ownerFlow)).click();
|
||||
await expect(page.getByTestId(TID.memberJoined(ownerFlow))).toHaveCount(0);
|
||||
|
||||
// disband the now-empty family
|
||||
await page.getByTestId(TID.deleteButton).click();
|
||||
await page.getByTestId(TID.deleteConfirm).click();
|
||||
await expect(page.getByTestId(TID.createFamilyName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('operator lifecycle: accept → leave, then reject', async ({ page }) => {
|
||||
await page.goto(storyUrl('families-flows--operator-lifecycle'));
|
||||
// After rejecting the last invite, the reject-node group is empty.
|
||||
await expect(page.getByTestId('node-invite-group-204-empty')).toBeVisible({ timeout: 30_000 });
|
||||
const fid = FAMILY_IDS.operatorFlow;
|
||||
await page.goto(familyMockUrl('operator'));
|
||||
await openOperatorTab(page);
|
||||
|
||||
// accept the invite on the accept-node (scope by node; invite buttons are keyed by family id)
|
||||
const acceptSection = page.getByTestId(TID.operatorNodeSection(operatorAccept));
|
||||
await acceptSection.getByTestId(TID.acceptCard(fid)).click();
|
||||
await page.getByTestId(TID.acceptConfirm(fid)).click();
|
||||
// joined → a Leave action appears for that node
|
||||
await expect(acceptSection.getByTestId(TID.leaveButton)).toBeVisible();
|
||||
|
||||
// leave the family
|
||||
await acceptSection.getByTestId(TID.leaveButton).click();
|
||||
await page.getByTestId(TID.leaveConfirm).click();
|
||||
|
||||
// reject the invite on the reject-node → its group ends empty
|
||||
await page.getByTestId(TID.operatorNodeSection(operatorReject)).getByTestId(TID.rejectCard(fid)).click();
|
||||
await page.getByTestId(TID.rejectConfirm(fid)).click();
|
||||
await expect(page.getByTestId(TID.inviteGroupEmpty(operatorReject))).toBeVisible();
|
||||
});
|
||||
|
||||
test('operator page shows multi-node invite states', async ({ page }) => {
|
||||
await page.goto(storyUrl('families-pages-operatorinvitespage--multi-node'));
|
||||
await expect(page.getByTestId('node-invite-group-201')).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByTestId('node-invite-group-203-empty')).toBeVisible();
|
||||
await page.goto(familyMockUrl('operator-seeded'));
|
||||
await openOperatorTab(page);
|
||||
// node with an active invite renders its section; node with none shows the empty state
|
||||
await expect(page.getByTestId(TID.operatorNodeSection(operatorAccept))).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByTestId(TID.inviteGroupEmpty(operatorNone))).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Shared journey constants for the Node Families e2e suites (parity requirement,
|
||||
* design D3). Both the primary Playwright suite (`e2e/families.spec.ts`) and the
|
||||
* optional WebdriverIO native leg (`e2e-tauri/families.tauri.ts`) import these.
|
||||
*
|
||||
* These are the test ids that render reliably in the DOM. We scope by the `operator-node-<n>`
|
||||
* Stack wrapper / table rows and key invite buttons by family id (the ids emitted by
|
||||
* `ConfirmActionButton`, e.g. `invite-card-<familyId>-accept`). Historically `NymCard` dropped
|
||||
* `data-testid` (its prop was `dataTestid`), so `node-invite-group-<n>` / `invite-card-<fid>`
|
||||
* didn't render and the original Storybook play-function selectors were invalid; that was fixed
|
||||
* in the `fix-nymcard-data-testid` change. These selectors remain valid and are kept as-is.
|
||||
*/
|
||||
|
||||
export type FamilyPersona = 'owner' | 'operator' | 'operator-seeded';
|
||||
|
||||
/** Fixture node ids (mirror src/context/mocks/families.fixtures.ts). */
|
||||
export const FAMILY_NODES = {
|
||||
ownerFlow: 301, // MOCK_OWNER_FLOW_NODE
|
||||
operatorAccept: 201, // MOCK_OPERATOR_FLOW_ACCEPT_NODE / MOCK_OPERATOR_NODE_ACTIVE
|
||||
operatorReject: 204, // MOCK_OPERATOR_FLOW_REJECT_NODE
|
||||
operatorNone: 203, // MOCK_OPERATOR_NODE_NONE
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* The family_id that issued each persona's invites — invite-card test ids are keyed by
|
||||
* family_id, not node id. Owner-flow + operator-flow each have a single family (id 1);
|
||||
* the seeded store's operator invites come from the second family (id 2).
|
||||
*/
|
||||
export const FAMILY_IDS = {
|
||||
ownerFlow: 1,
|
||||
operatorFlow: 1,
|
||||
seeded: 2,
|
||||
} as const;
|
||||
|
||||
/** Route into the mock-wired app shell for a given persona (see PERSONAS in src/main.mock.tsx). */
|
||||
export const familyMockUrl = (persona: FamilyPersona) => `/main.mock.html?persona=${persona}#/family`;
|
||||
|
||||
/** Test ids that actually render (parameterised by node id or family id where noted). */
|
||||
export const TID = {
|
||||
// owner: create + manage
|
||||
createFamilyName: 'create-family-name',
|
||||
createFamilyDescription: 'create-family-description',
|
||||
createFamilySubmit: 'create-family-submit',
|
||||
ownerManagementPage: 'owner-management-page',
|
||||
inviteNodeId: 'invite-node-id',
|
||||
inviteNodeSubmit: 'invite-node-submit',
|
||||
inviteNodeConfirm: 'invite-node-confirm',
|
||||
deleteButton: 'delete-family-button',
|
||||
deleteConfirm: 'delete-family-button-confirm',
|
||||
pendingInvite: (node: number) => `pending-invite-${node}`,
|
||||
memberJoined: (node: number) => `member-joined-${node}`,
|
||||
memberJoinedKick: (node: number) => `member-joined-${node}-kick`,
|
||||
memberJoinedKickConfirm: (node: number) => `member-joined-${node}-kick-confirm`,
|
||||
// tabs
|
||||
tabOwner: 'family-tab-owner',
|
||||
tabOperator: 'family-tab-operator',
|
||||
// operator: per-node section wrapper (Stack — renders) used for scoping
|
||||
operatorNodeSection: (node: number) => `operator-node-${node}`,
|
||||
inviteGroupEmpty: (node: number) => `node-invite-group-${node}-empty`,
|
||||
leaveButton: 'leave-family-button',
|
||||
leaveConfirm: 'leave-family-button-confirm',
|
||||
// invite cards: keyed by FAMILY id (ConfirmActionButton dataTestid — renders)
|
||||
acceptCard: (familyId: number) => `invite-card-${familyId}-accept`,
|
||||
acceptConfirm: (familyId: number) => `invite-card-${familyId}-accept-confirm`,
|
||||
rejectCard: (familyId: number) => `invite-card-${familyId}-reject`,
|
||||
rejectConfirm: (familyId: number) => `invite-card-${familyId}-reject-confirm`,
|
||||
} as const;
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-06-08
|
||||
@@ -0,0 +1,43 @@
|
||||
## Context
|
||||
|
||||
`src/components/NymCard.tsx` currently:
|
||||
|
||||
```tsx
|
||||
<Card data-testid={hideHeader ? dataTestid : undefined} ...>
|
||||
{!hideHeader && title !== undefined && (
|
||||
<CardHeader data-testid={dataTestid || (typeof title === 'string' ? title : 'nym-card')} ... />
|
||||
)}
|
||||
...children...
|
||||
</Card>
|
||||
```
|
||||
|
||||
Problems: (1) only reads `dataTestid` (camelCase) — the 17 call sites passing `data-testid` (kebab) are ignored; (2) when a header is shown the id lands on the `CardHeader`, not the content-wrapping root, so it can't scope children; (3) the `|| title` fallback emits misleading ids like `Node 201` / `Current family`.
|
||||
|
||||
Discovered while building `node-families-tauri-webdriver-e2e`: the e2e had to scope around this (using `operator-node-<n>` wrappers and family-id-keyed button ids) because `node-invite-group-<n>` / `invite-card-<n>` never rendered.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:** accept `data-testid`; put the resolved id on the card root (content-wrapping, scope-able); single element only; drop title-derived ids; keep `dataTestid` working.
|
||||
|
||||
**Non-Goals:** changing call sites from `dataTestid` to `data-testid` en masse; any visual/behavioural change; touching `ConfirmActionButton` (its `dataTestid` already renders correctly).
|
||||
|
||||
## Decisions
|
||||
|
||||
**D1 — Resolve `data-testid ?? dataTestid` and place it on the `<Card>` root, always.**
|
||||
Destructure both props (`'data-testid': dataTestidAttr`, `dataTestid`) and compute `const testId = dataTestidAttr ?? dataTestid`. Apply `data-testid={testId}` to the outer `<Card>` unconditionally (header or not), so it wraps content and scoping works. *Alternative considered:* keep it on the header when a header exists — rejected (can't scope children, and is the current broken behaviour).
|
||||
|
||||
**D2 — Remove the `|| title` / `'nym-card'` fallback; render no attribute when unset.**
|
||||
Emit `data-testid` only when `testId` is defined. Verified no test queries by a title-derived id, so this only removes noise. Avoids duplicate ids (root + header) that would break Playwright strict locators.
|
||||
|
||||
**D3 — Revert the e2e scope-arounds where the intended ids now render.**
|
||||
With the fix, `node-invite-group-<n>` and `invite-card-<familyId>` render on real elements. Optionally simplify `e2e/shared/families.ts` back toward the intended ids. Keep the suite green either way — this is cleanup, not required for correctness.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **17 previously-absent `data-testid`s start appearing** → could affect DOM snapshot tests. There are none today (Jest is node-env); mitigation: run the wallet build + the Node Families Playwright suite + `tsc`/eslint after the change.
|
||||
- **A consumer relied on the title-derived id** → verified none do; if one surfaces, it can pass an explicit `data-testid`.
|
||||
- **Type prop name** → React/TS allows `'data-testid'` as a prop key; type it explicitly in `NymCard`'s props so call sites keep type-checking.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Single-component change; additive (new ids appear, none removed except the unused title fallback). No rollback concerns. Verify by: build wallet, run `pnpm test`, `pnpm tsc`, and the Node Families Playwright suite (`pnpm test:e2e`).
|
||||
@@ -0,0 +1,27 @@
|
||||
## Why
|
||||
|
||||
`NymCard`'s test-id prop is `dataTestid` (camelCase), but **17** call sites across the wallet pass `data-testid` (kebab-case), which `NymCard` silently ignores. Those intended test ids (`member-list`, `create-family-form`, `invite-card-<id>`, `operator-node-<id>-family`, `dissolve-family-card`, `family-summary`, `balance-usd-approx`, error/gateway cards, …) never reach the DOM. Worse, when a `data-testid` is dropped `NymCard` stamps the header with the **title text** as a test id (e.g. `Node 201`, `Current family`), producing misleading ids.
|
||||
|
||||
This silently breaks UI test selectors: the Node Families Storybook `play` functions and the first Playwright pass both targeted ids that don't render (discovered during the `node-families-tauri-webdriver-e2e` work, which had to scope around it). Fixing the component restores the intended, consistent test-id contract for the whole app.
|
||||
|
||||
## What Changes
|
||||
|
||||
- `NymCard` SHALL accept a standard `data-testid` prop (in addition to the existing `dataTestid`) and apply the resolved id to the card **root** element, so it wraps the card's content and is usable as a scope container.
|
||||
- Remove the title-as-test-id fallback (`data-testid={dataTestid || title}`) so cards no longer emit misleading ids derived from their title; emit a test id only when one is explicitly provided.
|
||||
- The resolved id MUST appear on exactly one element (no duplicate root/header ids that would break strict locators).
|
||||
- Follow-up cleanup: revert the scope-around workarounds in the Node Families e2e selectors (`e2e/shared/families.ts`) back to the now-rendering intended ids where it simplifies them.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `nymcard-testid-contract`: The `NymCard` component's contract for exposing a DOM test id (accept `data-testid`, single deterministic element, no title-derived ids).
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- None — no existing capability spec covers NymCard. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **Component**: `src/components/NymCard.tsx` (the fix). ~27 `NymCard` call sites; **17** currently-dropped `data-testid`s will start rendering (families, balance, gateway, error cards). No call-site edits required.
|
||||
- **Tests**: `nym-wallet` Jest is node-env (no DOM render), so unit tests are unaffected; verify the wallet builds, `tsc`/eslint stay clean, the Node Families Playwright suite stays green, and (bonus) the Storybook `play` functions now resolve their original ids.
|
||||
- **Risk**: low — no test currently queries by the title-fallback id (verified); the change only adds/relocates ids. Snapshot-style DOM tests (none today) would see new `data-testid` attributes appear.
|
||||
- **Out of scope**: broad re-write of call sites to switch `dataTestid` → `data-testid` (both remain supported); behavioural/visual changes to `NymCard`.
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: NymCard exposes a DOM test id from a standard prop
|
||||
|
||||
`NymCard` SHALL apply a caller-provided test id to its root DOM element. It MUST accept the standard `data-testid` prop, and SHALL continue to accept the legacy `dataTestid` prop; when both are provided, one resolved value is used. The resolved test id MUST appear on exactly one element (the card root), never duplicated across the root and the header.
|
||||
|
||||
#### Scenario: data-testid reaches the DOM
|
||||
|
||||
- **WHEN** a `NymCard` is rendered with `data-testid="member-list"`
|
||||
- **THEN** the rendered card root element has `data-testid="member-list"`
|
||||
- **AND** a scoped query within that element can find the card's content
|
||||
|
||||
#### Scenario: legacy dataTestid still works
|
||||
|
||||
- **WHEN** a `NymCard` is rendered with `dataTestid="foo"` and no `data-testid`
|
||||
- **THEN** the rendered card root element has `data-testid="foo"`
|
||||
|
||||
#### Scenario: no duplicate ids
|
||||
|
||||
- **WHEN** a `NymCard` with a header and a provided test id is rendered
|
||||
- **THEN** exactly one element carries that test id (strict locators match a single node)
|
||||
|
||||
### Requirement: No title-derived test ids
|
||||
|
||||
`NymCard` SHALL NOT derive a test id from its `title` (or emit a `nym-card` fallback). When no test id is provided, the card SHALL render without a `data-testid` attribute.
|
||||
|
||||
#### Scenario: untagged card emits no test id
|
||||
|
||||
- **WHEN** a `NymCard` is rendered with a `title` but no `data-testid`/`dataTestid`
|
||||
- **THEN** neither the card root nor its header carries a `data-testid` derived from the title
|
||||
@@ -0,0 +1,17 @@
|
||||
## 1. Fix NymCard
|
||||
|
||||
- [x] 1.1 In `src/components/NymCard.tsx`, accept a `'data-testid'?: string` prop alongside `dataTestid`; resolve `dataTestidAttr ?? dataTestid`.
|
||||
- [x] 1.2 Apply `data-testid` to the root `<Card>` element unconditionally (header or not); rendered only when defined.
|
||||
- [x] 1.3 Remove the `dataTestid || title`/`'nym-card'` fallback on `CardHeader` so no title-derived id is emitted and the id is not duplicated.
|
||||
|
||||
## 2. Verify
|
||||
|
||||
- [x] 2.1 `pnpm tsc` clean ("No errors found"); eslint clean on `NymCard.tsx`; mock dev build compiles.
|
||||
- [x] 2.2 `pnpm test` (Jest) green — **85/85** (after removing 328 stray compiled `.js` artifacts that were shadowing the `.ts` sources and breaking Jest; unrelated to this fix).
|
||||
- [x] 2.3 Node Families Playwright suite green (3/3). Spot-checked in the mock app: `node-invite-group-201` and `invite-card-2` now render (were dropped before), and the misleading title ids (`Node 201`, `Current family`) are gone.
|
||||
- [ ] 2.4 (Bonus) Confirm the Storybook Node Families `play` functions resolve their original ids — not run (would need a Storybook run); the ids they target now render in the DOM.
|
||||
|
||||
## 3. Cleanup (optional)
|
||||
|
||||
- [ ] 3.1 Simplify `e2e/shared/families.ts` — **skipped intentionally**: the current selectors work and remain correct (invite buttons are family-id keyed via `ConfirmActionButton` regardless of this fix; `operator-node-<n>` is a valid scope). No change needed.
|
||||
- [x] 3.2 Updated the `NymCard` caveat note in `e2e/README.md` to record that the prop is fixed.
|
||||
@@ -35,8 +35,8 @@ This design supersedes an earlier framing that made WebdriverIO + `tauri-driver`
|
||||
**D1 — Playwright against the mock-wired dev server is the PRIMARY suite.**
|
||||
Point Playwright at `http://localhost:9000` with the mock flag on, and replay the journeys against the real app shell + router. Rationale: runs on every OS (incl. the developer's Mac), reuses the already-present `@playwright/test` dep and the existing selectors/specs, and tests the actual app chrome rather than Storybook iframes. *Alternative considered:* keep driving Storybook stories — lower fidelity (no router/app shell) and no closer to the real app. *Alternative considered:* Cypress — no advantage over the Playwright suite already in the repo.
|
||||
|
||||
**D2 — Build-time flag gates mock-vs-real; persona is chosen at runtime *within* the mock build.**
|
||||
A webpack `DefinePlugin` boolean (`WALLET_MOCK_FAMILIES=on|off`, default `off`) gates the import of `MockFamiliesContextProvider` vs `FamiliesContextProvider` behind a `const` check; with the flag off the dead branch and its transitive mock imports tree-shake out, so no mock code ships. The **persona** (`buildOwnerFlowStore` / `buildOperatorFlowStore` + sender) is selected at *runtime* from a URL param (`?persona=owner|operator`, default `owner`) read only by the mock route wrapper. This stays prod-safe — the runtime reader lives inside the already-build-gated mock branch — and means a **single** dev server serves both personas, so Playwright just navigates to different URLs (resolves the persona open question). *Alternatives considered:* tri-state build flag with one persona per build (forces two dev servers / two builds; rejected as heavier and slower); pure runtime URL flag for mock-vs-real (ships mock code in prod unless guarded; rejected); separate entry point (unnecessary — the page is already provider-agnostic).
|
||||
**D2 — Dedicated mock ENTRY (gated by the build flag); persona chosen at runtime within it.**
|
||||
*Revised during apply:* the original "swap the families provider inside the prod `main` entry" cannot work in a browser — `main.tsx` → `AppCommon` → `AppProvider` is Tauri-coupled at bootstrap (imports `tauri-forage`, `@tauri-apps/api/app`, ~9 `requests/*`, fires `invoke` effects on mount) and the main routes are login-gated, so a plain browser never reaches `/family`. Instead, add a **dedicated mock entry** (`src/main.mock.tsx` + generated `main.mock.html`) that mounts the real `HashRouter` + real `ApplicationLayout` + the Family page, but with the existing Storybook mocks (`MockMainContextProvider` for the app bootstrap + `MockFamiliesContextProvider` for families) — no Tauri, no login gate, browser-safe. The webpack flag (`WALLET_MOCK_FAMILIES=on|off`, default `off`, also added to `EnvironmentPlugin`) **gates whether the mock entry + its HTML are built at all**, so production builds never include it (cleaner than tree-shaking a swapped import — the mock code is simply never an entry). The **persona** (`buildOwnerFlowStore` / `buildOperatorFlowStore` + sender) is read at *runtime* from `?persona=owner|operator` (default `owner`) on `window.location.search`, so a **single** dev server serves both personas and Playwright just navigates to different URLs. The real `/family` route (`FamilyPageRoute.tsx` → `FamiliesContextProvider`) and `FamilyPage.tsx` are left untouched, keeping the merged Code Connect mapping valid. *Alternatives considered:* in-place provider swap (rejected — bootstrap can't boot in a browser); thin shell without `ApplicationLayout` (rejected — little fidelity over the Storybook suite being replaced); native-only via Tier 2 (rejected — never exercises the shell on macOS).
|
||||
|
||||
**D3 — Reuse the existing fixtures and selectors verbatim.**
|
||||
The mock-wired dev server seeds the same fixtures as the Storybook flows, and the page already exposes the journey `data-testid`s, so the Playwright journeys mirror `FamilyFlows.stories.tsx` step-for-step. The same selectors then carry over to the optional WebdriverIO leg, keeping all suites observably equivalent.
|
||||
|
||||
@@ -4,7 +4,7 @@ The Node Families feature is fully built and exercised in Storybook, but its end
|
||||
|
||||
## What Changes
|
||||
|
||||
- Mount the existing Family page in the wallet app behind a **build-time mock flag**: when set, the app wires `MockFamiliesContextProvider` (the in-memory contract engine already used by Storybook) instead of the Tauri-backed `FamiliesContextProvider`; the production bundle tree-shakes the mock code out.
|
||||
- Mount the existing Family page via a **dedicated, build-flag-gated mock entry** (`main.mock.tsx` + `main.mock.html`): it renders the real router + layout but with the Storybook mocks (`MockMainContextProvider` + `MockFamiliesContextProvider`), so it runs in a plain browser with no Tauri runtime or login. The real `main` entry and `/family` route are untouched; production builds never include the mock entry. (Revised during apply — the real app bootstrap is Tauri-coupled and can't boot in a browser, so an in-place provider swap was not viable.)
|
||||
- **Primary e2e — Playwright against the dev server (mock-wired):** point Playwright at the running webpack dev server (`http://localhost:9000`, the same `devUrl` Tauri loads) with the mock flag enabled, and replay the same journeys currently covered by the Storybook flow stories. This drives the real app shell + router in a real browser (Chromium/WebKit), is cross-platform (runs locally on macOS), and reuses the existing `@playwright/test` dependency and `data-testid` selectors. It supersedes the current Playwright→Storybook-iframe suite by pointing at the app shell instead.
|
||||
- **Optional validation — WebdriverIO + `tauri-driver` in CI:** as a "how far can we get against the actual binary" leg, add the Tauri WebDriver CI flow (Ubuntu + `xvfb-run`, `webkit2gtk-driver` + `tauri-driver`, WebdriverIO) to drive the **packaged app in the native WebKitGTK webview**. Linux/Windows only (macOS has no WKWebView driver), so it lives in CI, not local dev, and starts non-blocking.
|
||||
- **Optional higher-fidelity tier — sandbox real-IPC smoke:** the node-families contract is now deployed to **sandbox** (currently one family, one member). This unlocks a read-only smoke against the real `FamiliesContextProvider` + `src/requests/families.ts` wiring (parent-change task 9.4), separate from the deterministic mock e2e.
|
||||
|
||||
+8
-8
@@ -1,20 +1,20 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Build-time mock provider selection
|
||||
### Requirement: Dedicated mock entry, gated by a build flag
|
||||
|
||||
The wallet app SHALL select the Node Families context provider at build time based on a single mock flag. When the flag is enabled, the Family page SHALL be backed by `MockFamiliesContextProvider`; otherwise it SHALL be backed by the Tauri IPC-backed `FamiliesContextProvider`. The selection MUST be resolved by a compile-time constant so that, when the flag is disabled, mock modules (`src/context/mocks/**` for families) are eliminated from the bundle by tree-shaking and never reach a production build.
|
||||
The wallet app SHALL provide a dedicated mock entry (a separate webpack entry + generated HTML) that mounts the Family page with the mock app bootstrap (`MockMainContextProvider`) and mock families provider (`MockFamiliesContextProvider`), requiring no Tauri runtime and no login. A build flag (`WALLET_MOCK_FAMILIES`, default off) SHALL gate whether the mock entry and its HTML are built at all, so a production build never includes the mock entry or any families mock-engine code. The real `/family` route and `FamilyPage` component SHALL be left unchanged so the page stays backed by `FamiliesContextProvider` in production and remains importable in isolation.
|
||||
|
||||
#### Scenario: Mock build wires the mock provider
|
||||
#### Scenario: Mock build wires the mock providers
|
||||
|
||||
- **WHEN** the app is built with the families mock flag enabled
|
||||
- **THEN** the Family route renders `FamilyPage` wrapped in `MockFamiliesContextProvider`
|
||||
- **AND** the page resolves family reads from the in-memory mock engine without any Tauri IPC call
|
||||
- **THEN** the mock entry renders `FamilyPage` wrapped in `MockFamiliesContextProvider` (and the app shell in `MockMainContextProvider`)
|
||||
- **AND** the page resolves family reads from the in-memory mock engine without any Tauri IPC call or login
|
||||
|
||||
#### Scenario: Production build excludes mock code
|
||||
#### Scenario: Production build excludes the mock entry
|
||||
|
||||
- **WHEN** the app is built with the families mock flag disabled (default)
|
||||
- **THEN** the Family route renders `FamilyPage` wrapped in the real `FamiliesContextProvider`
|
||||
- **AND** the produced bundle contains no families mock-engine code
|
||||
- **THEN** no mock entry or `main.mock.html` is produced and the bundle contains no families mock-engine code
|
||||
- **AND** the real `/family` route still renders `FamilyPage` wrapped in `FamiliesContextProvider`
|
||||
|
||||
### Requirement: Deterministic seeded fixtures in the mock build
|
||||
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
## 1. Build-time mock provider seam
|
||||
## 1. Dedicated mock entry (D2)
|
||||
|
||||
- [ ] 1.1 Add a webpack `DefinePlugin` boolean for the families mock gate (`WALLET_MOCK_FAMILIES=on|off`, default `off`) in the dev/mock webpack config (mock-vs-real only; persona is runtime, see 1.3).
|
||||
- [ ] 1.2 Introduce a provider-selection module that exports either `FamiliesContextProvider` (real) or `MockFamiliesContextProvider` (mock) based on the compile-time flag, behind a `const` guard so the unused branch tree-shakes. Keep this seam in its **own** module — do NOT make `FamilyPage.tsx` depend on the flag or on Tauri, so the merged `FamilyPage.figma.tsx` Code Connect mapping (`example: () => <FamilyPage />`) still imports it in isolation.
|
||||
- [ ] 1.3 Have the Family route/entry consume the selection module; in mock mode, read the persona at runtime from a `?persona=owner|operator` URL param (default `owner`) and seed `buildOwnerFlowStore` / `buildOperatorFlowStore` accordingly (reuse `families.fixtures.ts`). The persona reader lives only inside the build-gated mock branch.
|
||||
- [ ] 1.4 Ensure the Family page is reachable via normal in-app navigation on the dev server (`:9000`) and renders inside the app shell + router (not a Storybook iframe), keeping the existing `data-testid`s.
|
||||
- [ ] 1.5 Verify a default (flag `off`) production build excludes families mock-engine code (inspect bundle / add a guard); confirm the real provider still wires unchanged.
|
||||
- [ ] 1.6 Add an npm script to launch the single mock-wired dev server (e.g. `WALLET_MOCK_FAMILIES=on pnpm webpack:dev`); both personas are reached on the one server via `?persona=`.
|
||||
- [x] 1.1 Add `WALLET_MOCK_FAMILIES` (default `off`) to webpack `EnvironmentPlugin`, and conditionally register a `mainMock` entry + a `main.mock.html` HtmlWebpackPlugin output **only when the flag is `on`**, so production builds never include the mock entry.
|
||||
- [x] 1.2 Create `src/main.mock.tsx`: mount real `HashRouter` + real `ApplicationLayout` + a `/family` route, wrapped in `MockMainContextProvider` (mock app bootstrap) + `MockFamiliesContextProvider` (mock families) + `QueryClientProvider` + `NymWalletTheme` — no Tauri, no login gate. Leave `FamilyPage.tsx` and the real `FamilyPageRoute.tsx` untouched (keeps Code Connect valid).
|
||||
- [x] 1.3 In `main.mock.tsx`, read the persona at runtime from `?persona=owner|operator` (default `owner`) on `window.location.search` and seed `buildOwnerFlowStore` / `buildOperatorFlowStore` + the matching sender (reuse `families.fixtures.ts`); seed the hash to `#/family`. (Added a third `operator-seeded` persona for the multi-node assertion.)
|
||||
- [x] 1.4 Confirm the Family page is reachable at `/main.mock.html?persona=...#/family` on the dev server (`:9000`), rendering inside the real layout/router (not a Storybook iframe), keeping the existing `data-testid`s. (Layout chrome `Nav`/`AppBar` verified Tauri-free; full browser render pending a live run — see 6.1.)
|
||||
- [x] 1.5 Confirm a default (flag `off`) production build is unchanged — no `mainMock` entry, no `main.mock.html`, no mock-engine code; the real `/family` route still wires `FamiliesContextProvider`. (Verified: config builds entry `auth,main,log` with flag off; `+mainMock` only with flag on.)
|
||||
- [x] 1.6 Add an npm script to launch the single mock-wired dev server (`webpack:dev:mock` = `WALLET_MOCK_FAMILIES=on webpack serve --config webpack.dev.js`); both personas are reached on the one server via `?persona=`.
|
||||
|
||||
## 2. Primary e2e — Playwright against the mock-wired dev server
|
||||
|
||||
- [ ] 2.1 Repoint `playwright.config.ts` from Storybook (:6006) to the mock-wired dev server (`baseURL http://localhost:9000`), with a single `webServer` (`WALLET_MOCK_FAMILIES=on`) and `reuseExistingServer` locally; tests pick persona via the `?persona=` URL.
|
||||
- [ ] 2.2 Port the owner lifecycle journey (create → invite → accept → kick → disband) against `/family?persona=owner`, reusing the existing `data-testid` selectors and the Storybook flow steps.
|
||||
- [ ] 2.3 Port the operator lifecycle journey against `/family?persona=operator` (accept → leave, then reject), asserting the reject-node invite group ends empty.
|
||||
- [ ] 2.4 Port the multi-node operator invite-states assertion (`node-invite-group-201` present, `node-invite-group-203-empty`).
|
||||
- [ ] 2.5 Handle portalled confirmation dialogs via their global test ids (mirror the Storybook `screen`-scoped queries).
|
||||
- [ ] 2.6 Retire the old Storybook-iframe specs (D10): replace `e2e/families.spec.ts` with the dev-server journeys and factor shared selector/step constants for parity (Storybook `play` functions stay as Storybook-level coverage).
|
||||
- [ ] 2.7 Confirm the suite runs green locally on macOS (Chromium; optionally WebKit project).
|
||||
- [x] 2.1 Repoint `playwright.config.ts` from Storybook (:6006) to the mock-wired dev server (`baseURL http://localhost:9000`), with a single `webServer` (`webpack:dev:mock`) and `reuseExistingServer` locally; tests pick persona via the `?persona=` URL.
|
||||
- [x] 2.2 Port the owner lifecycle journey (create → invite → accept → kick → disband) against `main.mock.html?persona=owner`, reusing the existing `data-testid` selectors and the Storybook flow steps.
|
||||
- [x] 2.3 Port the operator lifecycle journey against `?persona=operator` (accept → leave, then reject), asserting the reject-node invite group ends empty.
|
||||
- [x] 2.4 Port the multi-node operator invite-states assertion (`node-invite-group-201` present, `node-invite-group-203-empty`) against `?persona=operator-seeded`.
|
||||
- [x] 2.5 Handle portalled confirmation dialogs via their global test ids (page-scoped, mirroring the Storybook `screen` queries).
|
||||
- [x] 2.6 Retire the old Storybook-iframe specs (D10): replaced `e2e/families.spec.ts` with the dev-server journeys and factored shared selectors into `e2e/shared/families.ts` for parity (Storybook `play` functions stay as Storybook-level coverage).
|
||||
- [x] 2.7 Confirm the suite runs green locally on macOS — **DONE: 3/3 passing** (clean cold run, Chromium). Required selector corrections: the original Storybook-derived ids on `NymCard` don't render (its prop is `dataTestid`, not `data-testid`), so journeys scope by the `operator-node-<n>` wrapper and key invite buttons by family id.
|
||||
|
||||
## 3. CI — wire the primary suite in
|
||||
|
||||
- [ ] 3.1 Add a Playwright e2e step to the existing `build` job in `.github/workflows/ci-nym-wallet-frontend.yml` (it is not in CI today — only unit tests + `build-storybook` run); install browsers (`npx playwright install --with-deps chromium`) and run `test:e2e`.
|
||||
- [ ] 3.2 Ensure the step launches the mock-wired dev server (per persona) and fails the job on any owner/operator journey failure.
|
||||
- [x] 3.1 Add a Playwright e2e step to the existing `build` job in `.github/workflows/ci-nym-wallet-frontend.yml` (install `--with-deps chromium` + run `test:e2e`).
|
||||
- [x] 3.2 The step launches the mock-wired dev server via Playwright's `webServer` and fails the job on any journey failure (no `continue-on-error`).
|
||||
|
||||
## 4. Optional — native-webview validation leg (WebdriverIO + tauri-driver)
|
||||
|
||||
- [ ] 4.1 Add `webdriverio` (+ runner) to `nym-wallet` dev deps; document `cargo install tauri-driver --locked`.
|
||||
- [ ] 4.2 Create the WebdriverIO config that starts `tauri-driver`, points `tauri:options.application` at the mock-wired binary, and sets CI step timeouts; add a `test:e2e:tauri` script.
|
||||
- [ ] 4.3 Implement the skip-not-fail guard: detect macOS / missing `tauri-driver` / missing `webkit2gtk-driver` and skip with a clear message (design D5).
|
||||
- [ ] 4.4 Reuse the journey steps/selectors from §2 (shared constants) so the native leg asserts identical outcomes.
|
||||
- [ ] 4.5 Add a **separate** CI job (ubuntu-22.04) following the Tauri WebDriver-in-CI flow: install `libwebkit2gtk-4.1-dev` + `webkit2gtk-driver` + `xvfb`, set up Rust + cache, `cargo install tauri-driver --locked`, build the mock-wired binary, run the suite under `xvfb-run`. Start it `continue-on-error` until stable.
|
||||
- [x] 4.1 Add `webdriverio` + `@wdio/{cli,local-runner,mocha-framework,spec-reporter}` + `tsx` to dev deps; documented `cargo install tauri-driver --locked` (README + CI).
|
||||
- [x] 4.2 Create `wdio.conf.ts` (starts `tauri-driver`, `tauri:options.application` → built binary, mocha timeouts) + `test:e2e:tauri` script. **TODO in-file:** point the mock binary's window at `main.mock.html?persona=…` + confirm the release binary path (native-leg wiring).
|
||||
- [x] 4.3 Implement the skip-not-fail guard (`e2e-tauri/run.mjs`): macOS / missing `tauri-driver` / missing `webkit2gtk-driver` → exit 0 with a clear message (design D5).
|
||||
- [x] 4.4 Reuse the journey selectors from §2 via `e2e/shared/families.ts` (`e2e-tauri/families.tauri.ts`), so the native leg asserts identical outcomes.
|
||||
- [x] 4.5 Add a **separate** `e2e-tauri` CI job (ubuntu-22.04, `continue-on-error`) following the Tauri WebDriver-in-CI flow: `libwebkit2gtk-4.1-dev` + `webkit2gtk-driver` + `xvfb`, Rust + cache, `cargo install tauri-driver --locked`, build mock binary, run under `xvfb-run`.
|
||||
|
||||
## 5. Optional — sandbox real-IPC read smoke (manual first, D9)
|
||||
|
||||
- [ ] 5.1 Document a manual read-only smoke: connect the app to a sandbox account, open the Family page, and confirm it renders the known sandbox family/member via the real `FamiliesContextProvider` + `requests/families.ts` (no state-changing transaction).
|
||||
- [ ] 5.2 Pin the known sandbox family id and assert render/shape rather than exact contents, so a contract redeploy doesn't hard-fail; keep it separate from and non-blocking relative to the mock suites.
|
||||
- [ ] 5.3 (Follow-up, not a blocker) If/when a sandbox test account can be provisioned headlessly (mnemonic in CI secrets), promote the smoke to a non-blocking CI job.
|
||||
- [x] 5.1 Documented (e2e/README.md) a manual read-only smoke: connect to a sandbox account, open the Family page, confirm it renders the known sandbox family/member via real `FamiliesContextProvider` + `requests/families.ts` (no state-changing tx).
|
||||
- [x] 5.2 Documented: pin the known sandbox family id and assert render/shape, not exact contents; kept separate + non-blocking vs the mock suites.
|
||||
- [x] 5.3 Documented the follow-up: promote to a non-blocking CI job once a sandbox test account can be provisioned headlessly.
|
||||
|
||||
## 6. Verification & docs
|
||||
|
||||
- [ ] 6.1 Run the primary Playwright suite in CI and locally (macOS) — confirm both owner and operator journeys pass against the app shell.
|
||||
- [ ] 6.2 If implemented, confirm the native leg passes in Linux CI and skips cleanly on macOS.
|
||||
- [ ] 6.3 Confirm `tsc` + eslint stay clean and the production build is unaffected (no mock code, no behavior change).
|
||||
- [ ] 6.4 Confirm the provider seam didn't break Code Connect (`FamilyPage.figma.tsx` still type-checks) and the Nym 2.0 theme swap left all journey `data-testid`s intact.
|
||||
- [ ] 6.5 Document the tiered setup (primary Playwright→dev-server; optional WebdriverIO→tauri-driver CI; optional sandbox read smoke) and the mock-flag usage in the wallet README / e2e comments.
|
||||
- [x] 6.1 Run the primary Playwright suite locally (macOS) — **DONE: 3/3 green** against the mock-wired app shell. (CI execution still pending the first push.) Fixes needed to get a clean browser render: skip React Refresh/HMR + the dev-server live-reload client in the mock build (`webpack.dev.js`, avoids missing `core-js-pure`/`ansi-html-community`); add the relative `node_modules` walk for the mock build (pnpm strict + absolute `resolve.modules` dropped `object-assign`); make `src/utils/common.ts` resolve `getCurrentWebviewWindow()` lazily (was crashing at import outside Tauri).
|
||||
- [ ] 6.2 Confirm the native leg passes in Linux CI and skips cleanly on macOS — **pending** the native-leg wiring (4.2 TODO) + a Linux CI run.
|
||||
- [x] 6.3 Confirm `tsc` + eslint stay clean and the production build is unaffected — verified: after `pnpm install`, `tsc` is fully clean (exit 0); `main.mock.tsx` + `utils/common.ts` lint clean; webpack prod-safe (no `mainMock` entry with flag off).
|
||||
- [x] 6.4 Confirm the provider seam didn't break Code Connect (seam is a separate entry; `FamilyPage.tsx` + `FamilyPageRoute.tsx` untouched; `FamilyPage.figma.tsx` now type-checks once `@figma/code-connect` is installed) and the Nym 2.0 theme swap left journey `data-testid`s intact (color-only).
|
||||
- [x] 6.5 Document the tiered setup + mock-flag usage (`e2e/README.md`).
|
||||
|
||||
@@ -19,14 +19,17 @@
|
||||
"tauri:build:adhoc": "APPLE_SIGNING_IDENTITY=- tauri build -b app",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:buildx86": "tauri build --target x86_64-apple-darwin",
|
||||
"tauri:build:mock": "WALLET_MOCK_FAMILIES=on run-s webpack:prod tauri:build:no-sign",
|
||||
"test": "jest --config jest.config.cjs",
|
||||
"tsc": "tsc --noEmit true",
|
||||
"tsc:watch": "tsc --noEmit true --watch",
|
||||
"webpack:dev": "webpack serve --config webpack.dev.js",
|
||||
"webpack:dev:mock": "WALLET_MOCK_FAMILIES=on webpack serve --config webpack.dev.js",
|
||||
"webpack:prod": "webpack --progress --config webpack.prod.js",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:tauri": "node e2e-tauri/run.mjs",
|
||||
"test:storybook": "test-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -156,6 +159,12 @@
|
||||
"@svgr/webpack": "catalog:",
|
||||
"@tauri-apps/cli": "catalog:",
|
||||
"@testing-library/dom": "catalog:",
|
||||
"@wdio/cli": "^9.12.0",
|
||||
"@wdio/local-runner": "^9.12.0",
|
||||
"@wdio/mocha-framework": "^9.12.0",
|
||||
"@wdio/spec-reporter": "^9.12.0",
|
||||
"webdriverio": "^9.12.0",
|
||||
"tsx": "^4.19.2",
|
||||
"@testing-library/jest-dom": "catalog:",
|
||||
"@testing-library/react": "catalog:",
|
||||
"@types/big.js": "catalog:",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Playwright e2e config (tasks.md §8.3). The production app is Tauri, so e2e runs
|
||||
* against the Storybook flow stories served as a real browser session (design D8).
|
||||
* Primary e2e config (design D1/D10). Drives the Family page inside the real app shell +
|
||||
* router via the mock-wired dev server (`main.mock.html`, design D2) — a real browser
|
||||
* session with no Tauri runtime or chain. Cross-platform, runs locally on macOS.
|
||||
*
|
||||
* Requires `pnpm install` (adds @playwright/test) + `npx playwright install chromium`.
|
||||
* The webServer serves Storybook on :6006 (reused if already running).
|
||||
* Requires the workspace packages to be built (`pnpm --dir .. run build`) plus
|
||||
* `npx playwright install chromium`. The webServer launches `webpack:dev:mock` on :9000
|
||||
* with `WALLET_MOCK_FAMILIES=on` (reused if already running locally).
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
@@ -15,14 +17,15 @@ export default defineConfig({
|
||||
reporter: 'list',
|
||||
timeout: 60_000,
|
||||
use: {
|
||||
baseURL: 'http://localhost:6006',
|
||||
baseURL: 'http://localhost:9000',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
webServer: {
|
||||
command: 'npm run storybook -- --ci -p 6006',
|
||||
url: 'http://localhost:6006',
|
||||
command: 'npm run webpack:dev:mock',
|
||||
// The mock entry's generated page — only exists when WALLET_MOCK_FAMILIES=on.
|
||||
url: 'http://localhost:9000/main.mock.html',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 180_000,
|
||||
timeout: 300_000,
|
||||
},
|
||||
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||
});
|
||||
|
||||
@@ -19,14 +19,30 @@ export const NymCard: FCWithChildren<{
|
||||
borderless?: boolean;
|
||||
/** Omit the card header row (use for fully custom headers inside children). */
|
||||
hideHeader?: boolean;
|
||||
/** Preferred: standard DOM test id. `dataTestid` (camelCase) is kept for back-compat. */
|
||||
'data-testid'?: string;
|
||||
dataTestid?: string;
|
||||
sx?: SxProps;
|
||||
sxTitle?: SxProps;
|
||||
children?: React.ReactNode;
|
||||
}> = ({ title, subheader, Action, Icon, noPadding, borderless, hideHeader, children, dataTestid, sx, sxTitle }) => (
|
||||
}> = ({
|
||||
title,
|
||||
subheader,
|
||||
Action,
|
||||
Icon,
|
||||
noPadding,
|
||||
borderless,
|
||||
hideHeader,
|
||||
children,
|
||||
dataTestid,
|
||||
'data-testid': dataTestidAttr,
|
||||
sx,
|
||||
sxTitle,
|
||||
}) => (
|
||||
<Card
|
||||
variant="outlined"
|
||||
data-testid={hideHeader ? dataTestid : undefined}
|
||||
// Resolved id on the root element (wraps content, so it can scope children).
|
||||
data-testid={dataTestidAttr ?? dataTestid}
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
borderRadius: 4,
|
||||
@@ -49,7 +65,6 @@ export const NymCard: FCWithChildren<{
|
||||
}}
|
||||
title={<Title title={title} Icon={Icon} sx={sxTitle} />}
|
||||
subheader={subheader}
|
||||
data-testid={dataTestid || (typeof title === 'string' ? title : 'nym-card')}
|
||||
subheaderTypographyProps={{ variant: 'subtitle1' }}
|
||||
action={Action}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { HashRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import { ErrorFallback } from './components';
|
||||
import { ApplicationLayout } from './layouts';
|
||||
import { NymWalletTheme } from './theme';
|
||||
import { FamilyPage } from './pages/families/FamilyPage';
|
||||
import { MockMainContextProvider } from './context/mocks/main';
|
||||
import { MockFamiliesContextProvider } from './context/mocks/families';
|
||||
import {
|
||||
buildOperatorFlowStore,
|
||||
buildOwnerFlowStore,
|
||||
buildSeededStore,
|
||||
MOCK_OPERATOR_ADDRESS,
|
||||
MOCK_OWNER_ADDRESS,
|
||||
} from './context/mocks/families.fixtures';
|
||||
import type { MockStore } from './context/mocks/familiesMockState';
|
||||
|
||||
/**
|
||||
* Mock-wired entry for e2e (design D2). Mounts the real router + layout + Family page
|
||||
* but with the Storybook mocks ({@link MockMainContextProvider} for the app bootstrap,
|
||||
* {@link MockFamiliesContextProvider} for families) so it runs in a plain browser with
|
||||
* NO Tauri runtime and NO login gate. Built only when `WALLET_MOCK_FAMILIES=on`; the
|
||||
* production `main` entry and the real `/family` route are untouched.
|
||||
*/
|
||||
|
||||
// Persona is chosen at runtime from `?persona=...` (default `owner`), read off
|
||||
// `window.location.search` so it survives HashRouter (which owns the `#` fragment).
|
||||
// Each persona maps to one of the deterministic Storybook fixture stores + its sender.
|
||||
const PERSONAS: Record<string, { makeStore: () => MockStore; sender: string }> = {
|
||||
owner: { makeStore: buildOwnerFlowStore, sender: MOCK_OWNER_ADDRESS }, // owner lifecycle
|
||||
operator: { makeStore: buildOperatorFlowStore, sender: MOCK_OPERATOR_ADDRESS }, // operator lifecycle
|
||||
'operator-seeded': { makeStore: buildSeededStore, sender: MOCK_OPERATOR_ADDRESS }, // multi-node invite states
|
||||
};
|
||||
const personaKey = new URLSearchParams(window.location.search).get('persona') ?? 'owner';
|
||||
const { makeStore, sender } = PERSONAS[personaKey] ?? PERSONAS.owner;
|
||||
const store = makeStore();
|
||||
|
||||
// HashRouter starts at `#/`; seed it to the Family page so a bare URL lands there.
|
||||
if (!window.location.hash || window.location.hash === '#' || window.location.hash === '#/') {
|
||||
const { pathname, search } = window.location;
|
||||
window.history.replaceState(window.history.state, '', `${pathname}${search}#/family`);
|
||||
}
|
||||
|
||||
// Deterministic client for e2e: no retries, no cache carry-over between runs.
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
|
||||
|
||||
const MockApp = () => (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<HashRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SnackbarProvider anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}>
|
||||
<MockMainContextProvider>
|
||||
<NymWalletTheme>
|
||||
<ApplicationLayout>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/family" />} />
|
||||
<Route
|
||||
path="/family"
|
||||
element={
|
||||
<MockFamiliesContextProvider store={store} sender={sender} latencyMs={0}>
|
||||
<FamilyPage />
|
||||
</MockFamiliesContextProvider>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</ApplicationLayout>
|
||||
</NymWalletTheme>
|
||||
</MockMainContextProvider>
|
||||
</SnackbarProvider>
|
||||
</QueryClientProvider>
|
||||
</HashRouter>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
const elem = document.getElementById('root');
|
||||
if (elem) {
|
||||
createRoot(elem).render(<MockApp />);
|
||||
}
|
||||
@@ -15,8 +15,6 @@ import {
|
||||
} from '../requests';
|
||||
import { Console } from './console';
|
||||
|
||||
const appWindow = getCurrentWebviewWindow();
|
||||
|
||||
export const validateKey = (key: string, bytesLength: number): boolean => {
|
||||
// it must be a valid base58 key
|
||||
try {
|
||||
@@ -118,7 +116,9 @@ export const splice = (size: number, address?: string): string => {
|
||||
};
|
||||
|
||||
export const maximizeWindow = async () => {
|
||||
await appWindow.maximize();
|
||||
// Resolved lazily (not at module load) so this module is import-safe outside Tauri
|
||||
// (e.g. the mock-wired e2e build running in a plain browser).
|
||||
await getCurrentWebviewWindow().maximize();
|
||||
};
|
||||
|
||||
export function removeObjectDuplicates<T extends object, K extends keyof T>(arr: T[], id: K) {
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"webpack.common.js",
|
||||
"target",
|
||||
"e2e",
|
||||
"playwright.config.ts"
|
||||
"e2e-tauri",
|
||||
"playwright.config.ts",
|
||||
"wdio.conf.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { spawn, ChildProcess } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
|
||||
/**
|
||||
* Optional native-webview e2e config (design D4): drives the packaged Tauri binary through
|
||||
* tauri-driver + WebdriverIO. Launched via `e2e-tauri/run.mjs` (skip-not-fail on macOS).
|
||||
*
|
||||
* TODO (native-leg wiring — the remaining work to make this runnable):
|
||||
* 1. `application` below must point at the MOCK-WIRED binary (built with
|
||||
* WALLET_MOCK_FAMILIES=on, `tauri:build:mock`). Confirm the release binary path/name.
|
||||
* 2. The mock binary's window must open `main.mock.html?persona=...` (Tauri window URL),
|
||||
* so the journeys land on the Family page per persona. Until then this is a scaffold.
|
||||
*/
|
||||
|
||||
let tauriDriver: ChildProcess | undefined;
|
||||
|
||||
// Linux release output; mainBinaryName is "NymWallet" (see tauri.conf.json). Adjust if the
|
||||
// mock build emits a distinct artifact.
|
||||
const APPLICATION = path.resolve(__dirname, 'src-tauri/target/release/NymWallet');
|
||||
|
||||
export const config: WebdriverIO.Config = {
|
||||
runner: 'local',
|
||||
specs: ['./e2e-tauri/**/*.tauri.ts'],
|
||||
maxInstances: 1,
|
||||
capabilities: [
|
||||
{
|
||||
// tauri-driver reads this and attaches to the native webview.
|
||||
// @ts-expect-error tauri:options is a tauri-driver extension, not in the base WdIO types.
|
||||
'tauri:options': { application: APPLICATION },
|
||||
},
|
||||
],
|
||||
framework: 'mocha',
|
||||
mochaOpts: { ui: 'bdd', timeout: 120_000 },
|
||||
reporters: ['spec'],
|
||||
logLevel: 'warn',
|
||||
// tauri-driver listens on 4444 by default.
|
||||
hostname: '127.0.0.1',
|
||||
port: 4444,
|
||||
|
||||
onPrepare: () => {
|
||||
tauriDriver = spawn('tauri-driver', [], { stdio: [null, process.stdout, process.stderr] });
|
||||
},
|
||||
onComplete: () => {
|
||||
tauriDriver?.kill();
|
||||
},
|
||||
};
|
||||
@@ -14,12 +14,31 @@ const muiSystemDir = path.dirname(
|
||||
);
|
||||
const muiStyledEngineV5 = path.dirname(require.resolve('@mui/styled-engine/package.json', { paths: [muiSystemDir] }));
|
||||
|
||||
// Mock-wired build for e2e (design D2): gated by `WALLET_MOCK_FAMILIES=on`. When off (the
|
||||
// default, and always in production) the mock entry + its HTML are never registered.
|
||||
const MOCK_FAMILIES = process.env.WALLET_MOCK_FAMILIES === 'on';
|
||||
|
||||
const entry = {
|
||||
auth: path.resolve(__dirname, 'src/auth.tsx'), // JS bundle for sign up/sign in
|
||||
main: path.resolve(__dirname, 'src/main.tsx'), // JS bundle for main app
|
||||
log: path.resolve(__dirname, 'src/log.tsx'), // JS bundle for logging window
|
||||
...(MOCK_FAMILIES ? { mainMock: path.resolve(__dirname, 'src/main.mock.tsx') } : {}), // mock-wired app (e2e only)
|
||||
};
|
||||
|
||||
const htmlPages = [
|
||||
{ filename: 'index.html', chunks: ['auth'], template: path.resolve(__dirname, 'public/index.html') }, // the starting point is index.html (sign up/sign in)
|
||||
{ filename: 'main.html', chunks: ['main'], template: path.resolve(__dirname, 'public/index.html') }, // main app (loaded after sign in in a new window)
|
||||
{ filename: 'log.html', chunks: ['log'], template: path.resolve(__dirname, 'public/log.html') }, // the user can open a separate logging window
|
||||
];
|
||||
if (MOCK_FAMILIES) {
|
||||
// Served at /main.mock.html on the dev server; the e2e suite navigates here.
|
||||
htmlPages.push({
|
||||
filename: 'main.mock.html',
|
||||
chunks: ['mainMock'],
|
||||
template: path.resolve(__dirname, 'public/index.html'),
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = mergeWithRules({
|
||||
module: {
|
||||
rules: {
|
||||
@@ -28,11 +47,7 @@ module.exports = mergeWithRules({
|
||||
},
|
||||
},
|
||||
})(
|
||||
webpackCommon(__dirname, [
|
||||
{ filename: 'index.html', chunks: ['auth'], template: path.resolve(__dirname, 'public/index.html') }, // the starting point is index.html (sign up/sign in)
|
||||
{ filename: 'main.html', chunks: ['main'], template: path.resolve(__dirname, 'public/index.html') }, // main app (loaded after sign in in a new window)
|
||||
{ filename: 'log.html', chunks: ['log'], template: path.resolve(__dirname, 'public/log.html') }, // the user can open a separate logging window
|
||||
]),
|
||||
webpackCommon(__dirname, htmlPages),
|
||||
{
|
||||
entry,
|
||||
resolve: {
|
||||
@@ -71,6 +86,7 @@ module.exports = mergeWithRules({
|
||||
plugins: [
|
||||
new webpack.EnvironmentPlugin({
|
||||
NYM_WALLET_INTERNAL_DOCS: '',
|
||||
WALLET_MOCK_FAMILIES: 'off',
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
+24
-10
@@ -4,6 +4,11 @@ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'
|
||||
const ReactRefreshTypeScript = require('react-refresh-typescript');
|
||||
const commonConfig = require('./webpack.common');
|
||||
|
||||
// The mock-wired e2e build (WALLET_MOCK_FAMILIES=on) is driven by Playwright, which reloads
|
||||
// pages itself — it doesn't need (or want) React Fast Refresh / HMR. Skipping it also avoids
|
||||
// the HMR client's `core-js-pure` polyfill requirement. See e2e/README.md.
|
||||
const MOCK_FAMILIES = process.env.WALLET_MOCK_FAMILIES === 'on';
|
||||
|
||||
module.exports = mergeWithRules({
|
||||
module: {
|
||||
rules: {
|
||||
@@ -14,6 +19,11 @@ module.exports = mergeWithRules({
|
||||
})(commonConfig, {
|
||||
mode: 'development',
|
||||
devtool: 'inline-source-map',
|
||||
// The mock e2e build runs in development mode, where some deps (e.g. prop-types) use a
|
||||
// `require('object-assign')`-style transitive. webpack.common sets `resolve.modules` to
|
||||
// absolute dirs only, which disables the normal per-module node_modules walk and breaks
|
||||
// pnpm's nested symlinks. Re-add the relative walk so those resolve. (Merged/appended.)
|
||||
...(MOCK_FAMILIES ? { resolve: { modules: ['node_modules'] } } : {}),
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -22,7 +32,7 @@ module.exports = mergeWithRules({
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
getCustomTransformers: () => ({
|
||||
before: [ReactRefreshTypeScript()],
|
||||
before: MOCK_FAMILIES ? [] : [ReactRefreshTypeScript()],
|
||||
}),
|
||||
// `ts-loader` does not work with HMR unless `transpileOnly` is used.
|
||||
// If you need type checking, `ForkTsCheckerWebpackPlugin` is an alternative.
|
||||
@@ -31,12 +41,14 @@ module.exports = mergeWithRules({
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new ReactRefreshWebpackPlugin(),
|
||||
plugins: MOCK_FAMILIES
|
||||
? []
|
||||
: [
|
||||
new ReactRefreshWebpackPlugin(),
|
||||
|
||||
// this can be included automatically by the dev server, however build mode fails if missing
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
],
|
||||
// this can be included automatically by the dev server, however build mode fails if missing
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
],
|
||||
|
||||
// recommended for faster rebuild
|
||||
optimization: {
|
||||
@@ -58,9 +70,11 @@ module.exports = mergeWithRules({
|
||||
port: 9000,
|
||||
compress: true,
|
||||
historyApiFallback: true,
|
||||
hot: true,
|
||||
client: {
|
||||
overlay: false,
|
||||
},
|
||||
// Mock e2e: serve a static bundle only — no HMR, no live-reload client injection
|
||||
// (Playwright drives reloads). This also avoids the dev-server client's transitive deps.
|
||||
hot: !MOCK_FAMILIES,
|
||||
liveReload: !MOCK_FAMILIES,
|
||||
webSocketServer: MOCK_FAMILIES ? false : undefined,
|
||||
client: MOCK_FAMILIES ? false : { overlay: false },
|
||||
},
|
||||
});
|
||||
|
||||
Generated
+4315
-264
File diff suppressed because it is too large
Load Diff
@@ -20,9 +20,11 @@ allowBuilds:
|
||||
'@swc/core': true
|
||||
core-js: true
|
||||
core-js-pure: true
|
||||
edgedriver: false
|
||||
es5-ext: true
|
||||
esbuild: true
|
||||
fsevents: true
|
||||
geckodriver: false
|
||||
lefthook: true
|
||||
lmdb: true
|
||||
msgpackr-extract: true
|
||||
|
||||
Reference in New Issue
Block a user