#!/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 = ` Audit Veza — Rapport Complet

Audit Veza — Rapport Complet

Généré le ${new Date().toISOString().split('T')[0]} à ${new Date().toTimeString().split(' ')[0]}

${failed > 0 ? `
` : ''}
${globalScore}%
Score Global
${totalTests}
Tests Total
${passed}
Passés
${failed}
Échoués
${Object.entries(categories).filter(([, cat]) => cat.tests.length > 0).map(([key, cat]) => { const score = categoryScore(cat); return `
${score}%
${cat.icon} ${cat.name} (${cat.tests.length})
`; }).join('\n')}
${Object.entries(categories).filter(([, cat]) => cat.tests.length > 0).map(([key, cat]) => { const failedTests = cat.tests.filter(t => t.status === 'failed' || t.status === 'unexpected'); const passedTests = cat.tests.filter(t => t.status === 'passed' || t.status === 'expected'); return `

${cat.icon} ${cat.name} — ${categoryScore(cat)}%

${failedTests.length > 0 ? `

Échecs (${failedTests.length})

${failedTests.map(test => { const problems = extractProblems(test); const claudeLines = problems.filter(p => p.fix).map(p => { if (p.element) return `PROBLÈME: ${p.description}\\nÉLÉMENT: ${p.element}\\nPAGE: ${p.page}\\nMESURÉ: ${p.measured}\\nATTENDU: ${p.expected}\\nFIX: ${p.fix}`; return `PROBLÈME: ${p.description}\\nFIX: ${p.fix}`; }); const claudeText = claudeLines.join('\\n\\n'); return `
ÉCHOUÉ ${escapeHtml(test.title)}
${claudeLines.length > 0 ? `` : ''}
${problems.length > 0 ? problems.filter(p => p.fix).map(p => { const parts = []; if (p.element) parts.push(`ÉLÉMENT: ${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 `
${parts.length > 0 ? `
${parts.join(' | ')}
` : ''}
FIX: ${escapeHtml(p.fix)}
`; }).join('') : ''} ${test.error && problems.length === 0 ? `
${escapeHtml(test.error.slice(0, 1500))}
` : ''} ${test.error && problems.length > 0 ? `
Message d'erreur complet
${escapeHtml(test.error.slice(0, 1500))}
` : ''} ${test.stdout ? `
Détails (stdout)
${escapeHtml(test.stdout.slice(0, 3000))}
` : ''}
`; }).join('\n')} ` : ''} ${passedTests.length > 0 ? `
Tests passés (${passedTests.length}) ${passedTests.map(test => `
OK ${escapeHtml(test.title)} ${test.duration}ms
`).join('')}
` : ''} `; }).join('\n')} ${screenshots.length > 0 ? `

📸 Screenshots de référence (${screenshots.length})

${screenshots.map(s => `
${s}
${s.replace('.png', '').replace(/-/g, ' ')}
`).join('\n')}
` : ''}

Rapport généré par Veza Audit Suite — ${totalTests} tests, ${passed} passés, ${failed} échoués

Pour corriger les problèmes, copiez les blocs "FIX" et donnez-les à Claude Code.

`; function escapeHtml(str) { return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function escapeJs(str) { return str.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$'); } writeFileSync(OUTPUT_FILE, html, 'utf-8'); console.log(`\n✅ Rapport généré: ${OUTPUT_FILE}`); console.log(` ${totalTests} tests — ${passed} passés — ${failed} échoués — Score: ${globalScore}%\n`);