#!/usr/bin/env node /** * Génère un rapport HTML ultra-détaillé à partir des résultats Playwright JSON. * * Usage: node tests/e2e/audit/scripts/generate-report.mjs * Entrée: tests/e2e/audit/results/results.json * Sortie: tests/e2e/audit/results/AUDIT_REPORT.html */ import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const RESULTS_DIR = join(__dirname, '..', 'results'); const RESULTS_FILE = join(RESULTS_DIR, 'results.json'); const OUTPUT_FILE = join(RESULTS_DIR, 'AUDIT_REPORT.html'); const SCREENSHOTS_DIR = join(RESULTS_DIR, 'screenshots'); // Lire les résultats if (!existsSync(RESULTS_FILE)) { console.error(`❌ Fichier de résultats introuvable: ${RESULTS_FILE}`); console.error(' Lancez d\'abord: npm run audit'); process.exit(1); } const raw = readFileSync(RESULTS_FILE, 'utf-8'); const results = JSON.parse(raw); // Classifier les tests const categories = { functional: { name: 'Fonctionnel', icon: '⚙️', tests: [] }, 'pixel-perfect': { name: 'Pixel-Perfect', icon: '🎨', tests: [] }, interaction: { name: 'Interactions', icon: '🖱️', tests: [] }, accessibility: { name: 'Accessibilité', icon: '♿', tests: [] }, ethical: { name: 'Éthique', icon: '🛡️', tests: [] }, screenshots: { name: 'Screenshots', icon: '📸', tests: [] }, }; const suites = results.suites || []; function flattenTests(suite, path = '') { const tests = []; const suitePath = path ? `${path} > ${suite.title}` : suite.title; for (const spec of suite.specs || []) { for (const test of spec.tests || []) { for (const result of test.results || []) { tests.push({ title: spec.title, suite: suitePath, status: result.status, duration: result.duration, error: result.error?.message || '', stdout: (result.stdout || []).map(s => typeof s === 'string' ? s : s.text || '').join('\n'), attachments: result.attachments || [], }); } } } for (const child of suite.suites || []) { tests.push(...flattenTests(child, suitePath)); } return tests; } const allTests = []; for (const suite of suites) { allTests.push(...flattenTests(suite)); } // Classifier — uses both suite name and file path for accuracy const pixelKeywords = [ 'pixel', 'chevauche', 'hover', 'focus', 'spacing', 'espacement', 'typograph', 'couleur', 'contraste', 'bordure', 'transition', 'icône', 'image', 'responsive', 'texte', 'tap target', 'disabled', 'loading', 'scroll', 'dark mode', 'lisibilit', 'overflow', 'text', 'ombres', 'borders', 'shadows', 'readability', ]; const interactionKeywords = [ 'dropdown', 'modal', 'formulaire', 'toast', 'drag', 'keyboard', 'interaction', 'forms', 'menus', 'dialogs', 'notification', 'toasts', ]; for (const test of allTests) { const text = (test.suite + ' ' + test.title).toLowerCase(); if (text.includes('accessib') || text.includes('axe') || text.includes('wcag')) { categories.accessibility.tests.push(test); } else if (text.includes('éthique') || text.includes('ethical') || text.includes('gamification') || text.includes('dark pattern') || text.includes('principes')) { categories.ethical.tests.push(test); } else if (text.includes('screenshot')) { categories.screenshots.tests.push(test); } else if (pixelKeywords.some(k => text.includes(k))) { categories['pixel-perfect'].tests.push(test); } else if (interactionKeywords.some(k => text.includes(k))) { categories.interaction.tests.push(test); } else if (text.includes('fonctionnel') || text.includes('auth') || text.includes('listener') || text.includes('creator') || text.includes('admin') || text.includes('marketplace') || text.includes('intégrité') || text.includes('se charge')) { categories.functional.tests.push(test); } else { categories.functional.tests.push(test); } } // Scores const totalTests = allTests.length; const passed = allTests.filter(t => t.status === 'passed' || t.status === 'expected').length; const failed = allTests.filter(t => t.status === 'failed' || t.status === 'unexpected').length; const skipped = allTests.filter(t => t.status === 'skipped').length; const globalScore = totalTests > 0 ? Math.round((passed / totalTests) * 100) : 0; function categoryScore(cat) { const total = cat.tests.length; if (total === 0) return 100; const pass = cat.tests.filter(t => t.status === 'passed' || t.status === 'expected').length; return Math.round((pass / total) * 100); } // Extraire les problèmes depuis stdout et error messages des tests échoués function extractProblems(test) { const problems = []; const fullText = (test.stdout || '') + '\n' + (test.error || ''); const lines = fullText.split('\n'); // Pattern 1: structured format — ÉLÉMENT: ... | PAGE: ... | MESURÉ: ... | FIX TAILWIND: ... const structuredPattern = /ÉLÉMENT:\s*(.+?)\s*\|\s*PAGE:\s*(.+?)\s*\|\s*(?:MESURÉ:\s*(.+?)\s*\|)?\s*(?:ATTENDU:\s*(.+?)\s*\|)?\s*FIX\s*(?:TAILWIND|CSS)?:\s*(.+)/; // Pattern 2: console log tags — [CATEGORY] description const tagPattern = /\[(HOVER ISSUE|ALIGNMENT|CONTRASTE|OVERFLOW|FOCUS|RADIUS|ICONS|IMAGE|AXE|ETHICAL|PLAYER OVERLAP|HEADING|TRANSITION|REDUCED MOTION|TOAST|KEYBOARD|MODAL|DROPDOWN|SELECT|DRAG|TEXT OVERFLOW|TAP TARGET|SPACING|DISABLED|DARK MODE|TEXT SIZE|LINE HEIGHT|LOADING|SCROLL|COHÉRENCE|TOAST STYLE|IMAGE ALT|IMAGE DISTORTED|SPACING MOBILE)\]\s*(.+)/; for (const line of lines) { // Try structured format first const sm = line.match(structuredPattern); if (sm) { problems.push({ element: sm[1].trim(), page: sm[2].trim(), measured: (sm[3] || '').trim(), expected: (sm[4] || '').trim(), fix: sm[5].trim(), category: 'structured', description: line.trim(), }); continue; } // Try tag format const tm = line.match(tagPattern); if (tm) { const desc = tm[2].trim(); // Extract FIX from the description if it contains one const fixInDesc = desc.match(/FIX(?:\s*TAILWIND)?:\s*(.+)/); problems.push({ category: tm[1], description: desc, fix: fixInDesc ? fixInDesc[1].trim() : '', element: '', page: '', measured: '', expected: '', }); continue; } // Pattern 3: standalone FIX line — attach to previous problem const fixMatch = line.match(/^\s*FIX:\s*(.+)/); if (fixMatch && problems.length > 0 && !problems[problems.length - 1].fix) { problems[problems.length - 1].fix = fixMatch[1].trim(); } } // Also parse the error message for bullet-point fixes if (test.error) { const bulletFixes = test.error.match(/•\s*(.+)/g); if (bulletFixes) { for (const bullet of bulletFixes) { const text = bullet.replace(/^•\s*/, '').trim(); if (text.includes('FIX') || text.includes('Ajouter') || text.includes('Changer') || text.includes('Remplacer')) { // Check if this fix is already captured if (!problems.some(p => p.description.includes(text.slice(0, 40)))) { problems.push({ category: 'error', description: text, fix: text, element: '', page: '', measured: '', expected: '', }); } } } } } return problems; } // Build the global "Copy all FIX" TODO list function buildAllFixesList() { const fixes = []; for (const [, cat] of Object.entries(categories)) { const failedTests = cat.tests.filter(t => t.status === 'failed' || t.status === 'unexpected'); for (const test of failedTests) { const problems = extractProblems(test); for (const p of problems) { if (p.fix) { const page = p.page || extractPageFromTitle(test.title); fixes.push(`- [ ] ${page}: ${p.fix}`); } } } } return fixes; } function extractPageFromTitle(title) { const m = title.match(/\(([/][^)]+)\)/); return m ? m[1] : title.split(' — ')[0] || ''; } // Collecter les screenshots let screenshots = []; if (existsSync(SCREENSHOTS_DIR)) { screenshots = readdirSync(SCREENSHOTS_DIR).filter(f => f.endsWith('.png')).sort(); } // Générer le HTML const html = `
${escapeHtml(p.element)}`);
if (p.page) parts.push(`PAGE: ${escapeHtml(p.page)}`);
if (p.measured) parts.push(`MESURÉ: ${escapeHtml(p.measured)}`);
if (p.expected) parts.push(`ATTENDU: ${escapeHtml(p.expected)}`);
return `${escapeHtml(test.stdout.slice(0, 3000))}Pour corriger les problèmes, copiez les blocs "FIX" et donnez-les à Claude Code.