Modern UI port, render-based @ui tests, dev seed, layer contracts
- Port modern clean theme (DM Sans, orange accent, app-* CSS classes) and screen redesigns from festipod-mockups; replace sketchy Ubuntu theme. New shared components: BottomNav, EventCover, EventMeetingPoints, Toast, AvatarStack, Tag, RelevanceIcon. - Restructure from prototyping shell to real mobile web app: path-based routing (History API), Gallery/DemoMode/PhoneFrame removed, Storybook setup for screen/component browsing. - ConnectScreen ported from mockup (QR-based user connection); routed at /profile/connect, wired from FriendsListScreen. - Dev-only auto-seed of NG wallet when empty (gated on NODE_ENV !== 'production'); bootstrapWallet already self-checks for non-empty ngSet so safe even in race conditions. - Render-based @ui test infrastructure: happy-dom + LocalDataProvider + RouterProvider via src/shared/test-harness/renderHelper.tsx, exposed on the world as renderedDoc. world.hasText/hasField/hasElement prefer the rendered DOM and fall back to source for backward compatibility. - Migrate 25 brittle @ui assertions from regex-on-source to DOM queries; delete implementation-detail tests (showDuplicateWarning, importableEvents, importedFrom — anti-patterns per the new contract). Update feature files where the UI changed: "Mes amis" → "Mon réseau", "Mes événements à venir" → "À venir" on home, Thématique removed from create-event wizard, etc. - Path-based @e2e steps (pushState + popstate dispatch) replacing the legacy "#/demo/…" hash routing tied to the deleted Gallery. - Add .project/knowledge/test-layer-contracts.md defining the role of each test layer (@ui = display with seed data + DOM, @data = mutations through NG broker, @e2e = critical user journeys) with anti-patterns and migration consequences. Test status: 75 passed / 71 skipped (explicit "non implémenté") / 2 failed (pre-existing @wip on ngSet.delete() NG ORM limitation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# Refactor multi-store NextGraph
|
||||
|
||||
**Status:** Incubating — aucun travail démarré
|
||||
**Last updated:** 2026-05-17
|
||||
|
||||
## Context
|
||||
|
||||
L'app Festipod est aujourd'hui *mono-store* : tout ce que l'app écrit (events, profils, participations, friendships) atterrit dans le `private_store` de l'utilisateur connecté. C'est un héritage du sample expense-tracker-rdf, formalisé dans [la décision du 2026-03-17](../decisions/2026-03-17-1600-private-store-nuri-scope.md).
|
||||
|
||||
Ce choix bloque toute évolution vers du multi-utilisateurs : par construction le `private_store` est non partageable (cf. [data-layer](../knowledge/data-layer.md) et la doc NextGraph officielle — *« It is not possible to share the documents of your private store with anybody else »*). Tant que tout est dans le private_store, Bob ne pourra jamais voir l'event d'Alice.
|
||||
|
||||
Le modèle natif NextGraph est *multi-store par utilisateur* (private, protected, public, group, dialog) — chaque type d'information a sa place. Festipod doit s'aligner sur ce modèle avant de pouvoir devenir collaboratif.
|
||||
|
||||
**Déclencheur :** discussion du 2026-05-17 sur la suite multi-user. Décision prise : *poser le cap, exécuter plus tard*.
|
||||
|
||||
## What We Know
|
||||
|
||||
### État actuel du code
|
||||
|
||||
Deux fichiers concentrent le hardcoding du store unique :
|
||||
|
||||
- `src/shared/utils/ngGraph.ts:30` — `ensureGraphNuri()` retourne `did:ng:${session.private_store_id}` pour TOUTES les entités, peu importe leur nature.
|
||||
- `src/shared/hooks/useShapeWithDefaults.ts` — accepte un `storeNuri` mais l'appelant unique (`FestipodDataContext`) lui passe systématiquement le NURI du private_store.
|
||||
|
||||
Entités impactées (toutes mélangées dans le même store aujourd'hui) :
|
||||
- `FpEvent` — devrait vivre dans un store partagé (logique multi-user)
|
||||
- `FpUserProfile` — devrait être en partie privée, en partie publique
|
||||
- `FpParticipation` — liée à un event, devrait vivre avec lui
|
||||
- `FpMeetingPoint` — actuellement local-only côté types ([`src/shared/data/types.ts:106`](../../src/shared/data/types.ts)), pas encore branché à NextGraph
|
||||
- `FpFriendship` — actuellement local-only, naturellement privée
|
||||
|
||||
### Modèle cible proposé
|
||||
|
||||
Structure hiérarchique en **4 niveaux de Group stores** (pas de private/public pour le métier collaboratif — tout en Group) :
|
||||
|
||||
```
|
||||
┌─ Group store « index communautaire » ────────────────────┐
|
||||
│ Référence tous les events visibles dans la communauté │
|
||||
│ Lecture par tous les membres, sert d'annuaire/discovery │
|
||||
│ │
|
||||
│ ┌─ Group store « communauté » ──────────────────────┐ │
|
||||
│ │ Propriétaire de l'event │ │
|
||||
│ │ Permissions = qui peut modifier l'event │ │
|
||||
│ │ (organisateurs / membres de la communauté) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─ Group store « event » ─────────────────────┐ │ │
|
||||
│ │ │ Tout ce qui se rattache à l'event : │ │ │
|
||||
│ │ │ participations, infos pratiques, discu… │ │ │
|
||||
│ │ │ Membres = participants à l'event │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ ┌─ Group store « meeting point » ───────┐ │ │ │
|
||||
│ │ │ │ Un RDV de l'event = son propre group │ │ │ │
|
||||
│ │ │ │ Permet participations + discu │ │ │ │
|
||||
│ │ │ │ scopées au point de rencontre │ │ │ │
|
||||
│ │ │ └───────────────────────────────────────┘ │ │ │
|
||||
│ │ └─────────────────────────────────────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Mapping entités → store cible :
|
||||
|
||||
| Entité | Store cible | Justification |
|
||||
|---|---|---|
|
||||
| Event (métadonnées : titre, dates, description) | Group store « communauté » | C'est la communauté qui possède l'event, donc qui contrôle qui peut le modifier |
|
||||
| Référence d'event (pointeur depuis l'index) | Group store « index communautaire » | Discovery : « voici les events visibles » |
|
||||
| Participation | Group store « event » | Une participation n'a de sens que dans le contexte de son event |
|
||||
| MeetingPoint (métadonnées) | Group store « event » | Le RDV appartient à l'event |
|
||||
| Participation à un MeetingPoint | Group store « meeting point » | RSVP/présence scopés au RDV |
|
||||
| UserProfile (partie publique) | public_store de l'utilisateur | Modèle natif NextGraph |
|
||||
| Friendship | private_store de l'utilisateur | Donnée purement personnelle |
|
||||
|
||||
### Contrainte SDK bloquante
|
||||
|
||||
La création de Group stores et la gestion des invitations/permissions **ne sont pas exposées dans le SDK `@ng-org/web` actuel** (version `0.1.2-alpha.11`). Les méthodes disponibles : `doc_create`, `doc_subscribe`, `sparql_query/update`, `orm_start_*`, `file_get`, `app_request_stream`. Aucune méthode `share_doc`, `invite_user`, `create_group_store`, `accept_invite`. La doc NextGraph annonce qu'*« An API will be provided for permission manipulation »* — pas de date.
|
||||
|
||||
**Implication :** le refactor *structurel* (passer d'un store unique à un système de stores par entité) peut commencer sans attendre cette API, en utilisant des placeholders (par ex. continuer à pointer vers `private_store_id` pour les Group stores qui ne peuvent pas encore exister). Mais l'**aboutissement complet** (vrai multi-user, partage entre wallets distincts) dépend de l'arrivée de l'API SDK ou d'un contournement (fork du wallet, accès Rust direct, etc.).
|
||||
|
||||
### Implications côté code
|
||||
|
||||
Le refactor touche au moins :
|
||||
|
||||
1. **Disparition de `ensureGraphNuri()`** comme helper unique. Remplacé par des helpers par entité (`getEventStore(communityId)`, `getParticipationStore(eventId)`, `getProfileStore(scope: 'public' | 'private')`, …) ou par une couche `storeRegistry` qui résout le NURI selon `(entité, contexte)`.
|
||||
2. **`useShapeWithDefaults` reste un wrapper utile** mais l'appelant choisit explicitement le store. Aujourd'hui un seul appelant ([`FestipodDataContext`](../../src/shared/context/FestipodDataContext.tsx)), demain N appelants ou un appelant qui résout dynamiquement.
|
||||
3. **Chaque entité de domaine déclare son store cible** — soit via un mapping centralisé, soit via une convention (shape → store).
|
||||
4. **`bootstrapWallet()`** ([`src/shared/utils/ngBootstrap.ts`](../../src/shared/utils/ngBootstrap.ts)) doit être revu : on ne seed plus dans un unique store, on doit seed dans plusieurs (ou décider que seed ne crée que des données de l'utilisateur courant — ce qui colle mieux à la réalité multi-user).
|
||||
5. **`FestipodDataContext`** : structurer les hooks par entité, chacun avec son store résolu.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Quand crée-t-on un Group store de communauté ?** L'API n'existe pas en SDK aujourd'hui. Faut-il que ce soit un acte explicite de l'utilisateur (« créer une communauté ») ou bien tout user a une communauté par défaut à la création de son wallet ?
|
||||
2. **Comment Bob connaît-il l'index communautaire d'Alice ?** Discovery toujours ouverte — possiblement via le public_store d'Alice qui annonce le NURI de l'index communautaire.
|
||||
3. **Faut-il vraiment 4 niveaux d'imbrication ?** Le « meeting point comme group store » mérite d'être validé — quel besoin réel justifie une couche de permission supplémentaire vs un simple sous-graphe du group store de l'event ?
|
||||
4. **Que devient le seed de démo** quand l'app est multi-store et que les Group stores ne peuvent pas encore exister ? Mode dégradé en private_store le temps que le SDK rattrape, ou retirer le seed en mode connecté ?
|
||||
5. **Migration des wallets existants** : les wallets de test ont déjà des données dans le private_store. Comment on les fait évoluer (script de migration, wipe and reseed, ignore) ?
|
||||
6. **Bootstrap d'un user vierge** : à la première connexion, faut-il auto-créer un Group store communautaire « par défaut » pour lui ou attendre une action utilisateur ?
|
||||
|
||||
## Possible Approaches
|
||||
|
||||
Esquisses sans engagement (les arbitrages se feront dans une décision dédiée au moment de l'exécution) :
|
||||
|
||||
- **Refactor structurel d'abord, partage ensuite.** Réorganiser l'app en multi-store dès maintenant en utilisant `private_store_id` comme placeholder pour les Group stores manquants. Quand l'API arrive, on remplace les placeholders par de vrais NURIs de Group stores.
|
||||
- **Registry centralisé** vs **résolution par convention**. Soit un `storeRegistry.ts` qui mappe explicitement `(entité, contexte) → NURI`, soit chaque shape porte sa propre logique de scope.
|
||||
- **Big-bang** vs **par entité**. Tout migrer en un coup vs migrer entité par entité (commencer par Event qui est le plus stratégique).
|
||||
- **Maintenir un mode mono-store** parallèle pour le dev/demo tant que les Group stores ne sont pas fonctionnels.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
Ce brief — et le refactor qui en découlera — **ne traite pas** :
|
||||
- L'invitation effective d'utilisateurs à un Group store (capability sharing, Nuri d'invitation)
|
||||
- La gestion des permissions par rôle (organisateur / membre / lecteur)
|
||||
- La résolution du problème de discovery cross-wallet
|
||||
- Le contournement éventuel de l'UI wallet (jugée dysfonctionnelle dans cette conversation)
|
||||
- Le mode P2P direct sans broker
|
||||
|
||||
Ces sujets relèvent d'un **second chantier multi-user** dont le refactor multi-store est seulement le *prérequis structurel*.
|
||||
|
||||
## Starting Points
|
||||
|
||||
- [decision: private_store NURI scope](../decisions/2026-03-17-1600-private-store-nuri-scope.md) — la décision actuelle qu'on viendra modifier
|
||||
- [knowledge: data-layer](../knowledge/data-layer.md) — état actuel du pattern d'écriture
|
||||
- [`src/shared/utils/ngGraph.ts`](../../src/shared/utils/ngGraph.ts) — point de hardcoding principal
|
||||
- [`src/shared/hooks/useShapeWithDefaults.ts`](../../src/shared/hooks/useShapeWithDefaults.ts) — l'autre point de hardcoding
|
||||
- [`src/shared/context/FestipodDataContext.tsx`](../../src/shared/context/FestipodDataContext.tsx) — l'unique appelant aujourd'hui
|
||||
- [`src/shared/utils/ngBootstrap.ts`](../../src/shared/utils/ngBootstrap.ts) — le seed à revoir
|
||||
- NextGraph docs : [Documents et Stores](https://docs.nextgraph.org/en/documents/), [Getting started](https://docs.nextgraph.org/en/getting-started/)
|
||||
@@ -47,12 +47,11 @@ Tagged with `@CATEGORY @priority-N` for filtering.
|
||||
|
||||
### How UI Steps Work
|
||||
|
||||
Steps analyze screen **source code** (not rendered DOM):
|
||||
1. `world.ts` loads screen `.tsx` file content via `loadScreenSource()`
|
||||
2. Steps use regex patterns on JSX source to verify UI elements
|
||||
3. `screenFileMap` in `world.ts` maps screen IDs to file paths (e.g., `'home'` → `'src/modules/home/screens/HomeScreen.tsx'`)
|
||||
4. `screenFieldDetectors` define per-screen regex patterns for field verification
|
||||
5. `screenExpectedContent` lists expected text content per screen
|
||||
`@ui` steps render the screen with `LocalDataProvider` (seed data) and `RouterProvider` via happy-dom, then assert on the rendered DOM. The render helper lives in `src/shared/test-harness/renderHelper.tsx` and is invoked from `world.ts:renderCurrentScreen()` on every `navigateTo(...)`.
|
||||
|
||||
See [test-layer-contracts](./test-layer-contracts.md) for what `@ui` is allowed to test and the patterns to follow (and avoid).
|
||||
|
||||
Legacy: `screenFileMap`, `screenFieldDetectors`, `screenExpectedContent`, `screenRequiredFields` in `world.ts` are vestiges of an earlier source-code-grep approach. `hasText`/`hasField`/`hasElement` now prefer the rendered DOM and fall back to source so unmigrated steps keep working during the transition.
|
||||
|
||||
### Screen Name Resolution
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# Test Layer Contracts
|
||||
|
||||
Each BDD test layer (`@ui`, `@data`, `@e2e`) answers a distinct question. Mixing concerns produces brittle tests that fail on refactors without catching real regressions.
|
||||
|
||||
## Overview
|
||||
|
||||
```
|
||||
/\ @e2e ~10 scénarios, parcours utilisateur critiques
|
||||
/ \
|
||||
/----\
|
||||
/ @data\ ~10 scénarios, mutations & persistance NG
|
||||
/--------\
|
||||
/ @ui \ ~60 scénarios, 1-5 par état d'écran × 15 écrans
|
||||
/____________\
|
||||
```
|
||||
|
||||
The pyramid reflects cost: `@ui` runs in-process (instant), `@data` boots a broker (~50s for the suite), `@e2e` boots broker + real app + navigates a real browser (~2min). Move every assertion to the lowest layer that can answer the question — UI rendering claims belong in `@ui`, not `@e2e`.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **`@ui` — display layer.** Renders a screen with `LocalDataProvider` (seed data) + happy-dom and asserts on the resulting DOM. Verifies that *given known data, the screen shows the expected text and elements*. Does **not** test navigation outcomes, mutations, or data persistence.
|
||||
|
||||
- **`@data` — data layer.** Drives ORM mutations through the real NextGraph broker via a headless test harness. No app UI involved. Verifies that *operations on shapes are correctly persisted and observable in the wallet*. See [data-layer-testing](./data-layer-testing.md).
|
||||
|
||||
- **`@e2e` — integration layer.** Boots the real app inside the broker iframe with a Playwright-controlled Chromium. Verifies that *layers collaborate to deliver a user journey* (e.g. create → list → modify → reload → still there). Sparse: 1 scenario per critical path; never duplicate `@ui` content checks here.
|
||||
|
||||
## Implementation
|
||||
|
||||
### `@ui` — rendering helper
|
||||
|
||||
`src/shared/test-harness/renderHelper.tsx` installs happy-dom globals and renders any screen wrapped in `LocalDataProvider` + `RouterProvider`. Called from `world.ts:renderCurrentScreen()` on every `navigateTo(...)`. Seed data (`src/shared/data/seedData.ts`) provides predictable fixtures — `Marie Dupont`/`@mariedupont` is `currentUser`, `Jean Durand`/`@jeandurand` exists in `users`, 5 seed events, etc.
|
||||
|
||||
**Good `@ui` assertion patterns:**
|
||||
|
||||
```ts
|
||||
// Text visible to the user
|
||||
expect(this.getDomText()).to.include('Marie Dupont');
|
||||
|
||||
// Element presence by class/role
|
||||
expect(this.renderedDoc!.querySelector('.app-avatar')).to.not.be.null;
|
||||
|
||||
// Conditional rendering (filled state vs empty state)
|
||||
const cards = this.renderedDoc!.querySelectorAll('.app-card');
|
||||
expect(cards.length).to.be.greaterThan(0);
|
||||
|
||||
// Required form fields rendered with their label + asterisk
|
||||
const labels = Array.from(this.renderedDoc!.querySelectorAll('p'))
|
||||
.map(p => p.textContent ?? '');
|
||||
expect(labels.some(t => t.includes("Nom de l'événement *"))).to.be.true;
|
||||
```
|
||||
|
||||
**Anti-patterns to remove:**
|
||||
|
||||
```ts
|
||||
// ❌ Regex on source: couples test to code structure, fails on refactor
|
||||
expect(/<Title[^>]*>Marie Dupont<\/Title>/.test(source)).to.be.true;
|
||||
|
||||
// ❌ Testing implementation details
|
||||
expect(/showDuplicateWarning/.test(source)).to.be.true;
|
||||
expect(/importableEvents/.test(source)).to.be.true;
|
||||
|
||||
// ❌ Testing JSX structure rather than rendered output
|
||||
expect(/<Avatar[^>]*initials="MD"[^>]*size="lg"/.test(source)).to.be.true;
|
||||
```
|
||||
|
||||
### `@data` — broker-only
|
||||
|
||||
Already isolated correctly. See [data-layer-testing](./data-layer-testing.md). Don't touch the DOM here; use the test harness bridge (`window.__testData`).
|
||||
|
||||
### `@e2e` — full stack
|
||||
|
||||
Path-based routing: navigate via `window.history.pushState` + `popstate` dispatch (`src/modules/auth/steps/e2e/connexion.steps.ts`). Assert on actual DOM text after `appFrame.waitForFunction`. **Do not** re-verify here what `@ui` already covers — `@e2e` should fail when *collaboration* between layers breaks, not when an icon changes.
|
||||
|
||||
## Migration Consequences
|
||||
|
||||
The current `@ui` suite predates this contract. The migration plan:
|
||||
|
||||
1. **Rewrite source-grep assertions** → DOM queries via the helper. The `world.ts:hasText/hasField/hasElement` methods already prefer the rendered DOM and fall back to source — so unmigrated steps still work during the transition.
|
||||
2. **Delete tests on implementation details** (`/showDuplicateWarning/`, `/importableEvents/`, regex on JSX). They protect nothing the user sees.
|
||||
3. **Move behavioral assertions to `@e2e`** when not already covered ("clicking Suivant advances the wizard" — exercise it via Playwright if it's not redundant with existing journeys).
|
||||
4. **Drop redundant `@e2e` content checks** that duplicate `@ui` (e.g. "screen contains 'Découvrir'" — let `@ui` own that).
|
||||
|
||||
`world.ts:screenFileMap`, `screenFieldDetectors`, `screenExpectedContent`, `screenRequiredFields` are vestiges of the source-analysis era. Once the migration is complete, they can be removed in favor of seed-data assertions on the rendered DOM.
|
||||
|
||||
## See Also
|
||||
|
||||
- [BDD Testing setup](./bdd-testing.md) — Cucumber config, file layout, scripts
|
||||
- [Data Layer](./data-layer.md) — NextGraph shapes, seed data, contexts
|
||||
- [Data-Layer Testing](./data-layer-testing.md) — broker harness, wallet setup, Playwright
|
||||
- [Architecture](./architecture.md) — module structure
|
||||
Reference in New Issue
Block a user