NYM-1199: Add openspec for Node Families native webview E2E testing

This openspec outlines the design, proposal, requirements, and tasks to implement end-to-end tests for the Node Families feature within the actual Tauri desktop shell. It aims to verify the UI and user journeys against the native webview, complementing existing Storybook-based E2E tests.

The plan involves using WebdriverIO and `tauri-driver` to drive a mock-wired Tauri build, ensuring mock code is tree-shaken from production. These tests will run in Linux CI, providing confidence that the feature behaves as expected in its native environment.
```
This commit is contained in:
Yana Matrosova
2026-06-05 16:43:15 +03:00
parent 5650bfbd4e
commit 7d72526405
6 changed files with 235 additions and 0 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-05
@@ -0,0 +1,64 @@
## Context
The Node Families feature (parent change `node-families-wallet`) is complete and verified in Storybook. The page component is already cleanly decoupled from Tauri:
- `src/pages/families/FamilyPage.tsx` — pure page, consumes `useFamiliesContext()`, no Tauri imports.
- `src/context/FamiliesContextProvider.tsx` — the **real** provider; the only families file importing `./main` (Tauri runtime).
- `src/context/mocks/families.tsx` (`MockFamiliesContextProvider`) — drives the page from an in-memory contract engine (`familiesMockState.ts`) seeded by `families.fixtures.ts`.
- `src/pages/families/FamilyPageRoute.tsx` (`FamilyPageWithProvider`) — wraps `FamilyPage` in the real provider for the live app route.
- Storybook flow stories (`FamilyFlows.stories.tsx`) and `e2e/families.spec.ts` (Playwright → Storybook on :6006) already encode the journeys via `data-testid`s.
Constraints established up front:
- **Playwright cannot drive Tauri's WebDriver.** `tauri-driver` exposes the classic W3C WebDriver protocol; Playwright speaks CDP/BiDi. The Tauri-documented clients are Selenium and WebdriverIO.
- **`tauri-driver` has no macOS support** (no WKWebView driver). Native-webview e2e therefore runs only on Linux/Windows — in CI, not on the developer's Mac.
Decisions taken with the user: **WebdriverIO + `tauri-driver` in CI** for the native-webview suite, and a **build-time env flag** for mock-provider selection.
## Goals / Non-Goals
**Goals:**
- Render the existing Family page inside the real Tauri desktop shell, backed by the Storybook mock providers, with zero chain/IPC dependency.
- Keep all mock code out of the production bundle (compile-time elimination).
- Replay the owner and operator journeys against the native webview via WebdriverIO + `tauri-driver`, reusing the existing `data-testid` selectors and assertions.
- Run the native-webview suite in Linux CI; keep the existing Playwright/Storybook suite as the cross-platform/local check.
**Non-Goals:**
- No real IPC/chain wiring (remains parent-change tasks 9.4/9.5).
- No macOS native-webview e2e (unsupported by Tauri).
- No migration of the existing Playwright/Storybook suite to WebdriverIO — both coexist.
- No change to the Family feature's behavior or its specs.
## Decisions
**D1 — WebdriverIO + `tauri-driver` for native-webview e2e (not Playwright).**
Playwright cannot connect to `tauri-driver`'s W3C WebDriver endpoint. WebdriverIO is the Tauri-documented client and natively speaks WebDriver classic. The journeys are simple click/type/assert sequences, so the port from Playwright is mechanical. *Alternative considered:* Selenium — also supported, but WebdriverIO's config + assertion ergonomics are closer to the existing Playwright specs, lowering porting cost.
**D2 — Build-time env flag selects the provider (not runtime).**
A webpack `DefinePlugin` constant (e.g. `process.env.WALLET_MOCK_FAMILIES`) gates the import of `MockFamiliesContextProvider` vs `FamiliesContextProvider` behind a `const MOCK = ...` check. With the flag off (default), the dead branch and its transitive mock imports tree-shake out — no mock engine in production. *Alternatives considered:* runtime URL/query flag (ships mock code in prod unless guarded; rejected for prod-safety) and a fully separate entry point (heavier; the flag is enough since the page is already provider-agnostic).
**D3 — Reuse the existing fixtures and selectors verbatim.**
The mock build seeds `buildOwnerFlowStore` / `buildOperatorFlowStore` and the page already exposes the journey `data-testid`s. The WebdriverIO journeys mirror `FamilyFlows.stories.tsx` step-for-step so the two suites stay observably equivalent. Persona/seed selection at launch reuses the same flag mechanism (e.g. `WALLET_MOCK_FAMILIES=owner|operator`) so each journey gets a deterministic start state.
**D4 — Mock build artifact for the harness.**
`tauri-driver` launches a built binary, so the harness needs a mock-wired build. Options: (a) `tauri dev` with the mock webpack config behind `devUrl`, or (b) a one-off `tauri build` with the mock define set, then point WebdriverIO at the produced binary. CI uses (b) for a stable artifact; local debugging (on Linux) can use (a). The Tauri `frontendDist`/`devUrl` already point at the webpack output, so only the define + entry wiring changes.
**D5 — Skip-not-fail on unsupported platforms.**
The WebdriverIO suite detects macOS (or a missing `tauri-driver`/`WebKitWebDriver`) and skips with a clear message, so `pnpm test:e2e:tauri` on a Mac is a no-op rather than a red failure. CI is the source of truth.
## Risks / Trade-offs
- **Native-webview e2e can't run on the developer's Mac** → Keep the Playwright/Storybook suite as the local check (works everywhere); rely on Linux CI for native-webview coverage; D5 makes the local invocation a clean skip.
- **WebKitGTK rendering/timing differs from Chromium** → Journeys use explicit waits on `data-testid`s (as the Storybook flows already do) and zero/low mock latency for the auto-run paths; generous step timeouts in CI.
- **Build-flag branch could accidentally ship mock code** → Default flag off; add a check (bundle assertion or the existing `check:singletons`-style guard) and the spec scenario "Production build excludes mock code" to lock it in.
- **`tauri-driver` + `WebKitWebDriver` version drift in CI** → Pin `tauri-driver` (`cargo install --locked`) and install a known WebKitGTK driver in the CI image; cache cargo bin.
- **Duplicated journey logic across two suites drifts over time** → Both target identical `data-testid`s and assert identical outcomes (parity requirement); factor shared selector/step constants if drift appears.
## Migration Plan
Additive only — no rollback of existing behavior needed. Rollout: (1) add the build-time flag + provider seam; (2) add the WebdriverIO config/spec; (3) add the Linux CI job. If the native-webview suite proves flaky in CI, it can be gated/`continue-on-error` while the Playwright suite continues to gate merges, with no impact on the app itself.
## Open Questions
- Build vs dev-server launch for the CI harness (D4 (a) vs (b)) — default to a built binary; revisit if build time in CI is prohibitive.
- Whether to also add a Windows CI leg (supported by `tauri-driver`) or keep Linux-only initially — start Linux-only.
- Exact flag surface for persona seeding (single tri-state `WALLET_MOCK_FAMILIES=owner|operator|off` vs a separate seed var) — resolve during tasks.
@@ -0,0 +1,29 @@
## Why
The Node Families feature is fully built and exercised in Storybook, but its end-to-end journeys have only ever run in a browser (Storybook `play` functions + Playwright against the Storybook iframe). Nothing yet proves the page renders and the owner/operator flows work **inside the real Tauri desktop shell** — the native webview, the app router, the production chrome. We want that confidence without needing a live chain or Rust IPC handlers, so we run the same journeys against the page mounted in the Tauri app but backed by the existing Storybook mock providers.
## What Changes
- Mount the existing Family page in the Tauri app behind a **build-time mock flag**: when the flag is 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 entirely.
- Produce a dedicated **mock-wired Tauri build** the test harness can launch — the real binary and native webview, but seeded with the deterministic Storybook fixture stores (`buildOwnerFlowStore` / `buildOperatorFlowStore`) so flows are reproducible offline.
- Add a **WebdriverIO + `tauri-driver`** e2e harness that drives the actual native webview and replays the same journeys currently covered by the Storybook flow stories: owner lifecycle (create → invite → accept → kick → disband) and operator lifecycle (accept → leave, then reject), plus the multi-node invite-states assertion. Selectors reuse the existing `data-testid`s.
- Wire the WebdriverIO suite into **CI on Linux** (Tauri WebDriver supports only Linux/Windows; macOS has no WKWebView driver), keeping the existing Playwright-against-Storybook suite as the cross-platform/local check.
- **Not** adopting Playwright for the Tauri runtime: Playwright cannot speak the classic W3C WebDriver protocol that `tauri-driver` exposes; WebdriverIO is the supported client.
## Capabilities
### New Capabilities
- `families-app-mock-build`: A build-time flag that mounts the Family page inside the Tauri wallet app with the Storybook mock providers, seeded by deterministic fixtures, while keeping mock code out of production builds.
- `families-webdriver-e2e`: A WebdriverIO + `tauri-driver` end-to-end suite that launches the mock-wired Tauri binary and verifies the owner and operator Node Families journeys against the native webview in CI.
### Modified Capabilities
<!-- None: the existing node-families-owner / node-families-operator specs describe behavior that this change exercises but does not alter. -->
## Impact
- **Frontend (`nym-wallet/src`)**: provider-selection seam (build-time flag) around `FamiliesContextProvider` vs `MockFamiliesContextProvider`; a Family route/entry reachable in the mock build; webpack config gains a mock-flag-driven define + entry path.
- **Build/config**: webpack (`webpack.dev.js` / a mock variant), Tauri (`tauri.conf.json` `devUrl`/`frontendDist` or a build profile) so the harness can launch the mock-wired app.
- **Tests (`nym-wallet/e2e`)**: new WebdriverIO config + spec(s) mirroring `e2e/families.spec.ts` journeys; `tauri-driver` as a dev/CI dependency (`cargo install tauri-driver`).
- **CI (`.github`)**: a Linux job that installs `WebKitWebDriver` + `tauri-driver`, builds the mock-wired binary, and runs the WebdriverIO suite.
- **Dependencies**: add `webdriverio` (+ runner) to `nym-wallet` dev deps; `tauri-driver` via cargo. No production dependency or runtime change.
- **Out of scope**: real IPC/chain wiring (still task 9.4/9.5 of the parent change); macOS native-webview e2e (unsupported by Tauri).
@@ -0,0 +1,42 @@
## ADDED Requirements
### Requirement: Build-time mock provider selection
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.
#### Scenario: Mock build wires the mock provider
- **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
#### Scenario: Production build excludes mock code
- **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
### Requirement: Deterministic seeded fixtures in the mock build
The mock-wired build SHALL seed the families mock store from the same deterministic fixtures used by the Storybook flow stories, so that a given launch presents a reproducible starting state for each persona. The build SHALL expose, without a live chain or Rust handlers, an owner-persona entry state and an operator-persona entry state equivalent to `buildOwnerFlowStore` and `buildOperatorFlowStore`.
#### Scenario: Owner persona entry state
- **WHEN** the mock build launches in the owner-persona configuration
- **THEN** the Family page opens with no existing family and the create-family entry point is reachable
- **AND** the self-controlled flow node is available to invite
#### Scenario: Operator persona entry state
- **WHEN** the mock build launches in the operator-persona configuration
- **THEN** the Node invites tab presents the seeded pending invitations on the operator's controlled nodes
### Requirement: Family page reachable in the Tauri app shell
The mock-wired build SHALL mount the Family page within the production app shell (the real router and chrome), not a Storybook iframe, and the page SHALL be reachable through normal in-app navigation. The page and its interactive elements SHALL expose the same `data-testid` selectors as the Storybook stories so a single set of journey selectors works in both environments.
#### Scenario: Navigating to the Family page in the app
- **WHEN** the mock-wired app is loaded and the user navigates to the Family section
- **THEN** the `family-page` element is rendered inside the app shell
- **AND** the owner and operator tabs (`family-tab-owner`, `family-tab-operator`) are present with the same test ids used in Storybook
@@ -0,0 +1,56 @@
## ADDED Requirements
### Requirement: Native-webview e2e harness
The project SHALL provide a WebdriverIO test harness that drives the mock-wired Tauri binary through `tauri-driver` against the platform native webview. The harness MUST launch the actual desktop application (not a browser pointed at a dev server) and MUST NOT require a live chain or Rust IPC handlers, relying instead on the build-time mock providers.
#### Scenario: Harness launches the mock-wired binary
- **WHEN** the WebdriverIO suite starts
- **THEN** `tauri-driver` launches the mock-wired Tauri binary and a session attaches to its native webview
- **AND** the Family page is reachable within that session
#### Scenario: Unsupported platform is skipped, not failed
- **WHEN** the suite is invoked on macOS (no WKWebView driver)
- **THEN** the WebdriverIO/`tauri-driver` suite is skipped with a clear message rather than reported as a failure
### Requirement: Owner lifecycle journey
The harness SHALL replay the owner lifecycle journey end to end against the native webview: create a family, invite the self-controlled node, accept the invite from the operator tab, kick the member, and disband the family. The journey SHALL assert the same post-step DOM transitions verified by the Storybook owner-lifecycle flow.
#### Scenario: Owner create-to-disband completes
- **WHEN** the harness runs the owner lifecycle against the owner-persona mock build
- **THEN** creating a family reveals the owner management page
- **AND** the invited node appears as a pending invite, then as a joined member after acceptance
- **AND** kicking the member removes it, and disbanding returns the create-family entry point
### Requirement: Operator lifecycle journey
The harness SHALL replay the operator lifecycle journey end to end: accept an invite on one controlled node, leave that family, then reject an invite on another controlled node. The journey SHALL assert the same post-step DOM transitions verified by the Storybook operator-lifecycle flow, including that the reject-node invite group ends empty.
#### Scenario: Operator accept-leave-reject completes
- **WHEN** the harness runs the operator lifecycle against the operator-persona mock build
- **THEN** accepting the invite shows the current-family card with a leave action
- **AND** after leaving and rejecting the other node's invite, that node's invite group is empty
### Requirement: Selector and journey parity with Storybook
The WebdriverIO journeys SHALL target the same `data-testid` selectors and assert the same observable outcomes as the existing Storybook flow stories and Playwright specs, so the two suites verify equivalent behavior across the browser and native-webview environments. Confirmation dialogs that portal outside the page canvas SHALL be located by their global test ids, mirroring the Storybook `screen`-scoped queries.
#### Scenario: Equivalent assertions across environments
- **WHEN** a journey step is asserted in both the Playwright/Storybook suite and the WebdriverIO suite
- **THEN** both suites query the same `data-testid` and expect the same visible/absent outcome
### Requirement: CI execution on a supported platform
The WebdriverIO suite SHALL run in CI on a supported platform (Linux). The CI job MUST install the platform webdriver (`WebKitWebDriver`) and `tauri-driver`, build the mock-wired binary, and execute the suite; failures of any owner or operator journey MUST fail the job.
#### Scenario: CI runs the native-webview journeys
- **WHEN** CI runs on Linux for the change
- **THEN** the job installs `WebKitWebDriver` and `tauri-driver`, builds the mock-wired binary, and runs the WebdriverIO journeys
- **AND** the job fails if any owner or operator journey assertion fails
@@ -0,0 +1,42 @@
## 1. Build-time mock provider seam
- [ ] 1.1 Add a webpack `DefinePlugin` constant for the families mock flag (e.g. `process.env.WALLET_MOCK_FAMILIES`, tri-state `owner|operator|off`, default `off`) in the dev/mock webpack config.
- [ ] 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.
- [ ] 1.3 Have the Family route/entry consume the selection module instead of importing `FamiliesContextProvider` directly; in mock mode seed `buildOwnerFlowStore` / `buildOperatorFlowStore` per the persona flag (reuse `families.fixtures.ts`).
- [ ] 1.4 Ensure the Family page is reachable via normal in-app navigation in the mock build and renders inside the app shell (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.
## 2. Mock-wired Tauri build
- [ ] 2.1 Add a mock webpack build script (and, if needed, a `tauri.conf` profile / `devUrl` wiring) that produces the app bundle with the mock define set.
- [ ] 2.2 Add a `tauri:dev`/`tauri:build` variant (npm scripts) that launches/produces the mock-wired binary for each persona configuration.
- [ ] 2.3 Manually confirm (on Linux, or via `tauri dev`) the mock-wired app opens to the correct persona entry state: owner → create-family entry; operator → seeded node invites.
## 3. WebdriverIO + tauri-driver harness
- [ ] 3.1 Add `webdriverio` (+ runner/test framework) to `nym-wallet` dev deps; document `cargo install tauri-driver --locked`.
- [ ] 3.2 Create the WebdriverIO config that starts `tauri-driver`, points `tauri:options.application` at the mock-wired binary, and sets sensible CI step timeouts.
- [ ] 3.3 Implement a skip-not-fail guard: detect macOS / missing `tauri-driver` / missing `WebKitWebDriver` and skip the suite with a clear message (design D5).
- [ ] 3.4 Add `test:e2e:tauri` npm script wiring the WebdriverIO run.
## 4. Journey specs (parity with Storybook)
- [ ] 4.1 Port the owner lifecycle journey (create → invite → accept → kick → disband) from `FamilyFlows.stories.tsx` / `e2e/families.spec.ts` to WebdriverIO, reusing the same `data-testid` selectors.
- [ ] 4.2 Port the operator lifecycle journey (accept → leave, then reject) to WebdriverIO, asserting the reject-node invite group ends empty.
- [ ] 4.3 Port the multi-node operator invite-states assertion (`node-invite-group-201` present, `node-invite-group-203-empty`).
- [ ] 4.4 Handle portalled confirmation dialogs by locating their global test ids (mirror the Storybook `screen`-scoped queries).
- [ ] 4.5 Factor shared selector/step constants so the Playwright and WebdriverIO suites stay observably equivalent (parity requirement).
## 5. CI integration
- [ ] 5.1 Add a Linux CI job that installs `WebKitWebDriver` (WebKitGTK) and `tauri-driver` (`cargo install --locked`, cached).
- [ ] 5.2 In the job, build the mock-wired binary (per persona) and run `test:e2e:tauri`; fail the job on any owner/operator journey failure.
- [ ] 5.3 Keep the existing Playwright/Storybook `test:e2e` job as the cross-platform check (unchanged); document the split in the e2e README/comment.
- [ ] 5.4 Optionally gate the native-webview job as non-blocking initially if flaky, with a note to promote it to required once stable.
## 6. Verification & docs
- [ ] 6.1 Run the WebdriverIO suite in CI (Linux) and confirm both owner and operator journeys pass against the native webview.
- [ ] 6.2 Confirm the macOS local invocation skips cleanly (no red failure).
- [ ] 6.3 Confirm `tsc` + eslint stay clean and the production build is unaffected (no mock code, no behavior change).
- [ ] 6.4 Document the two-suite setup (Playwright→Storybook for local/cross-platform; WebdriverIO→tauri-driver for native-webview CI) and the mock-flag usage in the wallet README / e2e comments.