ReadCap read filter: consume the lib's per-document model + validate against broker
Align Festipod's @data read-filter scenario and harness bridge with ng-eventually's grant→ReadCap refactor: the access unit is the document (an item's `@graph`), not the item. - harness-ng.tsx: governDocument(reader, user)/setUser via getCaps()/resetCaps() (replaces setupReadFilter/setGrantOf); FilterProbe exposes a lazy snapshot() reflecting the current user without remount. - read-filter.feature/steps: validate per-document ReadCap on the real DeepSignalSet — govern the wallet document, grant the cap to another user → current user sees 0; current user gets the cap → sees all (all-or-nothing in mono-store, the faithful behavior). 5/5 steps pass against the broker. - doctrine: knowledge_stores-permissions records the verified store/document/ repo/ReadCap model (containment by reference, no read-cap inheritance); decision_2026-06-17_eventually-library updates the access-rights + filter status to the ReadCap model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
# language: fr
|
||||
@WORKSHOP @priority-1
|
||||
Fonctionnalité: Filtre de lecture (ng-eventually)
|
||||
Fonctionnalité: Filtre ReadCap (ng-eventually)
|
||||
En tant que développeur
|
||||
Je veux valider, contre le vrai broker, que le filtre de lecture de la lib
|
||||
ne renvoie que les données autorisées pour l'utilisateur courant, sur le vrai
|
||||
set réactif de l'ORM.
|
||||
applique les ReadCap au niveau du DOCUMENT (le repo où vit chaque item) sur le
|
||||
vrai set réactif de l'ORM : on ne voit un document que si on détient sa ReadCap.
|
||||
En mono-store (tout dans un seul repo) c'est donc tout-ou-rien sur ce document
|
||||
— le comportement fidèle de NextGraph.
|
||||
|
||||
@data
|
||||
Scénario: Le filtre ne renvoie que les participations autorisées
|
||||
Étant donné le wallet contient des participations de plusieurs utilisateurs
|
||||
Quand j'active le filtre de lecture pour l'utilisateur courant
|
||||
Alors je ne vois que les participations de l'utilisateur courant
|
||||
Et le filtre a masqué au moins une participation d'un autre utilisateur
|
||||
Scénario: On ne voit un document que si on détient sa ReadCap
|
||||
Étant donné le wallet contient des participations dans un document
|
||||
Quand je gouverne ce document par une ReadCap accordée à un autre utilisateur
|
||||
Alors l'utilisateur courant ne voit aucune participation de ce document
|
||||
Quand l'utilisateur courant obtient la ReadCap du document
|
||||
Alors il voit toutes les participations du document
|
||||
|
||||
@@ -2,45 +2,45 @@ import { Given, When, Then } from '@cucumber/cucumber';
|
||||
import { expect } from 'chai';
|
||||
import type { FestipodWorld } from '../../../../shared/support/world';
|
||||
|
||||
// Validates ng-eventually's READ FILTER on the REAL ORM set, against the broker.
|
||||
// The harness grants each participation to its own `user`; with the filter on
|
||||
// for a chosen user, useShape must yield only that user's participations.
|
||||
// Validates ng-eventually's READ FILTER (ReadCap) on the REAL ORM set, against
|
||||
// the broker. The filter is per-DOCUMENT (an item's @graph = the repo it lives
|
||||
// in): you see a document only if you hold its read cap. In mono-store, every
|
||||
// participation shares one document, so governing it is all-or-nothing — the
|
||||
// faithful NextGraph behavior. Two synthetic users discriminate cap possession,
|
||||
// NOT the participation's own `user` field.
|
||||
|
||||
Given('le wallet contient des participations de plusieurs utilisateurs', async function (this: FestipodWorld) {
|
||||
// Deterministic: create two participations for two synthetic users on a
|
||||
// synthetic event (isolated from real-event counts; joinEvent is idempotent on
|
||||
// event+user, so this doesn't accumulate across runs).
|
||||
Given('le wallet contient des participations dans un document', async function (this: FestipodWorld) {
|
||||
// Deterministic: ensure ≥1 participation exists in the wallet document.
|
||||
// joinEvent is idempotent on (event,user), so this doesn't accumulate.
|
||||
await this.appFrame!.evaluate(async () => {
|
||||
const td = (window as any).__testData;
|
||||
await td.joinEvent('urn:rf:event', 'urn:rf:alice');
|
||||
await td.joinEvent('urn:rf:event', 'urn:rf:bob');
|
||||
await td.joinEvent('urn:rf:event', 'urn:rf:p1');
|
||||
await td.joinEvent('urn:rf:event', 'urn:rf:p2');
|
||||
});
|
||||
await this.appFrame!.waitForFunction(
|
||||
() => {
|
||||
const ps = [...(window as any).__testData.participations];
|
||||
return ps.some((p: any) => p.user === 'urn:rf:alice') && ps.some((p: any) => p.user === 'urn:rf:bob');
|
||||
return ps.some((p: any) => p.user === 'urn:rf:p1') && ps.some((p: any) => p.user === 'urn:rf:p2');
|
||||
},
|
||||
null,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
const data = await this.appFrame!.evaluate(() => {
|
||||
const td = (window as any).__testData;
|
||||
const parts = [...td.participations];
|
||||
const targetUser = 'urn:rf:alice';
|
||||
return {
|
||||
targetUser,
|
||||
total: parts.length,
|
||||
ownCount: parts.filter((p: any) => p.user === targetUser).length,
|
||||
};
|
||||
// Raw set (no policy yet) → true total in the document.
|
||||
return { total: [...td.participations].length, documentNuri: td.documentNuri };
|
||||
});
|
||||
(this as any).rf = data;
|
||||
expect(data.ownCount, 'exactly one participation for the target user').to.equal(1);
|
||||
expect(data.total, 'other users have participations too').to.be.greaterThan(data.ownCount);
|
||||
(this as any).rf = { ...data, reader: 'urn:rf:alice', other: 'urn:rf:bob' };
|
||||
expect(data.total, 'the document holds participations').to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
When('j\'active le filtre de lecture pour l\'utilisateur courant', async function (this: FestipodWorld) {
|
||||
const { targetUser } = (this as any).rf;
|
||||
await this.appFrame!.evaluate((u: string) => (window as any).__testData.setupReadFilter(u), targetUser);
|
||||
When('je gouverne ce document par une ReadCap accordée à un autre utilisateur', async function (this: FestipodWorld) {
|
||||
const { reader, other } = (this as any).rf;
|
||||
// Grant the document's read cap to `reader`; current user is `other` (no cap).
|
||||
await this.appFrame!.evaluate(
|
||||
(args: { reader: string; user: string }) => (window as any).__testData.governDocument(args.reader, args.user),
|
||||
{ reader, user: other },
|
||||
);
|
||||
await this.appFrame!.waitForFunction(
|
||||
() => (window as any).__readFilter?.ready === true,
|
||||
null,
|
||||
@@ -48,15 +48,18 @@ When('j\'active le filtre de lecture pour l\'utilisateur courant', async functio
|
||||
);
|
||||
});
|
||||
|
||||
Then('je ne vois que les participations de l\'utilisateur courant', async function (this: FestipodWorld) {
|
||||
const r = await this.appFrame!.evaluate(() => (window as any).__readFilter);
|
||||
const { targetUser, ownCount } = (this as any).rf;
|
||||
expect(r.users.every((u: string) => u === targetUser), 'all filtered participations belong to the target user').to.be.true;
|
||||
expect(r.count, 'filtered count equals the target user own participations').to.equal(ownCount);
|
||||
Then('l\'utilisateur courant ne voit aucune participation de ce document', async function (this: FestipodWorld) {
|
||||
const snap = await this.appFrame!.evaluate(() => (window as any).__readFilter.snapshot());
|
||||
expect(snap.count, 'a user without the read cap sees nothing of the document').to.equal(0);
|
||||
});
|
||||
|
||||
Then('le filtre a masqué au moins une participation d\'un autre utilisateur', async function (this: FestipodWorld) {
|
||||
const r = await this.appFrame!.evaluate(() => (window as any).__readFilter);
|
||||
const { total } = (this as any).rf;
|
||||
expect(total, 'the filter hid at least one other-user participation').to.be.greaterThan(r.count);
|
||||
When('l\'utilisateur courant obtient la ReadCap du document', async function (this: FestipodWorld) {
|
||||
const { reader } = (this as any).rf;
|
||||
await this.appFrame!.evaluate((u: string) => (window as any).__testData.setUser(u), reader);
|
||||
});
|
||||
|
||||
Then('il voit toutes les participations du document', async function (this: FestipodWorld) {
|
||||
const { total } = (this as any).rf;
|
||||
const snap = await this.appFrame!.evaluate(() => (window as any).__readFilter.snapshot());
|
||||
expect(snap.count, 'the cap holder sees every participation of the document').to.equal(total);
|
||||
});
|
||||
|
||||
+116
-208
@@ -356,7 +356,7 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'écran contient une section \"Relayer l'événement\""
|
||||
"text": "l'écran contient un bouton \"Suivant\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -374,48 +374,6 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Détecter un événement similaire déjà relayé",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Étant donné que ",
|
||||
"text": "l'écran \"create-event\" est affiché"
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "le formulaire permet de détecter les doublons"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Importer un événement depuis une source externe",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Étant donné que ",
|
||||
"text": "l'écran \"create-event\" est affiché"
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "le formulaire permet d'importer depuis Mobilizon ou Transiscope"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Pas d'alerte doublon lors d'un import externe",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Étant donné que ",
|
||||
"text": "l'écran \"create-event\" est affiché"
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'import externe ne déclenche pas d'alerte doublon"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Modifier un événement",
|
||||
"tags": [],
|
||||
@@ -433,7 +391,7 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
}
|
||||
],
|
||||
"filePath": "src/modules/event/features/us-13-creer-evenement.feature",
|
||||
"rawContent": "# language: fr\n@EVENT @priority-1\nFonctionnalité: US-13 Relayer/Modifier/Supprimer un événement\n En tant qu'utilisateur\n Je peux relayer/modifier/supprimer un événement\n En choisissant les dates, horaires, lieu et thématique\n Afin de relayer/présenter le contenu de cet événement et le catégoriser\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder au formulaire de relai d'événement\n Étant donné que je suis sur la page \"accueil\"\n Quand je navigue vers \"relayer un événement\"\n Alors je vois l'écran \"create-event\"\n\n Scénario: Vérifier les champs obligatoires du formulaire\n Étant donné que l'écran \"create-event\" est affiché\n Alors le formulaire contient les champs obligatoires suivants:\n | Nom de l'événement |\n | Date de début |\n | Heure de début |\n | Lieu |\n | Thématique |\n\n Scénario: Vérifier la présence du bouton de relai\n Étant donné que je suis sur la page \"relayer un événement\"\n Alors l'écran contient une section \"Relayer l'événement\"\n\n Scénario: Pouvoir annuler le relai d'événement\n Étant donné que je suis sur la page \"relayer un événement\"\n Alors je peux annuler et revenir à l'écran précédent\n\n Scénario: Détecter un événement similaire déjà relayé\n Étant donné que l'écran \"create-event\" est affiché\n Alors le formulaire permet de détecter les doublons\n\n Scénario: Importer un événement depuis une source externe\n Étant donné que l'écran \"create-event\" est affiché\n Alors le formulaire permet d'importer depuis Mobilizon ou Transiscope\n\n Scénario: Pas d'alerte doublon lors d'un import externe\n Étant donné que l'écran \"create-event\" est affiché\n Alors l'import externe ne déclenche pas d'alerte doublon\n\n Scénario: Modifier un événement\n * Scénario non implémenté\n\n Scénario: Supprimer un événement\n * Scénario non implémenté\n\n Scénario: Retirer une organisation (personne ou structure)\n * Scénario non implémenté\n",
|
||||
"rawContent": "# language: fr\n@EVENT @priority-1\nFonctionnalité: US-13 Relayer/Modifier/Supprimer un événement\n En tant qu'utilisateur\n Je peux relayer/modifier/supprimer un événement\n En choisissant les dates, horaires, lieu et thématique\n Afin de relayer/présenter le contenu de cet événement et le catégoriser\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder au formulaire de relai d'événement\n Étant donné que je suis sur la page \"accueil\"\n Quand je navigue vers \"relayer un événement\"\n Alors je vois l'écran \"create-event\"\n\n Scénario: Vérifier les champs obligatoires du formulaire\n Étant donné que l'écran \"create-event\" est affiché\n Alors le formulaire contient les champs obligatoires suivants:\n | Nom de l'événement |\n | Date de début |\n\n Scénario: Vérifier la présence du bouton de relai\n Étant donné que je suis sur la page \"relayer un événement\"\n Alors l'écran contient un bouton \"Suivant\"\n\n Scénario: Pouvoir annuler le relai d'événement\n Étant donné que je suis sur la page \"relayer un événement\"\n Alors je peux annuler et revenir à l'écran précédent\n\n Scénario: Modifier un événement\n * Scénario non implémenté\n\n Scénario: Supprimer un événement\n * Scénario non implémenté\n\n Scénario: Retirer une organisation (personne ou structure)\n * Scénario non implémenté\n",
|
||||
"screenIds": [
|
||||
"create-event",
|
||||
"home"
|
||||
@@ -523,21 +481,17 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
"background": [
|
||||
{
|
||||
"keyword": "Étant donné que ",
|
||||
"text": "l'utilisateur a chargé les données de test"
|
||||
"text": "le portefeuille contient des données de test"
|
||||
}
|
||||
],
|
||||
"scenarios": [
|
||||
{
|
||||
"name": "Créer un événement depuis l'accueil",
|
||||
"name": "Créer un événement et vérifier qu'il apparaît sur l'accueil",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Quand",
|
||||
"text": "l'utilisateur navigue vers l'écran \"home\""
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur clique sur le bouton \"Relayer un événement\""
|
||||
"text": "l'utilisateur navigue vers l'écran \"create-event\""
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
@@ -554,13 +508,7 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'écran contient le texte \"Pique-nique au parc\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "L'événement créé apparaît sur l'accueil",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
},
|
||||
{
|
||||
"keyword": "Quand",
|
||||
"text": "l'utilisateur navigue vers l'écran \"home\""
|
||||
@@ -575,13 +523,9 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
"name": "L'événement créé persiste après reconnexion",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Quand",
|
||||
"text": "l'utilisateur navigue vers l'écran \"home\""
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'écran contient le texte \"Pique-nique au parc\""
|
||||
"text": "l'écran d'accueil contient le texte \"Pique-nique au parc\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -591,11 +535,7 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Quand",
|
||||
"text": "l'utilisateur navigue vers l'écran \"home\""
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur clique sur l'événement \"Pique-nique au parc\""
|
||||
"text": "l'utilisateur clique sur un événement de l'accueil"
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
@@ -603,16 +543,12 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'écran contient le texte \"Pique-nique au parc\""
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'écran contient le texte \"Parc Bordelais\""
|
||||
"text": "l'écran contient le texte \"Participants\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "S'inscrire à un événement existant",
|
||||
"name": "S'inscrire à un événement",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
@@ -621,15 +557,19 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur clique sur le premier événement"
|
||||
"text": "l'utilisateur clique sur un événement de la liste"
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur clique sur le bouton \"Participer\""
|
||||
"text": "l'utilisateur attend que l'écran \"event-detail\" soit affiché"
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur clique sur le bouton \"J'y serai\" si visible"
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'écran contient le texte \"Inscrit\""
|
||||
"text": "l'écran contient le texte \"Je participe\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -643,20 +583,46 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur clique sur le premier événement"
|
||||
"text": "l'utilisateur clique sur un événement de la liste"
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur clique sur le bouton \"Inscrit\""
|
||||
"text": "l'utilisateur attend que l'écran \"event-detail\" soit affiché"
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur clique sur le bouton \"Je participe\""
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'écran contient le texte \"Participer\""
|
||||
"text": "l'écran contient le texte \"J'y serai\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Modifier le titre d'un événement créé",
|
||||
"name": "La désinscription persiste après reconnexion",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Quand",
|
||||
"text": "l'utilisateur navigue vers l'écran \"events\""
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur clique sur un événement de la liste"
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur attend que l'écran \"event-detail\" soit affiché"
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'écran contient le texte \"J'y serai\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Modifier un événement et vérifier la persistance",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
@@ -665,7 +631,11 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur clique sur l'événement \"Pique-nique au parc\""
|
||||
"text": "l'utilisateur clique sur un événement de l'accueil"
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur attend que l'écran \"event-detail\" soit affiché"
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
@@ -673,7 +643,11 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur modifie le champ \"Nom de l'événement\" avec \"Pique-nique d'été\""
|
||||
"text": "l'utilisateur attend que l'écran \"update-event\" soit affiché"
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur modifie le champ lieu avec \"Jardin Public, Bordeaux\""
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
@@ -685,31 +659,13 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'écran contient le texte \"Pique-nique d'été\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "La modification persiste après reconnexion",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Quand",
|
||||
"text": "l'utilisateur navigue vers l'écran \"home\""
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'écran contient le texte \"Pique-nique d'été\""
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'écran ne contient pas le texte \"Pique-nique au parc\""
|
||||
"text": "l'écran contient le texte \"Jardin Public\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"filePath": "src/modules/event/features/cycle-de-vie-evenement.feature",
|
||||
"rawContent": "# language: fr\n@EVENT @priority-1\nFonctionnalité: Cycle de vie d'un événement\n En tant qu'utilisateur connecté\n Je peux créer, consulter, modifier et participer à des événements\n Et ces actions persistent dans mon portefeuille NextGraph\n\n Contexte:\n Étant donné que l'utilisateur a chargé les données de test\n\n # --- Création ---\n\n @e2e\n Scénario: Créer un événement depuis l'accueil\n Quand l'utilisateur navigue vers l'écran \"home\"\n Et l'utilisateur clique sur le bouton \"Relayer un événement\"\n Et l'utilisateur remplit le formulaire de création d'événement:\n | champ | valeur |\n | Nom de l'événement | Pique-nique au parc |\n | Date de début | 2026-06-15 |\n | Heure de début | 14:00 |\n | Lieu | Parc Bordelais, Bordeaux |\n Et l'utilisateur clique sur le bouton \"Relayer l'événement\"\n Alors l'application affiche l'écran \"event-detail\"\n Et l'écran contient le texte \"Pique-nique au parc\"\n\n @e2e\n Scénario: L'événement créé apparaît sur l'accueil\n Quand l'utilisateur navigue vers l'écran \"home\"\n Alors l'écran contient le texte \"Pique-nique au parc\"\n\n @e2e\n Scénario: L'événement créé persiste après reconnexion\n Quand l'utilisateur navigue vers l'écran \"home\"\n Alors l'écran contient le texte \"Pique-nique au parc\"\n\n # --- Consultation ---\n\n @e2e\n Scénario: Consulter le détail d'un événement depuis l'accueil\n Quand l'utilisateur navigue vers l'écran \"home\"\n Et l'utilisateur clique sur l'événement \"Pique-nique au parc\"\n Alors l'application affiche l'écran \"event-detail\"\n Et l'écran contient le texte \"Pique-nique au parc\"\n Et l'écran contient le texte \"Parc Bordelais\"\n\n # --- Inscription / Désinscription ---\n\n @e2e\n Scénario: S'inscrire à un événement existant\n Quand l'utilisateur navigue vers l'écran \"events\"\n Et l'utilisateur clique sur le premier événement\n Et l'utilisateur clique sur le bouton \"Participer\"\n Alors l'écran contient le texte \"Inscrit\"\n\n @e2e\n Scénario: Se désinscrire d'un événement\n Quand l'utilisateur navigue vers l'écran \"events\"\n Et l'utilisateur clique sur le premier événement\n Et l'utilisateur clique sur le bouton \"Inscrit\"\n Alors l'écran contient le texte \"Participer\"\n\n # --- Modification ---\n\n @e2e\n Scénario: Modifier le titre d'un événement créé\n Quand l'utilisateur navigue vers l'écran \"home\"\n Et l'utilisateur clique sur l'événement \"Pique-nique au parc\"\n Et l'utilisateur clique sur le bouton de modification\n Et l'utilisateur modifie le champ \"Nom de l'événement\" avec \"Pique-nique d'été\"\n Et l'utilisateur clique sur le bouton \"Enregistrer les modifications\"\n Alors l'application affiche l'écran \"event-detail\"\n Et l'écran contient le texte \"Pique-nique d'été\"\n\n @e2e\n Scénario: La modification persiste après reconnexion\n Quand l'utilisateur navigue vers l'écran \"home\"\n Alors l'écran contient le texte \"Pique-nique d'été\"\n Et l'écran ne contient pas le texte \"Pique-nique au parc\"\n",
|
||||
"rawContent": "# language: fr\n@EVENT @priority-1\nFonctionnalité: Cycle de vie d'un événement\n En tant qu'utilisateur connecté\n Je peux créer, consulter, modifier et participer à des événements\n Et ces actions persistent dans mon portefeuille NextGraph\n\n Contexte:\n Étant donné que le portefeuille contient des données de test\n\n # --- Création et persistance ---\n\n @e2e\n Scénario: Créer un événement et vérifier qu'il apparaît sur l'accueil\n Quand l'utilisateur navigue vers l'écran \"create-event\"\n Et l'utilisateur remplit le formulaire de création d'événement:\n | champ | valeur |\n | Nom de l'événement | Pique-nique au parc |\n | Date de début | 2026-06-15 |\n | Heure de début | 14:00 |\n | Lieu | Parc Bordelais, Bordeaux |\n Et l'utilisateur clique sur le bouton \"Relayer l'événement\"\n Alors l'application affiche l'écran \"event-detail\"\n Et l'écran contient le texte \"Pique-nique au parc\"\n Quand l'utilisateur navigue vers l'écran \"home\"\n Alors l'écran contient le texte \"Pique-nique au parc\"\n\n @e2e\n Scénario: L'événement créé persiste après reconnexion\n Alors l'écran d'accueil contient le texte \"Pique-nique au parc\"\n\n # --- Consultation ---\n\n @e2e\n Scénario: Consulter le détail d'un événement depuis l'accueil\n Quand l'utilisateur clique sur un événement de l'accueil\n Alors l'application affiche l'écran \"event-detail\"\n Et l'écran contient le texte \"Participants\"\n\n # --- Inscription / Désinscription ---\n\n @e2e\n Scénario: S'inscrire à un événement\n Quand l'utilisateur navigue vers l'écran \"events\"\n Et l'utilisateur clique sur un événement de la liste\n Et l'utilisateur attend que l'écran \"event-detail\" soit affiché\n Et l'utilisateur clique sur le bouton \"J'y serai\" si visible\n Alors l'écran contient le texte \"Je participe\"\n\n # ngSet.delete() updates UI but doesn't persist — NG ORM limitation.\n @e2e @wip\n Scénario: Se désinscrire d'un événement\n Quand l'utilisateur navigue vers l'écran \"events\"\n Et l'utilisateur clique sur un événement de la liste\n Et l'utilisateur attend que l'écran \"event-detail\" soit affiché\n Et l'utilisateur clique sur le bouton \"Je participe\"\n Alors l'écran contient le texte \"J'y serai\"\n\n @e2e @wip\n Scénario: La désinscription persiste après reconnexion\n Quand l'utilisateur navigue vers l'écran \"events\"\n Et l'utilisateur clique sur un événement de la liste\n Et l'utilisateur attend que l'écran \"event-detail\" soit affiché\n Alors l'écran contient le texte \"J'y serai\"\n\n # --- Modification ---\n\n @e2e\n Scénario: Modifier un événement et vérifier la persistance\n Quand l'utilisateur navigue vers l'écran \"home\"\n Et l'utilisateur clique sur un événement de l'accueil\n Et l'utilisateur attend que l'écran \"event-detail\" soit affiché\n Et l'utilisateur clique sur le bouton de modification\n Et l'utilisateur attend que l'écran \"update-event\" soit affiché\n Et l'utilisateur modifie le champ lieu avec \"Jardin Public, Bordeaux\"\n Et l'utilisateur clique sur le bouton \"Enregistrer les modifications\"\n Alors l'application affiche l'écran \"event-detail\"\n Et l'écran contient le texte \"Jardin Public\"\n",
|
||||
"screenIds": []
|
||||
},
|
||||
{
|
||||
@@ -748,31 +704,13 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Voir le bouton pour proposer un point de rencontre",
|
||||
"name": "Voir le formulaire de proposition",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Étant donné que ",
|
||||
"text": "je suis sur la page \"points de rencontre\""
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'écran contient un bouton \"Proposer un point de rencontre\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Ouvrir le formulaire de proposition",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Étant donné que ",
|
||||
"text": "je suis sur la page \"points de rencontre\""
|
||||
},
|
||||
{
|
||||
"keyword": "Quand",
|
||||
"text": "je clique sur \"Proposer un point de rencontre\""
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'écran contient un bouton \"Créer le point de rencontre\""
|
||||
@@ -784,34 +722,26 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Définir l'heure de rencontre",
|
||||
"name": "Renseigner les détails du point de rencontre",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Étant donné que ",
|
||||
"text": "je suis sur la page \"points de rencontre\""
|
||||
},
|
||||
{
|
||||
"keyword": "Quand",
|
||||
"text": "je clique sur \"Proposer un point de rencontre\""
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'écran contient un bouton \"30 min avant\""
|
||||
"text": "l'écran contient un champ \"Quand\""
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'écran contient un bouton \"1h avant\""
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'écran contient un bouton \"Personnalisé\""
|
||||
"text": "l'écran contient un champ \"Durée\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"filePath": "src/modules/meeting/features/us-16-point-rencontre.feature",
|
||||
"rawContent": "# language: fr\n@MEETING @priority-1\nFonctionnalité: US-16 Indiquer un ou plusieurs points de rencontre\n En tant qu'utilisateur\n Je peux indiquer un ou plusieurs points de rencontre\n En précisant le lieu et l'heure de cette rencontre\n Afin de croiser et faire connaissance d'autres participants\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder aux points de rencontre\n Étant donné que je suis sur la page \"détail événement\"\n Quand je navigue vers \"points de rencontre\"\n Alors je vois l'écran \"meeting-points\"\n\n Scénario: Voir le bouton pour proposer un point de rencontre\n Étant donné que je suis sur la page \"points de rencontre\"\n Alors l'écran contient un bouton \"Proposer un point de rencontre\"\n\n Scénario: Ouvrir le formulaire de proposition\n Étant donné que je suis sur la page \"points de rencontre\"\n Quand je clique sur \"Proposer un point de rencontre\"\n Alors l'écran contient un bouton \"Créer le point de rencontre\"\n Et l'écran contient un champ \"Lieu\"\n\n Scénario: Définir l'heure de rencontre\n Étant donné que je suis sur la page \"points de rencontre\"\n Quand je clique sur \"Proposer un point de rencontre\"\n Alors l'écran contient un bouton \"30 min avant\"\n Et l'écran contient un bouton \"1h avant\"\n Et l'écran contient un bouton \"Personnalisé\"\n",
|
||||
"rawContent": "# language: fr\n@MEETING @priority-1\nFonctionnalité: US-16 Indiquer un ou plusieurs points de rencontre\n En tant qu'utilisateur\n Je peux indiquer un ou plusieurs points de rencontre\n En précisant le lieu et l'heure de cette rencontre\n Afin de croiser et faire connaissance d'autres participants\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder aux points de rencontre\n Étant donné que je suis sur la page \"détail événement\"\n Quand je navigue vers \"points de rencontre\"\n Alors je vois l'écran \"meeting-points\"\n\n Scénario: Voir le formulaire de proposition\n Étant donné que je suis sur la page \"points de rencontre\"\n Alors l'écran contient un bouton \"Créer le point de rencontre\"\n Et l'écran contient un champ \"Lieu\"\n\n Scénario: Renseigner les détails du point de rencontre\n Étant donné que je suis sur la page \"points de rencontre\"\n Alors l'écran contient un champ \"Quand\"\n Et l'écran contient un champ \"Durée\"\n",
|
||||
"screenIds": [
|
||||
"event-detail",
|
||||
"meeting-points"
|
||||
@@ -984,7 +914,7 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'URL contient \"demo/events\""
|
||||
"text": "l'URL contient \"/events\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -999,62 +929,12 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Le bouton Galerie ramène à la galerie depuis le mode démo",
|
||||
"name": "La liste des événements est peuplée après connexion",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Quand",
|
||||
"text": "l'utilisateur navigue vers l'écran \"home\" sans historique"
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur clique sur le bouton \"Galerie\""
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'application affiche la galerie"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Le bouton \"Charger données de test\" est visible quand connecté",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "la galerie affiche le bouton \"Charger données de test\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Charger les données de test remplit le portefeuille",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Quand",
|
||||
"text": "l'utilisateur clique sur le bouton \"Charger données de test\""
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur attend la fin du chargement"
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'utilisateur navigue vers l'écran \"home\""
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'écran d'accueil affiche des événements"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Les données chargées persistent après reconnexion",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Quand",
|
||||
"text": "l'utilisateur navigue vers l'écran \"home\""
|
||||
"text": "l'utilisateur navigue vers l'écran \"events\""
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
@@ -1064,7 +944,7 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
}
|
||||
],
|
||||
"filePath": "src/modules/auth/features/connexion-nextgraph.feature",
|
||||
"rawContent": "# language: fr\n@AUTH @priority-1\nFonctionnalité: Connexion NextGraph et chargement des données\n En tant qu'utilisateur\n Je peux me connecter à mon portefeuille NextGraph\n Et charger les données de test dans mon portefeuille\n Afin d'utiliser l'application avec mes propres données\n\n # --- UI layer: écran de connexion ---\n\n @ui\n Scénario: L'écran de connexion affiche le bouton NextGraph\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran contient un bouton \"Se connecter avec NextGraph\"\n\n @ui\n Scénario: L'écran de connexion redirige automatiquement quand connecté\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran gère la redirection automatique après connexion\n\n @ui\n Scénario: L'état initial est \"en cours\" quand une connexion est en attente\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran gère l'état de connexion en cours\n\n @ui\n Scénario: Aucune donnée de démonstration n'est visible pendant la connexion\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran n'importe pas de données de démonstration\n\n # --- Data layer: comportement du portefeuille ---\n\n @data\n Scénario: Un portefeuille connecté est vide par défaut\n Alors le portefeuille est connecté\n Et le portefeuille ne contient aucun événement de démonstration\n\n @data\n Scénario: Charger les données de test dans le portefeuille\n Étant donné que le portefeuille est vide\n Quand je charge les données de test\n Alors le portefeuille contient des événements\n Et le portefeuille contient des utilisateurs\n\n @data\n Scénario: Les données de test ne sont pas rechargées si le portefeuille contient déjà des données\n Étant donné que le portefeuille contient déjà des événements\n Quand je charge les données de test\n Alors le nombre d'événements n'a pas changé\n\n @data\n Scénario: Les données du portefeuille sont distinctes des données par défaut\n Étant donné que le portefeuille est vide\n Quand je charge les données de test\n Alors les événements ont des identifiants NextGraph\n Et les utilisateurs ont des identifiants NextGraph\n\n # --- E2E layer: comportement réel dans le navigateur ---\n\n @e2e\n Scénario: L'écran de connexion redirige vers l'accueil si déjà connecté\n Quand l'utilisateur navigue vers l'écran \"login\"\n Alors l'application affiche l'écran \"home\"\n\n @e2e\n Scénario: La navigation interne met à jour l'URL\n Quand l'utilisateur navigue vers l'écran \"events\"\n Alors l'URL contient \"demo/events\"\n\n @e2e\n Scénario: L'application ne redirige pas vers le broker quand elle est dans l'iframe\n Alors l'application est toujours dans l'iframe\n\n @e2e\n Scénario: Le bouton Galerie ramène à la galerie depuis le mode démo\n Quand l'utilisateur navigue vers l'écran \"home\" sans historique\n Et l'utilisateur clique sur le bouton \"Galerie\"\n Alors l'application affiche la galerie\n\n @e2e\n Scénario: Le bouton \"Charger données de test\" est visible quand connecté\n Alors la galerie affiche le bouton \"Charger données de test\"\n\n @e2e\n Scénario: Charger les données de test remplit le portefeuille\n Quand l'utilisateur clique sur le bouton \"Charger données de test\"\n Et l'utilisateur attend la fin du chargement\n Et l'utilisateur navigue vers l'écran \"home\"\n Alors l'écran d'accueil affiche des événements\n\n @e2e\n Scénario: Les données chargées persistent après reconnexion\n Quand l'utilisateur navigue vers l'écran \"home\"\n Alors l'écran d'accueil affiche des événements\n",
|
||||
"rawContent": "# language: fr\n@AUTH @priority-1\nFonctionnalité: Connexion NextGraph et chargement des données\n En tant qu'utilisateur\n Je peux me connecter à mon portefeuille NextGraph\n Et charger les données de test dans mon portefeuille\n Afin d'utiliser l'application avec mes propres données\n\n # --- UI layer: écran de connexion ---\n\n @ui\n Scénario: L'écran de connexion affiche le bouton NextGraph\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran contient un bouton \"Se connecter avec NextGraph\"\n\n @ui @wip\n # Behavioral: requires simulating an NG status change. Better tested at the\n # @e2e layer where a real connected session triggers the redirect.\n Scénario: L'écran de connexion redirige automatiquement quand connecté\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran gère la redirection automatique après connexion\n\n @ui\n Scénario: L'état initial est \"en cours\" quand une connexion est en attente\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran gère l'état de connexion en cours\n\n @ui\n Scénario: Aucune donnée de démonstration n'est visible pendant la connexion\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran n'importe pas de données de démonstration\n\n # --- Data layer: comportement du portefeuille ---\n\n @data\n Scénario: Un portefeuille connecté est vide par défaut\n Alors le portefeuille est connecté\n Et le portefeuille ne contient aucun événement de démonstration\n\n @data\n Scénario: Charger les données de test dans le portefeuille\n Étant donné que le portefeuille est vide\n Quand je charge les données de test\n Alors le portefeuille contient des événements\n Et le portefeuille contient des utilisateurs\n\n @data\n Scénario: Les données de test ne sont pas rechargées si le portefeuille contient déjà des données\n Étant donné que le portefeuille contient déjà des événements\n Quand je charge les données de test\n Alors le nombre d'événements n'a pas changé\n\n @data\n Scénario: Les données du portefeuille sont distinctes des données par défaut\n Étant donné que le portefeuille est vide\n Quand je charge les données de test\n Alors les événements ont des identifiants NextGraph\n Et les utilisateurs ont des identifiants NextGraph\n\n # --- E2E layer: comportement réel dans le navigateur ---\n\n @e2e\n Scénario: L'écran de connexion redirige vers l'accueil si déjà connecté\n Quand l'utilisateur navigue vers l'écran \"login\"\n Alors l'application affiche l'écran \"home\"\n\n @e2e\n Scénario: La navigation interne met à jour l'URL\n Quand l'utilisateur navigue vers l'écran \"events\"\n Alors l'URL contient \"/events\"\n\n @e2e\n Scénario: L'application ne redirige pas vers le broker quand elle est dans l'iframe\n Alors l'application est toujours dans l'iframe\n\n @e2e\n Scénario: La liste des événements est peuplée après connexion\n Quand l'utilisateur navigue vers l'écran \"events\"\n Alors l'écran d'accueil affiche des événements\n",
|
||||
"screenIds": [
|
||||
"login"
|
||||
]
|
||||
@@ -1402,7 +1282,7 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
},
|
||||
{
|
||||
"keyword": "Et",
|
||||
"text": "l'écran contient un texte \"Mes amis\""
|
||||
"text": "l'écran contient un texte \"Mon réseau\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1467,7 +1347,7 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
}
|
||||
],
|
||||
"filePath": "src/modules/user/features/us-20-profil-reseau.feature",
|
||||
"rawContent": "# language: fr\n@USER @priority-1\nFonctionnalité: US-20 Voir le profil des personnes faisant partie de mon réseau\n En tant qu'utilisateur\n Je peux voir le profil des personnes faisant partie de mon réseau\n Ainsi que le profil des personnes publiques\n Et consulter la description de l'événement afin de savoir si je veux participer\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder à mon profil\n Étant donné que je suis sur la page \"accueil\"\n Quand je navigue vers \"mon profil\"\n Alors je vois l'écran \"profile\"\n\n Scénario: Voir mon réseau\n Étant donné que je suis sur la page \"mon profil\"\n Alors l'écran contient un texte \"Amis\"\n Et l'écran contient un texte \"Mes amis\"\n\n Scénario: Voir un profil de mon réseau\n Étant donné que je suis sur la page \"mon profil\"\n Quand je clique sur un participant\n Alors je vois l'écran \"user-profile\"\n\n Scénario: Consulter un événement depuis un profil\n Étant donné que je suis sur la page \"profil utilisateur\"\n Quand je clique sur un événement\n Alors je vois l'écran \"event-detail\"\n\n Scénario: Vérifier les données du profil\n Étant donné que je suis sur la page \"mon profil\"\n Alors l'écran contient un texte \"Événements\"\n Et l'écran contient un texte \"Participations\"\n\n Scénario: Voir les profils publiques\n * Scénario non implémenté\n",
|
||||
"rawContent": "# language: fr\n@USER @priority-1\nFonctionnalité: US-20 Voir le profil des personnes faisant partie de mon réseau\n En tant qu'utilisateur\n Je peux voir le profil des personnes faisant partie de mon réseau\n Ainsi que le profil des personnes publiques\n Et consulter la description de l'événement afin de savoir si je veux participer\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder à mon profil\n Étant donné que je suis sur la page \"accueil\"\n Quand je navigue vers \"mon profil\"\n Alors je vois l'écran \"profile\"\n\n Scénario: Voir mon réseau\n Étant donné que je suis sur la page \"mon profil\"\n Alors l'écran contient un texte \"Amis\"\n Et l'écran contient un texte \"Mon réseau\"\n\n Scénario: Voir un profil de mon réseau\n Étant donné que je suis sur la page \"mon profil\"\n Quand je clique sur un participant\n Alors je vois l'écran \"user-profile\"\n\n Scénario: Consulter un événement depuis un profil\n Étant donné que je suis sur la page \"profil utilisateur\"\n Quand je clique sur un événement\n Alors je vois l'écran \"event-detail\"\n\n Scénario: Vérifier les données du profil\n Étant donné que je suis sur la page \"mon profil\"\n Alors l'écran contient un texte \"Événements\"\n Et l'écran contient un texte \"Participations\"\n\n Scénario: Voir les profils publiques\n * Scénario non implémenté\n",
|
||||
"screenIds": [
|
||||
"event-detail",
|
||||
"home",
|
||||
@@ -1475,6 +1355,48 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
"user-profile"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "read-filter",
|
||||
"name": "Filtre ReadCap (ng-eventually)",
|
||||
"description": "En tant que développeur Je veux valider, contre le vrai broker, que le filtre de lecture de la lib applique les ReadCap au niveau du DOCUMENT (le repo où vit chaque item) sur le vrai set réactif de l'ORM : on ne voit un document que si on détient sa ReadCap. En mono-store (tout dans un seul repo) c'est donc tout-ou-rien sur ce document — le comportement fidèle de NextGraph.",
|
||||
"tags": [
|
||||
"@WORKSHOP",
|
||||
"@priority-1"
|
||||
],
|
||||
"category": "WORKSHOP",
|
||||
"priority": 1,
|
||||
"scenarios": [
|
||||
{
|
||||
"name": "On ne voit un document que si on détient sa ReadCap",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Étant donné",
|
||||
"text": "le wallet contient des participations dans un document"
|
||||
},
|
||||
{
|
||||
"keyword": "Quand",
|
||||
"text": "je gouverne ce document par une ReadCap accordée à un autre utilisateur"
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'utilisateur courant ne voit aucune participation de ce document"
|
||||
},
|
||||
{
|
||||
"keyword": "Quand",
|
||||
"text": "l'utilisateur courant obtient la ReadCap du document"
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "il voit toutes les participations du document"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"filePath": "src/modules/workshop/features/read-filter.feature",
|
||||
"rawContent": "# language: fr\n@WORKSHOP @priority-1\nFonctionnalité: Filtre ReadCap (ng-eventually)\n En tant que développeur\n Je veux valider, contre le vrai broker, que le filtre de lecture de la lib\n applique les ReadCap au niveau du DOCUMENT (le repo où vit chaque item) sur le\n vrai set réactif de l'ORM : on ne voit un document que si on détient sa ReadCap.\n En mono-store (tout dans un seul repo) c'est donc tout-ou-rien sur ce document\n — le comportement fidèle de NextGraph.\n\n @data\n Scénario: On ne voit un document que si on détient sa ReadCap\n Étant donné le wallet contient des participations dans un document\n Quand je gouverne ce document par une ReadCap accordée à un autre utilisateur\n Alors l'utilisateur courant ne voit aucune participation de ce document\n Quand l'utilisateur courant obtient la ReadCap du document\n Alors il voit toutes les participations du document\n",
|
||||
"screenIds": []
|
||||
},
|
||||
{
|
||||
"id": "us-19",
|
||||
"name": "US-19 Recevoir un récapitulatif des prochaines rencontres",
|
||||
@@ -1502,7 +1424,7 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'écran contient une section \"Mes événements à venir\""
|
||||
"text": "l'écran contient une section \"À venir\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1546,7 +1468,7 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
}
|
||||
],
|
||||
"filePath": "src/modules/notification/features/us-19-recapitulatif.feature",
|
||||
"rawContent": "# language: fr\n# Note: US-19 concerne les récapitulatifs par email - non testable via écrans\n# Les scénarios ci-dessous testent l'affichage sur l'écran d'accueil (aspect UI)\n@NOTIF @priority-2\nFonctionnalité: US-19 Recevoir un récapitulatif des prochaines rencontres\n En tant qu'utilisateur\n Je peux recevoir un récapitulatif des prochaines rencontres\n En réceptionnant une liste des événements auxquels je suis inscrit ou qui sont proches de chez moi\n Afin d'établir un programme des événements auxquels je participe par période\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Voir les événements à venir sur l'accueil\n Étant donné que je suis sur la page \"accueil\"\n Alors l'écran contient une section \"Mes événements à venir\"\n\n Scénario: Voir le récapitulatif par période\n * Scénario non implémenté\n\n Scénario: Voir les événements proches géographiquement\n * Scénario non implémenté\n\n Scénario: Voir mes inscriptions\n Étant donné que je suis sur la page \"mon profil\"\n Alors l'écran contient une section \"Mes événements à venir\"\n\n Scénario: Vérifier les données de l'accueil\n Étant donné que je suis sur la page \"accueil\"\n Alors les événements affichent leur lieu\n",
|
||||
"rawContent": "# language: fr\n# Note: US-19 concerne les récapitulatifs par email - non testable via écrans\n# Les scénarios ci-dessous testent l'affichage sur l'écran d'accueil (aspect UI)\n@NOTIF @priority-2\nFonctionnalité: US-19 Recevoir un récapitulatif des prochaines rencontres\n En tant qu'utilisateur\n Je peux recevoir un récapitulatif des prochaines rencontres\n En réceptionnant une liste des événements auxquels je suis inscrit ou qui sont proches de chez moi\n Afin d'établir un programme des événements auxquels je participe par période\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Voir les événements à venir sur l'accueil\n Étant donné que je suis sur la page \"accueil\"\n Alors l'écran contient une section \"À venir\"\n\n Scénario: Voir le récapitulatif par période\n * Scénario non implémenté\n\n Scénario: Voir les événements proches géographiquement\n * Scénario non implémenté\n\n Scénario: Voir mes inscriptions\n Étant donné que je suis sur la page \"mon profil\"\n Alors l'écran contient une section \"Mes événements à venir\"\n\n Scénario: Vérifier les données de l'accueil\n Étant donné que je suis sur la page \"accueil\"\n Alors les événements affichent leur lieu\n",
|
||||
"screenIds": [
|
||||
"home",
|
||||
"profile"
|
||||
@@ -1712,7 +1634,7 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
{
|
||||
"id": "us-26",
|
||||
"name": "US-26 Définir la portée d'un événement",
|
||||
"description": "En tant qu'utilisateur Je peux relayer/présenter le contenu d'un événement et le catégoriser par type/thématique En indiquant son rayon d'intérêt en kilomètres Afin de m'assurer que les utilisateurs qui habitent trop loin ne reçoivent pas de notification",
|
||||
"description": "En tant qu'utilisateur Je peux relayer/présenter le contenu d'un événement En indiquant son rayon d'intérêt en kilomètres Afin de m'assurer que les utilisateurs qui habitent trop loin ne reçoivent pas de notification",
|
||||
"tags": [
|
||||
"@USER",
|
||||
"@priority-2"
|
||||
@@ -1749,20 +1671,6 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
"tags": [],
|
||||
"steps": []
|
||||
},
|
||||
{
|
||||
"name": "Choisir une thématique",
|
||||
"tags": [],
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Étant donné que ",
|
||||
"text": "je suis sur la page \"relayer un événement\""
|
||||
},
|
||||
{
|
||||
"keyword": "Alors",
|
||||
"text": "l'écran contient une section \"Thématique\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Vérifier les champs obligatoires",
|
||||
"tags": [],
|
||||
@@ -1779,7 +1687,7 @@ export const parsedFeatures: ParsedFeature[] = [
|
||||
}
|
||||
],
|
||||
"filePath": "src/modules/user/features/us-26-portee-evenement.feature",
|
||||
"rawContent": "# language: fr\n@USER @priority-2\nFonctionnalité: US-26 Définir la portée d'un événement\n En tant qu'utilisateur\n Je peux relayer/présenter le contenu d'un événement et le catégoriser par type/thématique\n En indiquant son rayon d'intérêt en kilomètres\n Afin de m'assurer que les utilisateurs qui habitent trop loin ne reçoivent pas de notification\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder au formulaire de relai d'événement\n Étant donné que je suis sur la page \"accueil\"\n Quand je navigue vers \"relayer un événement\"\n Alors je vois l'écran \"create-event\"\n\n Scénario: Définir le rayon d'intérêt\n * Scénario non implémenté\n\n Scénario: Choisir une thématique\n Étant donné que je suis sur la page \"relayer un événement\"\n Alors l'écran contient une section \"Thématique\"\n\n Scénario: Vérifier les champs obligatoires\n Étant donné que l'écran \"create-event\" est affiché\n Alors le formulaire contient les champs obligatoires suivants:\n | Nom de l'événement |\n | Date de début |\n | Heure de début |\n | Lieu |\n | Thématique |\n",
|
||||
"rawContent": "# language: fr\n@USER @priority-2\nFonctionnalité: US-26 Définir la portée d'un événement\n En tant qu'utilisateur\n Je peux relayer/présenter le contenu d'un événement\n En indiquant son rayon d'intérêt en kilomètres\n Afin de m'assurer que les utilisateurs qui habitent trop loin ne reçoivent pas de notification\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder au formulaire de relai d'événement\n Étant donné que je suis sur la page \"accueil\"\n Quand je navigue vers \"relayer un événement\"\n Alors je vois l'écran \"create-event\"\n\n Scénario: Définir le rayon d'intérêt\n * Scénario non implémenté\n\n Scénario: Vérifier les champs obligatoires\n Étant donné que l'écran \"create-event\" est affiché\n Alors le formulaire contient les champs obligatoires suivants:\n | Nom de l'événement |\n | Date de début |\n",
|
||||
"screenIds": [
|
||||
"create-event",
|
||||
"home"
|
||||
|
||||
@@ -12,7 +12,7 @@ import { createRoot } from 'react-dom/client';
|
||||
import { NextGraphProvider, useNextGraph } from '../context/NextGraphContext';
|
||||
import { FestipodDataProvider, useFestipodData } from '../context/FestipodDataContext';
|
||||
import { useShape } from '@ng-eventually/client';
|
||||
import { setGrantOf, setCurrentUser } from '@ng-eventually/client/polyfill';
|
||||
import { getCaps, setCurrentUser, resetCaps } from '@ng-eventually/client/polyfill';
|
||||
import type { DeepSignalSet } from '@ng-eventually/client';
|
||||
import {
|
||||
FpEventShapeType,
|
||||
@@ -68,8 +68,9 @@ function ConnectedHarness() {
|
||||
const participations = useShape(FpParticipationShapeType, privateNuri) as DeepSignalSet<FpParticipation>;
|
||||
|
||||
const [bridgeReady, setBridgeReady] = useState(false);
|
||||
// Read-filter validation: when set, <FilterProbe> mounts a filtered useShape.
|
||||
const [filterUser, setFilterUser] = useState<string | null>(null);
|
||||
// Read-filter validation: once a ReadCap policy is active, <FilterProbe> mounts
|
||||
// a useShape that returns the read-filtered VIEW.
|
||||
const [filterActive, setFilterActive] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Small delay for useShape to populate
|
||||
@@ -150,17 +151,26 @@ function ConnectedHarness() {
|
||||
return bootstrapWallet(events as any, users as any, participations as any);
|
||||
},
|
||||
|
||||
/** The document (repo NURI) all wallet entities live in (mono-store). */
|
||||
documentNuri: privateNuri,
|
||||
|
||||
/**
|
||||
* Enable the lib's READ FILTER on the real ORM set: each participation is
|
||||
* granted to its own `user`, and the current user is `user`. <FilterProbe>
|
||||
* then exposes window.__readFilter with the filtered participations.
|
||||
* Put the wallet document under a ReadCap policy: grant its read cap to
|
||||
* `reader` only, and set the current user to `user`. The lib's read
|
||||
* filter is per-DOCUMENT, so this is all-or-nothing on that document —
|
||||
* the faithful NextGraph behavior in a mono-store layout. <FilterProbe>
|
||||
* then exposes window.__readFilter.snapshot() over the filtered view.
|
||||
*/
|
||||
setupReadFilter(user: string) {
|
||||
setGrantOf((item: any) =>
|
||||
item && item.user ? { read: [item.user], write: [item.user] } : undefined,
|
||||
);
|
||||
governDocument(reader: string, user: string) {
|
||||
resetCaps();
|
||||
getCaps().grantRead(privateNuri!, reader);
|
||||
setCurrentUser(user);
|
||||
setFilterActive(true);
|
||||
},
|
||||
|
||||
/** Switch the current user (does the user now hold the document's cap?). */
|
||||
setUser(user: string) {
|
||||
setCurrentUser(user);
|
||||
setFilterUser(user);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -176,15 +186,16 @@ function ConnectedHarness() {
|
||||
return (
|
||||
<>
|
||||
<div id="harness-status">{bridgeReady ? 'READY' : 'LOADING_SHAPES'}</div>
|
||||
{filterUser && privateNuri && <FilterProbe privateNuri={privateNuri} />}
|
||||
{filterActive && privateNuri && <FilterProbe privateNuri={privateNuri} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FilterProbe — subscribes participations AFTER the read filter is enabled, so
|
||||
// useShape returns a filtered view. Exposes window.__readFilter for the @data
|
||||
// scenario validating the read filter on the real ORM set.
|
||||
// FilterProbe — subscribes participations AFTER a ReadCap policy is active, so
|
||||
// useShape returns the read-filtered VIEW. Exposes window.__readFilter.snapshot()
|
||||
// (evaluated lazily → reflects the CURRENT user) for the @data scenario that
|
||||
// validates the per-document read filter on the real ORM set.
|
||||
// ============================================================================
|
||||
|
||||
function FilterProbe({ privateNuri }: { privateNuri: string }) {
|
||||
@@ -192,8 +203,9 @@ function FilterProbe({ privateNuri }: { privateNuri: string }) {
|
||||
useEffect(() => {
|
||||
(window as any).__readFilter = {
|
||||
ready: true,
|
||||
count: set.size,
|
||||
users: [...set].map(p => p.user),
|
||||
// Lazy: the filtered view reads the current user at access time, so calling
|
||||
// snapshot() after setUser() reflects the new cap holder without remount.
|
||||
snapshot: () => ({ count: set.size, users: [...set].map(p => p.user) }),
|
||||
};
|
||||
}, [set]);
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user