1a87b22fd7
Livrable QA Sprint 1 (roadmap §Sprint 1 · GAP_ANALYSIS §7) : projet Playwright auto-contenu sous tests/, data-driven sur e2e/routes.json pour /waf-home /crm /qa /choisir-mon-unite. 4 contrôles/route : status<400 · HTML titré+lang · brand luxury (#0a0a12/#f0b429 + Fraunces/Cormorant) · zéro erreur JS/5xx. Cible via DTP_BASE_URL (aucune URL codée en dur). Zéro invention de chiffres (#6). - tests/{playwright.config.ts,package.json,tsconfig.json,.gitignore,README.md} - tests/e2e/{routes.json,smoke.spec.ts,_shared/contract.ts} - CI : job e2e-baseline manuel (workflow_dispatch) dans .gitea/workflows/ci.yml — hors gate push/PR (exige serveur live), Gitea Actions only (#2) - Fix : gate CI rouge sur HEAD (guard flaguait sa propre doc de test négatif) → escape hatch documenté ci-allow. Les 3 scripts du gate repassent verts. - GAP_ANALYSIS §7 : critère Baseline Playwright → ✅ (exécution VPS différée) - daily report 2026-07-30 (session 2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
3.0 KiB
TypeScript
77 lines
3.0 KiB
TypeScript
// ============================================================================
|
|
// Contrat de baseline QA · tokens de marque + helpers partagés
|
|
// ----------------------------------------------------------------------------
|
|
// Ces constantes matérialisent les NON-NÉGOCIABLES de CLAUDE.md (#4 design
|
|
// luxury) sous forme d'assertions vérifiables. AUCUN chiffre projet (prix,
|
|
// superficie, disponibilité) n'est asserté ici — contrainte #6 (zéro invention).
|
|
// ============================================================================
|
|
import type { Page, Response } from '@playwright/test';
|
|
|
|
/** Design luxury dark+doré — CLAUDE.md contrainte #4. */
|
|
export const LUXURY = {
|
|
/** Couleurs de marque (hex, insensible à la casse). */
|
|
colors: ['#0a0a12', '#f0b429'],
|
|
/** Familles de police officielles. */
|
|
fonts: ['Fraunces', 'Cormorant Garamond', 'Cormorant'],
|
|
} as const;
|
|
|
|
export interface RouteSpec {
|
|
path: string;
|
|
name: string;
|
|
gated: boolean;
|
|
note?: string;
|
|
}
|
|
|
|
export interface RoutesManifest {
|
|
comment?: string;
|
|
routes: RouteSpec[];
|
|
}
|
|
|
|
/** Une page est « accessible » si le serveur ne renvoie pas d'erreur (< 400).
|
|
* Une redirection login (3xx→200 sur /login) reste accessible. */
|
|
export function isReachable(res: Response | null): boolean {
|
|
if (!res) return false;
|
|
return res.status() < 400;
|
|
}
|
|
|
|
/** Détecte un mur de login (route gated non authentifiée) sans échouer le test. */
|
|
export function looksLikeLoginGate(url: string, bodyText: string): boolean {
|
|
const u = url.toLowerCase();
|
|
if (u.includes('/login') || u.includes('/app/login')) return true;
|
|
const b = bodyText.toLowerCase();
|
|
return /\b(login|se connecter|sign in|mot de passe|password)\b/.test(b)
|
|
&& b.length < 4000; // heuristique : page de login = courte
|
|
}
|
|
|
|
/** Récupère le HTML rendu + le texte de toutes les feuilles de style liées,
|
|
* pour chercher les tokens de marque dans le CSS réel servi. */
|
|
export async function collectStyleSurface(page: Page): Promise<string> {
|
|
const html = await page.content();
|
|
const cssHrefs = await page.$$eval(
|
|
'link[rel="stylesheet"]',
|
|
(links) => links.map((l) => (l as HTMLLinkElement).href).filter(Boolean),
|
|
);
|
|
const inlineCss = await page.$$eval('style', (els) =>
|
|
els.map((e) => e.textContent || '').join('\n'),
|
|
);
|
|
|
|
const parts = [html, inlineCss];
|
|
// Feuilles de style externes : on tente de les lire via le contexte réseau
|
|
// de la page (même origine / mêmes cookies). Best-effort, tolérant aux échecs.
|
|
for (const href of cssHrefs.slice(0, 12)) {
|
|
try {
|
|
const r = await page.request.get(href);
|
|
if (r.ok()) parts.push(await r.text());
|
|
} catch {
|
|
/* réseau/CORS — ignoré, best-effort */
|
|
}
|
|
}
|
|
return parts.join('\n');
|
|
}
|
|
|
|
/** Vrai si `haystack` contient au moins un des `needles` (insensible à la casse). */
|
|
export function containsAny(haystack: string, needles: readonly string[]): boolean {
|
|
const h = haystack.toLowerCase();
|
|
return needles.some((n) => h.includes(n.toLowerCase()));
|
|
}
|