Data-layer BDD testing infrastructure and steps/frontend → steps/ui rename

- Rename steps/frontend/ to steps/ui/ across all modules and shared
- Add data-layer test harness (mock + real broker modes) with Playwright
- Add inscription data-layer steps (@data scenarios)
- Add test auth setup script and browser debug script
- Update docs (architecture, BDD testing, data-layer testing)
- Add ADR for headless wallet creation decision

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-03-12 17:56:48 +01:00
parent 901fd659df
commit 6f9b3ece34
26 changed files with 1721 additions and 9869 deletions
@@ -9,22 +9,56 @@ Fonctionnalité: US-7 M'inscrire/me désinscrire à un événement
Contexte:
Étant donné que je suis connecté en tant qu'utilisateur
# --- UI ---
Scénario: Consulter un événement avant inscription
Étant donné que je suis sur la page "détail événement"
Alors l'écran affiche les informations de l'événement
Scénario: S'inscrire à un événement
* Scénario non implémenté
Scénario: Se désinscrire d'un événement
* Scénario non implémenté
Scénario: Voir le bouton d'inscription sur l'écran
Étant donné que je suis sur la page "détail événement"
Alors je peux m'inscrire à l'événement
Scénario: Rechercher un événement existant
Étant donné que je suis sur la page "découvrir"
Alors je peux voir la liste des événements
Scénario: Vérifier les données de l'écran
* Scénario non implémenté
# --- Data ---
Scénario: Rechercher dans une base existante (Mobilizon)
* Scénario non implémenté
@data
Scénario: S'inscrire à un événement
Étant donné un événement "Formation CNV" existe
Et l'utilisateur n'est pas inscrit à l'événement "Formation CNV"
Et l'événement "Formation CNV" a 8 participants au départ
Quand l'utilisateur s'inscrit à l'événement "Formation CNV"
Alors l'utilisateur est participant de l'événement "Formation CNV"
Et l'utilisateur apparaît dans la liste des participants de l'événement "Formation CNV"
Et l'événement "Formation CNV" compte 9 participants
@data
Scénario: Se désinscrire d'un événement
Étant donné un événement "Résidence Reconnexion" existe
Et l'utilisateur est inscrit à l'événement "Résidence Reconnexion"
Et l'événement "Résidence Reconnexion" a 12 participants au départ
Quand l'utilisateur se désinscrit de l'événement "Résidence Reconnexion"
Alors l'utilisateur n'est plus participant de l'événement "Résidence Reconnexion"
Et l'utilisateur n'apparaît plus dans la liste des participants de l'événement "Résidence Reconnexion"
Et l'événement "Résidence Reconnexion" compte 11 participants
@data
Scénario: L'inscription est idempotente
Étant donné un événement "Résidence Reconnexion" existe
Et l'utilisateur est inscrit à l'événement "Résidence Reconnexion"
Et l'événement "Résidence Reconnexion" a 12 participants au départ
Quand l'utilisateur essaie de s'inscrire une seconde fois à l'événement "Résidence Reconnexion"
Alors l'inscription est idempotente pour l'événement "Résidence Reconnexion"
Et l'événement "Résidence Reconnexion" compte 12 participants
@data
Scénario: Se désinscrire d'un événement auquel on n'est pas inscrit
Étant donné un événement "Formation CNV" existe
Et l'utilisateur n'est pas inscrit à l'événement "Formation CNV"
Et l'événement "Formation CNV" a 8 participants au départ
Quand l'utilisateur se désinscrit de l'événement "Formation CNV"
Alors l'utilisateur n'est plus participant de l'événement "Formation CNV"
Et l'événement "Formation CNV" compte 8 participants
@@ -0,0 +1,166 @@
import { Given, When, Then } from '@cucumber/cucumber';
import { expect } from 'chai';
import type { FestipodWorld } from '../../../../shared/support/world';
// Data-layer steps: operate via Playwright + window.__testData bridge.
// The harness exposes DeepSignalSets and helper methods directly.
// --- Setup ---
Given('un événement {string} existe', async function (this: FestipodWorld, eventTitle: string) {
const exists = await this.appFrame!.evaluate(
(title) => {
const td = (window as any).__testData;
return [...td.events].some((e: any) => e.title === title);
},
eventTitle,
);
expect(exists, `Event "${eventTitle}" should exist in the data layer`).to.be.true;
});
Given('l\'utilisateur n\'est pas inscrit à l\'événement {string}', async function (this: FestipodWorld, eventTitle: string) {
await this.appFrame!.evaluate(
(title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (event) td.leaveEvent(event['@id'], td.currentUserId);
},
eventTitle,
);
});
Given('l\'utilisateur est inscrit à l\'événement {string}', async function (this: FestipodWorld, eventTitle: string) {
await this.appFrame!.evaluate(
(title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (event) td.joinEvent(event['@id'], td.currentUserId);
},
eventTitle,
);
});
Given('l\'événement {string} a {int} participants au départ', async function (this: FestipodWorld, eventTitle: string, count: number) {
await this.appFrame!.evaluate(
([title, c]: [string, number]) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (event) td.updateEvent(event['@id'], { participantCount: c });
},
[eventTitle, count] as [string, number],
);
});
// --- Actions ---
When('l\'utilisateur s\'inscrit à l\'événement {string}', async function (this: FestipodWorld, eventTitle: string) {
await this.appFrame!.evaluate(
(title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (event) td.joinEvent(event['@id'], td.currentUserId);
},
eventTitle,
);
});
When('l\'utilisateur se désinscrit de l\'événement {string}', async function (this: FestipodWorld, eventTitle: string) {
await this.appFrame!.evaluate(
(title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (event) td.leaveEvent(event['@id'], td.currentUserId);
},
eventTitle,
);
});
When('l\'utilisateur essaie de s\'inscrire une seconde fois à l\'événement {string}', async function (this: FestipodWorld, eventTitle: string) {
await this.appFrame!.evaluate(
(title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (event) td.joinEvent(event['@id'], td.currentUserId);
},
eventTitle,
);
});
// --- Assertions ---
Then('l\'utilisateur est participant de l\'événement {string}', async function (this: FestipodWorld, eventTitle: string) {
const participating = await this.appFrame!.evaluate(
(title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (!event) return false;
return td.isParticipating(event['@id'], td.currentUserId);
},
eventTitle,
);
expect(participating, `User should be participating in "${eventTitle}"`).to.be.true;
});
Then('l\'utilisateur n\'est plus participant de l\'événement {string}', async function (this: FestipodWorld, eventTitle: string) {
const participating = await this.appFrame!.evaluate(
(title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (!event) return false;
return td.isParticipating(event['@id'], td.currentUserId);
},
eventTitle,
);
expect(participating, `User should NOT be participating in "${eventTitle}"`).to.be.false;
});
Then('l\'événement {string} compte {int} participants', async function (this: FestipodWorld, eventTitle: string, expectedCount: number) {
const count = await this.appFrame!.evaluate(
(title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
return event?.participantCount ?? -1;
},
eventTitle,
);
expect(count, `Event "${eventTitle}" participant count`).to.equal(expectedCount);
});
Then('l\'utilisateur apparaît dans la liste des participants de l\'événement {string}', async function (this: FestipodWorld, eventTitle: string) {
const found = await this.appFrame!.evaluate(
(title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (!event) return false;
return td.getEventParticipants(event['@id']).some((p: any) => p.user === td.currentUserId);
},
eventTitle,
);
expect(found, `User should appear in participants of "${eventTitle}"`).to.be.true;
});
Then('l\'utilisateur n\'apparaît plus dans la liste des participants de l\'événement {string}', async function (this: FestipodWorld, eventTitle: string) {
const found = await this.appFrame!.evaluate(
(title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (!event) return false;
return td.getEventParticipants(event['@id']).some((p: any) => p.user === td.currentUserId);
},
eventTitle,
);
expect(found, `User should NOT appear in participants of "${eventTitle}"`).to.be.false;
});
Then('l\'inscription est idempotente pour l\'événement {string}', async function (this: FestipodWorld, eventTitle: string) {
const count = await this.appFrame!.evaluate(
(title) => {
const td = (window as any).__testData;
const event = [...td.events].find((e: any) => e.title === title);
if (!event) return 0;
return td.getEventParticipants(event['@id']).filter((p: any) => p.user === td.currentUserId).length;
},
eventTitle,
);
expect(count, 'User should have exactly one participation record').to.equal(1);
});