// ============================================================================ // Baseline QA E2E · smoke + brand · endpoints Sprint 1 // ---------------------------------------------------------------------------- // Livrable Sprint 1 (roadmap §Sprint 1 · GAP_ANALYSIS §7 · QA Agent). // Data-driven sur tests/e2e/routes.json → /waf-home /crm /qa /choisir-mon-unite. // // Philosophie « baseline » : on vérifie la SANTÉ et le BRAND, jamais le contenu // métier (prix/superficie/dispo) — contrainte #6 (zéro invention de chiffres). // Exécution réelle : sur le runner Gitea VPS avec DTP_BASE_URL défini // (voir tests/README.md). Autorable/validable en-repo, run différé côté VPS. // ============================================================================ import { test, expect } from '@playwright/test'; import routesManifest from './routes.json'; import { LUXURY, isReachable, looksLikeLoginGate, collectStyleSurface, containsAny, type RoutesManifest, } from './_shared/contract'; const { routes } = routesManifest as RoutesManifest; for (const route of routes) { test.describe(`${route.path} · ${route.name}`, () => { test('répond sans erreur serveur (status < 400)', async ({ page }) => { const res = await page.goto(route.path, { waitUntil: 'domcontentloaded' }); expect(res, `aucune réponse HTTP pour ${route.path}`).not.toBeNull(); expect( isReachable(res), `${route.path} → status ${res?.status()} (attendu < 400)`, ).toBeTruthy(); }); test('sert une page HTML titrée avec attribut lang', async ({ page }) => { await page.goto(route.path, { waitUntil: 'domcontentloaded' }); const title = (await page.title()).trim(); expect(title.length, `titre vide sur ${route.path}`).toBeGreaterThan(0); const lang = await page.locator('html').getAttribute('lang'); // Multilingue FR/EN/ES attendu — on vérifie la présence de l'attribut, // pas une valeur précise (routes servies dans plusieurs langues). expect(lang, `attribut absent sur ${route.path}`).toBeTruthy(); }); test('respecte le brand luxury dark+doré (tokens présents)', async ({ page }) => { const res = await page.goto(route.path, { waitUntil: 'networkidle' }); const bodyText = await page.locator('body').innerText().catch(() => ''); // Route protégée non authentifiée : le brand du mur de login n'est pas // représentatif → on annote et on ne bloque pas (baseline tolérante). if (route.gated && looksLikeLoginGate(page.url(), bodyText)) { test.info().annotations.push({ type: 'gate', description: `${route.path} sert un login (non authentifié) — brand non asserté.`, }); test.skip(true, 'Mur de login — brand vérifié une fois authentifié (couverture ultérieure).'); } expect(isReachable(res)).toBeTruthy(); const surface = await collectStyleSurface(page); const hasColor = containsAny(surface, LUXURY.colors); const hasFont = containsAny(surface, LUXURY.fonts); expect( hasColor, `aucune couleur de marque (${LUXURY.colors.join(' / ')}) trouvée sur ${route.path}`, ).toBeTruthy(); expect( hasFont, `aucune police de marque (${LUXURY.fonts.join(' / ')}) trouvée sur ${route.path}`, ).toBeTruthy(); }); test('ne produit ni erreur JS non capturée ni réponse 5xx', async ({ page }) => { const pageErrors: string[] = []; const serverErrors: string[] = []; page.on('pageerror', (err) => pageErrors.push(err.message)); page.on('response', (r) => { if (r.status() >= 500) serverErrors.push(`${r.status()} ${r.url()}`); }); await page.goto(route.path, { waitUntil: 'networkidle' }); expect( pageErrors, `erreurs JS non capturées sur ${route.path}:\n${pageErrors.join('\n')}`, ).toEqual([]); expect( serverErrors, `réponses 5xx sur ${route.path}:\n${serverErrors.join('\n')}`, ).toEqual([]); }); }); }