E2E testing infrastructure, NextGraph connection fixes, and documentation

- Add @e2e test layer: real app in broker iframe via Playwright
- Fix broker redirect: conditional auto-init only when inside iframe
- Fix seed data flash: empty data during 'connecting' phase
- Fix Gallery button in iframe: explicit navigate instead of history.back
- Add auth e2e feature scenarios and step definitions
- Update docs: bdd-testing, data-layer-testing, data-layer, AGENTS.md
- Add decision record for conditional NG init approach

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-03-13 17:51:44 +01:00
parent 6f9b3ece34
commit 708cbeead8
23 changed files with 12033 additions and 361 deletions
@@ -0,0 +1,48 @@
# Conditional NextGraph Init Based on Broker Iframe Detection
**Date:** 2026-03-13 14:00
**Status:** Accepted
## Context
`@ng-org/web`'s `initNgWeb()` checks `window.self === window.top`. When the app runs standalone (not in an iframe), it redirects the entire page to `nextgraph.net/redir/` to trigger broker authentication. This caused the app to redirect on every load — even during development or when the user hadn't clicked "Se connecter".
## Options Considered
### Option A: Always auto-init NG on mount
**Arguments for:**
- Simpler code — no branching logic
**Arguments against:**
- Causes immediate redirect to broker when loaded standalone
- Breaks development workflow
- User sees broker login page instead of the app
### Option B: Conditional auto-init based on iframe detection
**Arguments for:**
- When in iframe, the broker has already authenticated — safe to auto-init
- When standalone, user must explicitly click "Se connecter" to trigger the redirect
- Preserves standalone demo/development experience
- Matches `@ng-org/web`'s own detection logic
**Arguments against:**
- Relies on `window.self !== window.top` heuristic (could theoretically be wrong if embedded in non-broker iframe)
## Decision
Option B. `NextGraphContext` checks `const isInsideBroker = typeof window !== 'undefined' && window.self !== window.top` at module level. `useEffect` only auto-calls `initNg()` when `isInsideBroker` is true. The `connect()` callback remains available for explicit user-initiated connection.
Additionally, `FestipodDataContext` now renders empty data (not seed data) during the `connecting` phase to avoid flashing demo content before the wallet loads.
## Consequences
**Positive:**
- App loads without redirecting — works standalone for development and demo
- In broker iframe, connection is seamless and automatic
- No seed data flash during wallet connection
**Negative:**
- None significant
**Risks:**
- If `@ng-org/web` changes its detection logic, our guard may diverge — keep them aligned
+12 -3
View File
@@ -15,9 +15,9 @@ Each module has step directories for three test layers:
```
src/modules/event/steps/
ui/ # UI/screen assertions (active)
data/ # Data layer assertions
e2e/ # Full integration (planned)
ui/ # UI/screen assertions (source analysis)
data/ # Data layer assertions (Playwright + broker)
e2e/ # E2E assertions (Playwright + broker + real app UI)
```
Shared steps (cross-domain) live in `src/shared/steps/ui/`.
@@ -97,6 +97,15 @@ Run all: `bun run test:cucumber`
`@data` scenarios test through the real NextGraph broker. See [data-layer-testing](./data-layer-testing.md) for full architecture.
## E2E Testing
`@e2e` scenarios test the real app running in the broker iframe. See [data-layer-testing](./data-layer-testing.md#e2e-layer) for architecture. Key differences from `@data`:
- Uses the **real app** (not a test harness) served on a local HTTP port
- Interacts via Playwright locators and `evaluate()` on the app iframe
- Tests actual UI behavior: navigation, redirects, button clicks, screen content
- Requires real broker mode (fails with `Error` if broker unavailable)
## Adding New Steps
1. **Module-specific**: Create in `src/modules/{module}/steps/ui/`
+62 -2
View File
@@ -87,15 +87,75 @@ Exposed by the harness, consumed by steps via `appFrame.evaluate()`:
- `isParticipating(eventId, userId)`, `getEventParticipants(eventId)` — queries
- `updateEvent(eventId, updates)` — field updates
## E2E Layer (`@e2e`)
`@e2e` scenarios test the real app UI running inside the broker iframe. Unlike `@data` which loads a test harness, `@e2e` loads the actual app.
### Architecture
```
Cucumber steps → Playwright (Chromium, persistent profile)
https://nextgraph.net/redir/#/?o=http://127.0.0.1:{appPort}
Broker wallet login (automated, same as @data)
Broker loads REAL APP in iframe → http://127.0.0.1:{appPort}
App renders with NextGraphProvider auto-connecting
Steps interact via appFrame.evaluate() and Playwright locators
```
### App Server
Started in `BeforeAll` alongside the harness server:
1. Find a free port
2. `spawn('bun', ['src/index.ts'], { env: { PORT: appPort } })`
3. Poll until the server responds to HTTP GET
4. Killed in `AfterAll`
### Shared Infrastructure
`@e2e` reuses the same `setupBrokerPage()` helper as `@data` — handles broker redirect URL construction, wallet login automation, and iframe discovery.
### Step Definitions
E2E steps live in module directories (e.g., `src/modules/auth/steps/e2e/connexion.steps.ts`). They use:
- `this.appFrame!.evaluate()` — run JS in the app iframe (hash navigation, content checks)
- `this.appFrame!.locator()` — find and interact with DOM elements
- `this.appFrame!.waitForFunction()` — poll for expected state (screen content, URL changes)
- `SCREEN_MARKERS` — map screen IDs to unique text content for verification
### Before Hook (`@e2e`)
```
1. Open new Playwright page
2. setupBrokerPage(page, realAppUrl) → automated login → find app iframe
3. Wait for React render (root.innerHTML.length > 100)
4. Wait 3s for NG connection + provider stabilization
```
### Differences from `@data`
| Aspect | `@data` | `@e2e` |
|--------|---------|--------|
| What loads in iframe | Test harness (`harness-ng.tsx`) | Real app (`src/index.ts`) |
| Ready signal | `window.__testData.ready === true` | `root.innerHTML.length > 100` |
| Interaction | `evaluate()` on test bridge | `evaluate()` + Playwright locators |
| Mock fallback | Yes (standalone DeepSignalSets) | No — requires real broker |
| Tests | Data operations (CRUD, queries) | UI behavior (navigation, redirects, clicks) |
## Files
| File | Purpose |
|------|---------|
| `src/shared/test-harness/harness-ng.tsx` | Real broker harness (useShape through broker iframe) |
| `src/shared/test-harness/harness.tsx` | Mock harness (DeepSignalSets, no broker) |
| `src/shared/support/hooks.ts` | Playwright lifecycle (wallet creation, login automation, iframe detection) |
| `src/shared/support/hooks.ts` | Playwright lifecycle (wallet creation, login automation, iframe detection, app server) |
| `src/shared/support/world.ts` | World with `page`/`appFrame` fields |
| `src/modules/event/steps/data/inscription.steps.ts` | Inscription data steps |
| `src/modules/auth/steps/e2e/connexion.steps.ts` | Auth/connection e2e steps |
| `.playwright-profile/` | Persistent Chromium profile (gitignored) |
| `scripts/debug-browser.ts` | Manual browser debug tool — launches headed Chromium to inspect broker interactions |
| `.playwright-profile-debug/` | Chromium profile created by debug-browser.ts (gitignored) |
@@ -104,7 +164,7 @@ Exposed by the harness, consumed by steps via `appFrame.evaluate()`:
```bash
bun run test:data # Run @data scenarios (real broker if wallet exists, mock fallback)
bun run test:cucumber # Run all scenarios (UI + data)
bun run test:cucumber # Run all scenarios (UI + data + e2e)
```
## See Also
+11 -2
View File
@@ -39,15 +39,24 @@ Regenerate with `bun run build:orm`.
### NextGraphContext (`src/shared/context/NextGraphContext.tsx`)
- Connection lifecycle: `disconnected``connecting``connected` | `error`
- Auto-initializes via `initNg()` from `src/shared/utils/ngSession.ts`
- Provides session with store IDs (private, protected, public)
- **Conditional auto-init**: Only auto-calls `initNg()` when running inside the broker iframe (`window.self !== window.top`). Outside the iframe, `initNgWeb()` would redirect the page to the broker — so connection waits for explicit `connect()` call.
- `connect()`: Called by user clicking "Se connecter". When outside broker, triggers the redirect flow.
#### `@ng-org/web` redirect behavior
`initNgWeb()` checks `window.self === window.top`. If the app is NOT in an iframe, it redirects to `nextgraph.net/redir/` with the current URL encoded as a return parameter. The broker then loads the app back in an iframe after auth. This means the app must NOT auto-init NG when loaded standalone.
### FestipodDataContext (`src/shared/context/FestipodDataContext.tsx`)
- Wraps NextGraph shapes with `useShapeWithDefaults()` hook
- CRUD: `createEvent()`, `updateEvent()`, `joinEvent()`, `leaveEvent()`, etc.
- Exposes `useFestipodData()` hook consumed by all screens
- `selectedEventId` state for cross-screen event navigation
- Falls back to seed data when disconnected
- `loadTestData()`: Calls `bootstrapWallet()` to seed test data into NG wallet — only triggered by explicit user action
- **Provider states based on NG status**:
- `disconnected``LocalDataProvider` with seed data (demo mode)
- `connecting``LocalDataProvider` with **empty data** (avoids flashing seed data before wallet loads)
- `connected``NgDataProvider` with real wallet data
- `error``LocalDataProvider` with seed data (graceful fallback)
## Data Types