diff --git a/.gitignore b/.gitignore index b416474..a016054 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,8 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store .ng-tarballs + +# Playwright persistent profile (contains NG wallet) +.playwright-profile/ +.playwright-profile-debug/ +playwright/.auth/ diff --git a/.project/decisions/2026-03-12-1500-headless-wallet-creation.md b/.project/decisions/2026-03-12-1500-headless-wallet-creation.md new file mode 100644 index 0000000..d1176bb --- /dev/null +++ b/.project/decisions/2026-03-12-1500-headless-wallet-creation.md @@ -0,0 +1,54 @@ +# Automated Headless Wallet Creation for CI + +**Date:** 2026-03-12 15:00 +**Status:** Accepted + +## Context + +Data-layer BDD tests (`@data` scenarios) require a NextGraph wallet in a persistent Chromium profile. Previously, the first run required manual interaction: a visible browser opened and the user had to create a wallet and close the browser. This blocked CI execution. + +## Options Considered + +### Option A: Programmatic wallet creation via NG SDK +Call `ng.wallet_create()` directly from Node/Bun, bypassing the UI entirely. + +**Arguments for:** +- Fastest execution +- No browser needed for wallet creation + +**Arguments against:** +- `@ng-org/web` is browser-only (WASM + postMessage) +- Would need to reverse-engineer the registration API at `account.nextgraph.eu` +- Doesn't test the real auth flow + +### Option B: Automate the browser UI flow headlessly +Use Playwright to drive the same wallet creation UI a real user would use, but in headless mode. + +**Arguments for:** +- Tests the real auth/login feature end-to-end +- No API reverse-engineering needed +- Same persistent profile used for subsequent test runs +- CI-ready with no manual steps + +**Arguments against:** +- Depends on `nextgraph.eu` and `account.nextgraph.eu` being reachable +- UI changes in NextGraph could break the automation +- Adds ~27s to first run + +## Decision + +Option B — automate the browser UI. The wallet creation flow (navigate to `nextgraph.eu` → "Create Wallet" → accept ToS at `account.nextgraph.eu` → fill username/password → submit) is itself a legitimate test of the app's auth feature. The dependency on external services is acceptable since the tests already depend on the broker being reachable. + +## Consequences + +**Positive:** +- Tests are fully CI-ready (no human interaction) +- Auth/login flow is tested as a side effect +- Single command `bun run test:data` works from a clean state + +**Negative:** +- Requires internet access (nextgraph.eu, account.nextgraph.eu) +- Fragile to NextGraph UI changes (button text, form IDs) + +**Risks:** +- `account.nextgraph.eu` rate limiting could block CI runs that frequently recreate wallets diff --git a/.project/knowledge/architecture.md b/.project/knowledge/architecture.md index 73ebd42..61a2b30 100644 --- a/.project/knowledge/architecture.md +++ b/.project/knowledge/architecture.md @@ -18,7 +18,7 @@ src/modules/ Each module can contain: - `screens/` — React screen components - `features/` — Gherkin `.feature` files (BDD specs) -- `steps/{frontend,backend,e2e}/` — Cucumber step definitions by layer +- `steps/{ui,data,e2e}/` — Cucumber step definitions by layer ## Import Rules @@ -45,7 +45,7 @@ src/modules/event/screens/EventDetailScreen.tsx | `hooks/` | `useShapeWithDefaults` (NextGraph) | | `shapes/` | SHEX definitions + ORM TypeScript bindings | | `utils/` | `ngSession.ts`, `ngBootstrap.ts` | -| `steps/frontend/` | Shared BDD step definitions (navigation, screen, form) | +| `steps/ui/` | Shared BDD step definitions (navigation, screen, form) | | `support/` | Cucumber `world.ts`, `hooks.ts` | | `types/` | `gherkin.ts` (ParsedFeature, ParsedScenario types) | | `lib/` | `utils.ts` (cn helper for Tailwind) | diff --git a/.project/knowledge/bdd-testing.md b/.project/knowledge/bdd-testing.md index 67c81b3..32f8d0c 100644 --- a/.project/knowledge/bdd-testing.md +++ b/.project/knowledge/bdd-testing.md @@ -15,12 +15,12 @@ Each module has step directories for three test layers: ``` src/modules/event/steps/ - frontend/ # UI/screen assertions (active) - backend/ # Data layer assertions (planned) + ui/ # UI/screen assertions (active) + data/ # Data layer assertions e2e/ # Full integration (planned) ``` -Shared steps (cross-domain) live in `src/shared/steps/frontend/`. +Shared steps (cross-domain) live in `src/shared/steps/ui/`. ## Feature Files @@ -37,7 +37,7 @@ Tagged with `@CATEGORY @priority-N` for filtering. ## Step Definitions -### Shared Steps (`src/shared/steps/frontend/`) +### Shared Steps (`src/shared/steps/ui/`) | File | Purpose | |------|---------| @@ -45,7 +45,7 @@ Tagged with `@CATEGORY @priority-N` for filtering. | `form.steps.ts` | Form field validation, required fields, import/duplicate detection | | `screen.steps.ts` | Screen content assertions (participants, events, profiles, QR codes) | -### How Frontend Steps Work +### How UI Steps Work Steps analyze screen **source code** (not rendered DOM): 1. `world.ts` loads screen `.tsx` file content via `loadScreenSource()` @@ -89,13 +89,17 @@ Scripts in `scripts/` parse features and steps into TypeScript data files consum |--------|-------|--------| | `parse-features.ts` | `src/modules/*/features/*.feature` | `src/shared/data/features.ts` | | `parse-test-results.ts` | `reports/cucumber-report.json` | `src/shared/data/testResults.ts` | -| `extract-step-definitions.ts` | `src/shared/steps/frontend/*.ts` | `src/shared/data/stepDefinitions.ts` | +| `extract-step-definitions.ts` | `src/shared/steps/ui/*.ts` | `src/shared/data/stepDefinitions.ts` | Run all: `bun run test:cucumber` +## Data-Layer Testing + +`@data` scenarios test through the real NextGraph broker. See [data-layer-testing](./data-layer-testing.md) for full architecture. + ## Adding New Steps -1. **Module-specific**: Create in `src/modules/{module}/steps/frontend/` -2. **Cross-domain**: Add to `src/shared/steps/frontend/` +1. **Module-specific**: Create in `src/modules/{module}/steps/ui/` +2. **Cross-domain**: Add to `src/shared/steps/ui/` 3. Import `FestipodWorld` type from `../../support/world` (shared) or adjust relative path 4. Run `bun run steps:extract` to regenerate tooltip data diff --git a/.project/knowledge/data-layer-testing.md b/.project/knowledge/data-layer-testing.md new file mode 100644 index 0000000..271d2b6 --- /dev/null +++ b/.project/knowledge/data-layer-testing.md @@ -0,0 +1,113 @@ +# Data-Layer Testing + +BDD scenarios tagged `@data` test the real NextGraph data pipeline through a broker, not mocked data. + +## Overview + +`@data` scenarios run Cucumber steps against a real NextGraph broker. Playwright drives a Chromium instance that authenticates with the broker, which loads our test harness in an iframe. The harness uses real `useShape`/ORM subscriptions and exposes a `window.__testData` bridge for step definitions. + +## Architecture + +``` +Cucumber steps → Playwright (Chromium, persistent profile) + ↓ + https://nextgraph.eu/auth/#/?o=http://127.0.0.1:{port} + ↓ + Broker wallet login (automated) + ↓ + Broker loads app in iframe → http://127.0.0.1:{port} + ↓ + harness-ng.tsx (init → useShape → ORM → broker) + ↓ + window.__testData bridge +``` + +## Dual Mode + +- **Real broker** (default): `harness-ng.tsx` with NextGraph ORM through broker iframe +- **Mock fallback**: `harness.tsx` with standalone DeepSignalSets (if NG harness build fails) + +## Wallet Lifecycle + +Fully automated — no manual interaction required. CI-ready. + +### First Run (wallet creation) +1. `BeforeAll` detects no `.wallet-ready` marker in `.playwright-profile/` +2. Launches headless Chromium with persistent profile +3. Navigates to `https://nextgraph.eu/` → clicks "Create Wallet" +4. Redirected to `account.nextgraph.eu` → clicks "I accept" (ToS) +5. Redirected back → fills username/password form → submits +6. Wallet created in localStorage → marker written +7. This is also a real test of the app's auth/login feature + +### Subsequent Runs (automated login) +1. Marker found → skip wallet creation +2. Headless Chromium with persistent profile +3. Automated login: click "Login" → click wallet link → fill password → submit +4. Broker authenticates, loads app harness in iframe +5. Harness initializes NG, creates ORM subscriptions, seeds data if needed +6. `window.__testData.ready` → steps execute via `appFrame.evaluate()` + +### Wallet Credentials +- Name: `festipod-tests` +- Password: `festipod-tests` + +## Key Technical Details + +### Chromium Flags +``` +--disable-features=PrivateNetworkAccessRespectPreflightResults,BlockInsecurePrivateNetworkRequests,... +--allow-insecure-localhost +--disable-web-security +``` +Required because broker at `nextgraph.eu` (public) loads harness from `http://127.0.0.1:{port}` (local) in an iframe — Chromium's Private Network Access blocks this by default. + +### Persistent Profile (`.playwright-profile/`) +- Stores NG wallet in localStorage (`ng_wallets` on `nextgraph.eu`, `ng_bootstrap` on `nextgraph.net`) +- Gitignored +- Must use full Chrome binary, not `chrome-headless-shell` + +### HTTP Server +- Started in `BeforeAll` on auto-assigned port (`127.0.0.1:0`) +- Serves harness HTML at `/` and JS bundle at `/harness.js` (separate files — inline script breaks due to special characters in bundle) +- Shut down in `AfterAll` + +### ORM Subscriptions +Harness creates subscriptions for all three shapes with scope `did:ng:i` (private store): +- `FpEventShapeType` → events +- `FpUserProfileShapeType` → users +- `FpParticipationShapeType` → participations + +### Test Bridge (`window.__testData`) +Exposed by the harness, consumed by steps via `appFrame.evaluate()`: +- `events`, `users`, `participations` — live DeepSignalSets +- `currentUserId` — IRI of the test user +- `getEvent(id)`, `getEventByTitle(title)` — lookups +- `joinEvent(eventId, userId)`, `leaveEvent(eventId, userId)` — mutations +- `isParticipating(eventId, userId)`, `getEventParticipants(eventId)` — queries +- `updateEvent(eventId, updates)` — field updates + +## 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/world.ts` | World with `page`/`appFrame` fields | +| `src/modules/event/steps/data/inscription.steps.ts` | Inscription data 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) | + +## Commands + +```bash +bun run test:data # Run @data scenarios (real broker if wallet exists, mock fallback) +bun run test:cucumber # Run all scenarios (UI + data) +``` + +## See Also + +- [BDD Testing](./bdd-testing.md) — general Cucumber setup, UI-layer steps +- [Data Layer](./data-layer.md) — NextGraph stack, shapes, context providers diff --git a/AGENTS.md b/AGENTS.md index f548e24..a313247 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,8 @@ NextGraph (P2P/local-first) with SHEX shapes and ORM. See [data-layer](.project/ Multi-layer Cucumber/Gherkin in French. See [bdd-testing](.project/knowledge/bdd-testing.md). +`@data` scenarios test through the real NextGraph broker with Playwright. See [data-layer-testing](.project/knowledge/data-layer-testing.md). + ## Quick Start ```bash @@ -45,3 +47,4 @@ bun run build:orm # Regenerate ORM from SHEX shapes - [Data Layer](.project/knowledge/data-layer.md) — NextGraph, shapes, context, seed data - [BDD Testing](.project/knowledge/bdd-testing.md) — Cucumber setup, step layers, feature files - [Screens](.project/knowledge/screens.md) — screen inventory, registry, sketchy components +- [Data-Layer Testing](.project/knowledge/data-layer-testing.md) — real broker testing, wallet setup, Playwright harness diff --git a/CLAUDE.md b/CLAUDE.md index 3cefacd..c075dfe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ This project has two parts: Feature-based architecture: code is organized by business domain (module), not by technical layer. A module can only import from `shared/` — never from another module. -Multi-layer BDD: each module has `steps/frontend/`, `steps/backend/`, `steps/e2e/` directories. Shared step definitions live in `src/shared/steps/`. +Multi-layer BDD: each module has `steps/ui/`, `steps/data/`, `steps/e2e/` directories. Shared step definitions live in `src/shared/steps/`. ## Project Structure @@ -21,8 +21,8 @@ src/ screens/ # EventsScreen, EventDetailScreen, CreateEventScreen, etc. features/ # Gherkin .feature files for this domain steps/ # BDD step definitions - frontend/ # Frontend-layer steps - backend/ # Backend-layer steps (planned) + ui/ # UI-layer steps + data/ # Data-layer steps e2e/ # E2E steps (planned) user/ # User profiles, friends, sharing screens/ # ProfileScreen, FriendsListScreen, ShareProfileScreen, etc. @@ -51,8 +51,8 @@ src/ shapes/ # SHEX shapes + ORM bindings (NextGraph) utils/ # ngSession, ngBootstrap steps/ # Shared BDD step definitions (cross-domain) - frontend/ # navigation.steps.ts, form.steps.ts, screen.steps.ts - backend/ + ui/ # navigation.steps.ts, form.steps.ts, screen.steps.ts + data/ support/ # Cucumber hooks.ts, world.ts types/ # TypeScript type definitions lib/ # Utility functions (cn, etc.) diff --git a/bun.lock b/bun.lock index 16561a8..105118d 100644 --- a/bun.lock +++ b/bun.lock @@ -5,8 +5,8 @@ "": { "name": "bun-react-template", "dependencies": { - "@ng-org/orm": ".ng-tarballs/ng-org-orm-0.1.2-alpha.15.tgz", "@ng-org/alien-deepsignals": ".ng-tarballs/ng-org-alien-deepsignals-0.1.2-alpha.11.tgz", + "@ng-org/orm": ".ng-tarballs/ng-org-orm-0.1.2-alpha.15.tgz", "@ng-org/shex-orm": ".ng-tarballs/ng-org-shex-orm-0.1.2-alpha.7.tgz", "@ng-org/web": ".ng-tarballs/ng-org-web-0.1.2-alpha.11.tgz", "@radix-ui/react-label": "^2.1.7", @@ -31,6 +31,7 @@ "@types/react-dom": "^19", "chai": "^6.2.2", "happy-dom": "^16.6.0", + "playwright": "^1.58.2", "tailwindcss": "^4.1.11", "tsx": "^4.21.0", "tw-animate-css": "^1.4.0", @@ -382,7 +383,7 @@ "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -512,6 +513,10 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], + + "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], @@ -728,6 +733,8 @@ "then-request/@types/node": ["@types/node@8.10.66", "", {}, "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw=="], + "tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "yup/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], diff --git a/package.json b/package.json index 388c949..a7c74cb 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,13 @@ "private": true, "type": "module", "scripts": { - "dev": "portless run bun --hot src/index.ts", + "dev": "portless festipod bun --hot src/index.ts", "start": "NODE_ENV=production bun src/index.ts", "build": "bun run build.ts", "test:cucumber": "bun run cucumber:run && bun run cucumber:report && bun run features:parse && bun run steps:extract", "cucumber:run": "node --import tsx/esm node_modules/.bin/cucumber-js --config cucumber.json", + "test:data": "node --import tsx/esm node_modules/.bin/cucumber-js --config cucumber.json --tags @data", + "test:auth-setup": "bun scripts/setup-test-auth.ts", "cucumber:report": "bun scripts/parse-test-results.ts", "features:parse": "bun scripts/parse-features.ts", "steps:extract": "bun scripts/extract-step-definitions.ts", @@ -42,6 +44,7 @@ "@types/react-dom": "^19", "chai": "^6.2.2", "happy-dom": "^16.6.0", + "playwright": "^1.58.2", "tailwindcss": "^4.1.11", "tsx": "^4.21.0", "tw-animate-css": "^1.4.0" diff --git a/reports/cucumber-report.html b/reports/cucumber-report.html index 0900081..7095210 100644 --- a/reports/cucumber-report.html +++ b/reports/cucumber-report.html @@ -46,7 +46,7 @@