test(e2e): smoke @smoke garde la classe "page blanche une fois connecté"
Boote le VRAI App via le broker (hook Before @e2e existant), navigue vers l'accueil connecté et asserte deux choses fortes : HomeScreen a réellement monté (.app-navbar + bouton "Relayer", absents d'un spinner/bandeau broker) ET aucune erreur runtime (pageerror/console.error) n'a été émise pendant le boot connecté. Le World collecte désormais les pageErrors (réinitialisés par scénario, logging existant préservé). Câblé dans `bun run validate` (run par défaut) via @smoke and not @wip, avec nettoyage Chromium. Preuve: un throw dans HomeScreen fait virer le smoke au rouge; sans lui, vert. Comble le trou qui laissait passer la régression page-blanche (aucune suite n'exécutait @e2e et aucune assertion ne gardait le rendu connecté). Doctrine: bdd-testing/knowledge_e2e-layer documente le smoke @smoke + pageErrors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# language: fr
|
||||
#
|
||||
# SMOKE « crash de rendu connecté / page blanche ».
|
||||
#
|
||||
# Régression gardée : après connexion au broker, seul le bandeau de l'iframe
|
||||
# s'affichait et le vrai App ne rendait rien (page blanche). Aucun test
|
||||
# n'attrapait cette CLASSE de bug — un crash de rendu qui ne survient QUE une
|
||||
# fois l'app connectée et montée sur des données réelles.
|
||||
#
|
||||
# Ce smoke boote le VRAI App (serveur src/index.ts, pas le harness __testData),
|
||||
# se connecte au broker (fourni par le hook Before @e2e), navigue vers l'accueil
|
||||
# connecté (HomeScreen, qui consomme useFestipodData) et vérifie DEUX choses :
|
||||
# 1. l'accueil rend un élément d'app significatif (barre de navigation + bouton
|
||||
# « Relayer »), preuve que HomeScreen a monté — pas juste un spinner ou le
|
||||
# bandeau du broker ;
|
||||
# 2. AUCUNE erreur runtime (pageerror / console.error) n'a été émise pendant
|
||||
# le boot connecté — un throw dans un composant/provider monté après
|
||||
# connexion vire le smoke au rouge.
|
||||
#
|
||||
# Tout futur crash de rendu connecté = ce smoke passe au rouge.
|
||||
|
||||
@e2e @smoke
|
||||
Fonctionnalité: L'accueil connecté rend du contenu réel
|
||||
En tant qu'utilisateur qui vient de se connecter
|
||||
Je veux que l'écran d'accueil rende réellement l'application
|
||||
Afin de ne jamais retomber sur une page blanche une fois connecté
|
||||
|
||||
Scénario: L'accueil rend du contenu réel sans erreur après connexion
|
||||
Quand l'application connectée affiche l'accueil
|
||||
Alors l'accueil rend un contenu d'application réel
|
||||
Et aucune erreur runtime n'a été émise pendant le boot connecté
|
||||
@@ -0,0 +1,75 @@
|
||||
import { When, Then } from '@cucumber/cucumber';
|
||||
import { expect } from 'chai';
|
||||
import type { FestipodWorld } from '../../../../shared/support/world';
|
||||
|
||||
// --- Smoke « crash de rendu connecté / page blanche » ---
|
||||
//
|
||||
// Le hook Before @e2e a déjà : booté le VRAI App via le broker (this.appFrame),
|
||||
// attendu que #root ait du contenu, et laissé 3s aux providers pour se
|
||||
// stabiliser. On ne re-boote donc PAS l'app ici — on réutilise this.appFrame.
|
||||
//
|
||||
// Ces steps prouvent que l'App a réellement MONTÉ l'accueil connecté (HomeScreen,
|
||||
// qui consomme useFestipodData), pas juste un spinner ou le bandeau du broker.
|
||||
|
||||
When("l'application connectée affiche l'accueil", async function (this: FestipodWorld) {
|
||||
// Un utilisateur connecté qui atterrit sur '/' est redirigé vers '/home' par
|
||||
// WelcomeScreen. On navigue explicitement pour rendre le smoke déterministe
|
||||
// quel que soit l'état de la redirection au moment du boot.
|
||||
await this.appFrame!.evaluate(() => {
|
||||
window.history.pushState(null, '', '/home');
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
});
|
||||
// Laisse le routeur et HomeScreen (re)monter.
|
||||
await this.appFrame!.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
Then("l'accueil rend un contenu d'application réel", async function (this: FestipodWorld) {
|
||||
// Marqueurs FORTS et propres à HomeScreen (absents de WelcomeScreen / d'un
|
||||
// simple spinner / du bandeau broker) :
|
||||
// - .app-navbar : la barre de navigation basse (BottomNav) — rendue par
|
||||
// HomeScreen, pas par l'écran d'onboarding ;
|
||||
// - le bouton « Relayer » (aria-label="Relayer un événement") propre à
|
||||
// l'en-tête de l'accueil.
|
||||
// Si un throw dans HomeScreen (ou un provider monté après connexion) blanchit
|
||||
// le rendu, React démonte l'arbre (aucun ErrorBoundary) et ces marqueurs
|
||||
// disparaissent → l'attente échoue.
|
||||
const rendered = await this.appFrame!.waitForFunction(
|
||||
() => {
|
||||
const root = document.getElementById('root');
|
||||
if (!root) return false;
|
||||
const hasNavbar = document.querySelector('.app-navbar') !== null;
|
||||
const hasRelayer =
|
||||
document.querySelector('[aria-label="Relayer un événement"]') !== null;
|
||||
return hasNavbar && hasRelayer;
|
||||
},
|
||||
{ timeout: 15000 },
|
||||
).then(() => true).catch(() => false);
|
||||
|
||||
if (!rendered) {
|
||||
const debug = await this.appFrame!.evaluate(() => ({
|
||||
pathname: window.location.pathname,
|
||||
hasNavbar: document.querySelector('.app-navbar') !== null,
|
||||
hasRelayer: document.querySelector('[aria-label="Relayer un événement"]') !== null,
|
||||
rootLen: document.getElementById('root')?.innerHTML.length ?? 0,
|
||||
rootText: document.getElementById('root')?.textContent?.substring(0, 400),
|
||||
}));
|
||||
expect.fail(
|
||||
`L'accueil connecté n'a pas rendu de contenu d'app réel (page blanche ?). ` +
|
||||
`path="${debug.pathname}", .app-navbar=${debug.hasNavbar}, ` +
|
||||
`bouton Relayer=${debug.hasRelayer}, #root length=${debug.rootLen}, ` +
|
||||
`texte: "${debug.rootText}"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Then('aucune erreur runtime n\'a été émise pendant le boot connecté', function (this: FestipodWorld) {
|
||||
// this.pageErrors est peuplé par le hook Before (pageerror + console.error de
|
||||
// la page app), réinitialisé à chaque scénario. Un crash de rendu connecté
|
||||
// (throw non attrapé dans un composant/provider) émet un `pageerror` et
|
||||
// atterrit ici → assertion rouge avec la liste exacte.
|
||||
expect(
|
||||
this.pageErrors,
|
||||
`Des erreurs runtime ont été émises pendant le boot connecté :\n` +
|
||||
this.pageErrors.map((e, i) => ` [${i + 1}] ${e}`).join('\n'),
|
||||
).to.be.empty;
|
||||
});
|
||||
@@ -547,6 +547,7 @@ Before({ timeout: 60000 }, async function (this: FestipodWorld, scenario) {
|
||||
this.isAuthenticated = false;
|
||||
this.screenSourceContent = '';
|
||||
this.currentScreen = null;
|
||||
this.pageErrors = [];
|
||||
|
||||
// Multi-browser scenarios drive their own isolated browsers via steps
|
||||
// (this.openBrowser). They must NOT get the legacy single shared page.
|
||||
@@ -593,10 +594,19 @@ Before({ timeout: 60000 }, async function (this: FestipodWorld, scenario) {
|
||||
try { window.localStorage.setItem('festipod.account.username', u); } catch { /* opaque origin */ }
|
||||
}, freshUser);
|
||||
|
||||
// Capture console for debugging
|
||||
this.page.on('pageerror', (err) => console.error('[Browser error]', err.message));
|
||||
// Capture console for debugging AND collect into the World so smoke
|
||||
// scenarios can assert no runtime error was emitted during the connected
|
||||
// boot (guards the "page blanche once connected" render-crash class).
|
||||
const world = this;
|
||||
this.page.on('pageerror', (err) => {
|
||||
console.error('[Browser error]', err.message);
|
||||
world.pageErrors.push(`pageerror: ${err.message}`);
|
||||
});
|
||||
this.page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') console.error('[Browser console]', msg.text());
|
||||
if (msg.type() === 'error') {
|
||||
console.error('[Browser console]', msg.text());
|
||||
world.pageErrors.push(`console.error: ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,12 @@ export interface FestipodWorld extends World {
|
||||
page: Page | null;
|
||||
appFrame: Frame | null;
|
||||
|
||||
// Runtime errors emitted by the app page during the scenario (uncaught
|
||||
// `pageerror` + console.error). Collected by the Before hook so smoke
|
||||
// scenarios can assert the connected app booted WITHOUT crashing the render
|
||||
// (guards the "page blanche once connected" class of bug). Reset per scenario.
|
||||
pageErrors: string[];
|
||||
|
||||
// Multi-browser (named, isolated contexts) — for cross-browser wallet tests.
|
||||
// The wallet model (own vs shared) is an axis orthogonal to browser count.
|
||||
browsers: Map<string, NamedBrowser>;
|
||||
@@ -239,6 +245,9 @@ class CustomWorld extends World implements FestipodWorld {
|
||||
page: Page | null = null;
|
||||
appFrame: Frame | null = null;
|
||||
|
||||
// Runtime errors emitted by the app page during the scenario (see interface).
|
||||
pageErrors: string[] = [];
|
||||
|
||||
// Multi-browser (named, isolated contexts)
|
||||
browsers: Map<string, NamedBrowser> = new Map();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user