// ============================================================================ // 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 { 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())); }