fix(auth): land on home after gate entry; align the @humain e2e to the identifier flow

Removing ConnexionScreen dropped its post-login navigate('/home'). Since the
identifier is now entered at the barrier (before the broker round-trip), on return
the app can load at '/' (WelcomeScreen) with a session already open. AuthGate now
redirects welcome→/home once connected AND identified (gate-disabled paths, i.e.
@e2e/@data harness, are exempt).

Update the @humain assisted-import e2e (the real staging flow, the coverage for
this page) to the new UX: the tester types an identifier then clicks « Entrer »
(one act), and lands directly on home — the 'choisir un nom d'utilisateur'
(ConnexionScreen) steps are removed. Step bindings verified; tsc + build green.
Doctrine: knowledge_multibrowser-harness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-06 15:34:01 +02:00
parent 25b1c033d9
commit 17543f04c3
4 changed files with 23 additions and 24 deletions
@@ -38,7 +38,7 @@ Ne **pas** confondre `@multibrowser` (plusieurs navigateurs) avec `@shared-walle
## Parcours humain — e2e du mécanisme produit (vert)
Scénario `@humain` : valide le flux RÉEL de distribution du wallet **de bout en bout, via la vraie app**, pas l'injection de test. Un navigateur vierge ouvre l'app staging → l'`AccessGateScreen` propose le **fichier** + le **mot de passe** → on télécharge le fichier **depuis l'écran**, on vérifie que le mot de passe affiché **égale** celui du wallet → import sur `nextgraph.eu` « Import a Wallet File » → retour → clic « Entrer » → app connectée (`ConnexionScreen`).
Scénario `@humain` : valide le flux RÉEL de distribution du wallet **de bout en bout, via la vraie app**, pas l'injection de test. Un navigateur vierge ouvre l'app staging → l'`AccessGateScreen` propose le **fichier** + le **mot de passe** → on télécharge le fichier **depuis l'écran**, on vérifie que le mot de passe affiché **égale** celui du wallet → import sur `nextgraph.eu` « Import a Wallet File » → retour → on **saisit un identifiant** puis clic « Entrer » (nommer l'espace et ouvrir le wallet = un seul acte, cf. concept `app-security` [[decision_2026-07-06_identifier-at-access-barrier]]) → app connectée, arrivée directe sur l'accueil (plus d'écran « nom d'utilisateur » séparé).
- **Wallet e2e** : un fichier `.ngw` (`festipod-e2e-tests`, mot de passe = identifiant) placé **à la racine du worktree** ; `findE2eWalletFile()` le localise (`*.ngw`). Gitignoré → chaque environnement doit l'ajouter (sinon erreur claire).
- `pool.ensureStagingApp()` (`hooks.ts`) — build **isolé** `bun run build.ts --outdir=dist-staging` (barrière d'accès **ON par défaut** ; mot de passe gravé + **fichier copié** en `/shared-wallet.ngw`, cf. `build.ts`), servi statiquement. Mémoïsé, lazy (seul `@humain` le paie).
+14 -1
View File
@@ -13,10 +13,11 @@
* screens, not the auth flow). Absent → gate ON.
*/
import type { ReactNode } from 'react';
import { useEffect, type ReactNode } from 'react';
import { useNextGraph } from '../shared/context/NextGraphContext';
import { useAccount } from '../shared/context/AccountContext';
import { AccessGateScreen } from '../modules/auth/screens/AccessGateScreen';
import { useRouter, useNavigate } from './router';
declare global {
// eslint-disable-next-line no-var
@@ -27,6 +28,18 @@ const GATE_DISABLED = globalThis.__FESTIPOD_ACCESS_GATE_DISABLED__ === true;
export function AuthGate({ children }: { children: ReactNode }) {
const { status, error, connect } = useNextGraph();
const { username, login } = useAccount();
const { route } = useRouter();
const navigate = useNavigate();
// Once connected AND identified, leave the disconnected welcome screen for the
// app home. The identifier is now set at the barrier (before the broker
// round-trip), so on return the app can land on '/' with a session already
// open; the removed ConnexionScreen used to do this navigate on login.
useEffect(() => {
if (!GATE_DISABLED && status === 'connected' && username && route.page === 'welcome') {
navigate('/home');
}
}, [status, username, route.page, navigate]);
// Gate explicitly disabled (no-gate build / @e2e harness) → straight to app.
if (GATE_DISABLED) {
@@ -69,7 +69,5 @@ Fonctionnalité: Harness multi-navigateur — modèles private-wallet et shared-
Étant donné un nouveau testeur ouvre Festipod en staging sur un navigateur vierge
Alors Festipod affiche l'écran d'accès avec le portefeuille à télécharger
Quand le testeur télécharge le portefeuille et l'importe sur nextgraph.eu
Et le testeur revient sur Festipod et clique « Entrer »
Alors Festipod est connecté et propose de choisir un nom d'utilisateur
Quand le testeur choisit un nom d'utilisateur
Et le testeur revient sur Festipod, saisit un identifiant et clique « Entrer »
Alors il arrive sur l'accueil de l'application
@@ -57,34 +57,22 @@ When('le testeur télécharge le portefeuille et l\'importe sur nextgraph.eu', a
await pool.importWalletViaFile(page, filePath!, (this as any).displayedPassword);
});
When('le testeur revient sur Festipod et clique « Entrer »', async function (this: FestipodWorld) {
When('le testeur revient sur Festipod, saisit un identifiant et clique « Entrer »', async function (this: FestipodWorld) {
const handle = this.browser('H');
await handle.page.goto((this as any).stagingUrl, { waitUntil: 'domcontentloaded' });
// Saisit son identifiant (il nomme l'espace virtuel) — « Entrer » reste désactivé
// tant qu'il est vide. Naming the space and opening the wallet are one act.
const idf = handle.page.locator('[data-testid=identifier-input]');
await idf.waitFor({ state: 'visible', timeout: 15000 });
await idf.fill('testeur');
const entrer = handle.page.getByText('Entrer', { exact: true });
await entrer.waitFor({ state: 'visible', timeout: 15000 });
await entrer.click(); // déclenche le redirect vers le broker
await entrer.click(); // enregistre l'identifiant PUIS déclenche le redirect broker
await handle.page.waitForURL('**nextgraph**', { timeout: 20000 }).catch(() => {});
// Le broker demande de déverrouiller le wallet fraîchement importé → son mot de
// passe (completeBrokerLogin attend la page de login wallet de façon robuste).
handle.appFrame = await pool.completeBrokerLogin(handle.page, (this as any).stagingUrl, pool.sharedWalletPassword);
});
Then('Festipod est connecté et propose de choisir un nom d\'utilisateur', async function (this: FestipodWorld) {
const handle = this.browser('H');
expect(handle.appFrame, 'l\'app doit être chargée dans l\'iframe broker').to.not.equal(null);
// Connecté → AuthGate passe l'AccessGateScreen et montre ConnexionScreen.
await handle.appFrame!.waitForFunction(
() => /Choisissez votre nom d'utilisateur|Connexion/.test(document.body?.innerText ?? ''),
{ timeout: 30000 },
);
});
When('le testeur choisit un nom d\'utilisateur', async function (this: FestipodWorld) {
const frame = this.browser('H').appFrame!;
await frame.locator('input[placeholder="@votrepseudo"]').fill('@testeur');
await frame.getByRole('button', { name: 'Se connecter' }).click();
});
Then('il arrive sur l\'accueil de l\'application', async function (this: FestipodWorld) {
const frame = this.browser('H').appFrame!;
// On atterrit sur /home (l'app), PAS sur l'onboarding hors-connexion ('/'