710 lines
24 KiB
TypeScript
710 lines
24 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test';
|
|
import { writeFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
/**
|
|
* Deep E2E Audit - Runtime Stability Check
|
|
*
|
|
* Ce test effectue un parcours utilisateur complet et capture toutes les erreurs
|
|
* Runtime, Réseau, et d'Intégration pour valider la stabilité après les corrections
|
|
* de lazy loading.
|
|
*/
|
|
|
|
// Configuration
|
|
const FRONTEND_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000';
|
|
const TEST_EMAIL = process.env.TEST_EMAIL || 'user@example.com';
|
|
const TEST_PASSWORD = process.env.TEST_PASSWORD || 'password123';
|
|
|
|
// Types pour le rapport
|
|
interface RuntimeIssue {
|
|
id: string;
|
|
category: 'NETWORK' | 'CONSOLE' | 'NAVIGATION' | 'UX';
|
|
severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
|
|
location: string;
|
|
message: string;
|
|
details?: string;
|
|
reproduction_steps: string;
|
|
timestamp?: string;
|
|
}
|
|
|
|
interface PageCheckResult {
|
|
path: string;
|
|
loaded: boolean;
|
|
hasContent: boolean;
|
|
errors: RuntimeIssue[];
|
|
loadTime?: number;
|
|
}
|
|
|
|
interface AuditReport {
|
|
globalStatus: 'STABLE' | 'UNSTABLE';
|
|
loginSuccess: boolean;
|
|
pages: PageCheckResult[];
|
|
allIssues: RuntimeIssue[];
|
|
summary: {
|
|
totalIssues: number;
|
|
critical: number;
|
|
high: number;
|
|
medium: number;
|
|
low: number;
|
|
byCategory: {
|
|
NETWORK: number;
|
|
CONSOLE: number;
|
|
NAVIGATION: number;
|
|
UX: number;
|
|
};
|
|
};
|
|
}
|
|
|
|
// Collecteurs globaux
|
|
let allIssues: RuntimeIssue[] = [];
|
|
let issueCounter = 1;
|
|
|
|
function generateIssueId(): string {
|
|
return `RUN-${String(issueCounter++).padStart(3, '0')}`;
|
|
}
|
|
|
|
function addIssue(issue: Omit<RuntimeIssue, 'id' | 'timestamp'>): void {
|
|
allIssues.push({
|
|
...issue,
|
|
id: generateIssueId(),
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
// Helper pour vérifier qu'une page a du contenu
|
|
async function checkPageHasContent(page: Page, selectors: string[]): Promise<boolean> {
|
|
// Vérifier d'abord que le body n'est pas vide
|
|
const bodyText = await page.locator('body').textContent().catch(() => '');
|
|
if (!bodyText || bodyText.trim().length < 10) {
|
|
return false;
|
|
}
|
|
|
|
// Vérifier les sélecteurs spécifiques
|
|
for (const selector of selectors) {
|
|
try {
|
|
const count = await page.locator(selector).count();
|
|
if (count > 0) {
|
|
const element = await page.locator(selector).first();
|
|
if (await element.isVisible({ timeout: 2000 })) {
|
|
return true;
|
|
}
|
|
}
|
|
} catch {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Si aucun sélecteur spécifique n'est trouvé, vérifier qu'il y a au moins du contenu dans main ou body
|
|
const mainContent = await page.locator('main, [role="main"], .main-content').first().textContent().catch(() => '');
|
|
if (mainContent && mainContent.trim().length > 10) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Helper pour attendre qu'une page charge
|
|
async function waitForPageLoad(
|
|
page: Page,
|
|
expectedPath: string,
|
|
contentSelectors: string[],
|
|
timeout = 10000
|
|
): Promise<PageCheckResult> {
|
|
const startTime = Date.now();
|
|
const result: PageCheckResult = {
|
|
path: expectedPath,
|
|
loaded: false,
|
|
hasContent: false,
|
|
errors: [],
|
|
};
|
|
|
|
try {
|
|
// Vérifier d'abord si on est déjà sur la bonne page
|
|
const currentPath = new URL(page.url()).pathname;
|
|
if (currentPath !== expectedPath) {
|
|
// Attendre la navigation seulement si on n'est pas déjà sur la bonne page
|
|
await page.waitForURL(
|
|
(url) => url.pathname === expectedPath,
|
|
{ timeout, waitUntil: 'domcontentloaded' }
|
|
);
|
|
}
|
|
result.loaded = true;
|
|
|
|
// Attendre que le réseau soit idle
|
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
|
addIssue({
|
|
category: 'NAVIGATION',
|
|
severity: 'MEDIUM',
|
|
location: expectedPath,
|
|
message: 'Page took too long to reach networkidle state',
|
|
details: `Timeout after 5s waiting for networkidle`,
|
|
reproduction_steps: `Navigate to ${expectedPath}`,
|
|
});
|
|
});
|
|
|
|
// Vérifier qu'il y a du contenu
|
|
result.hasContent = await checkPageHasContent(page, contentSelectors);
|
|
|
|
if (!result.hasContent) {
|
|
addIssue({
|
|
category: 'UX',
|
|
severity: 'HIGH',
|
|
location: expectedPath,
|
|
message: 'Page appears to be blank or empty',
|
|
details: `None of the expected selectors found: ${contentSelectors.join(', ')}`,
|
|
reproduction_steps: `Navigate to ${expectedPath} after login`,
|
|
});
|
|
}
|
|
|
|
result.loadTime = Date.now() - startTime;
|
|
} catch (error) {
|
|
result.loaded = false;
|
|
addIssue({
|
|
category: 'NAVIGATION',
|
|
severity: 'CRITICAL',
|
|
location: expectedPath,
|
|
message: `Failed to navigate to ${expectedPath}`,
|
|
details: error instanceof Error ? error.message : String(error),
|
|
reproduction_steps: `Navigate to ${expectedPath} after login`,
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
test.describe('Deep E2E Runtime Audit', () => {
|
|
let report: AuditReport;
|
|
|
|
test.beforeEach(() => {
|
|
allIssues = [];
|
|
issueCounter = 1;
|
|
report = {
|
|
globalStatus: 'STABLE',
|
|
loginSuccess: false,
|
|
pages: [],
|
|
allIssues: [],
|
|
summary: {
|
|
totalIssues: 0,
|
|
critical: 0,
|
|
high: 0,
|
|
medium: 0,
|
|
low: 0,
|
|
byCategory: {
|
|
NETWORK: 0,
|
|
CONSOLE: 0,
|
|
NAVIGATION: 0,
|
|
UX: 0,
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
test('Complete User Journey - Runtime Audit', async ({ page, context }) => {
|
|
test.setTimeout(60000); // 60 secondes pour le test complet
|
|
console.log('🔍 [AUDIT] Starting comprehensive E2E audit...');
|
|
|
|
// ============================================
|
|
// PHASE 1: Setup Error Listeners
|
|
// ============================================
|
|
|
|
// Console errors & warnings
|
|
page.on('console', (msg) => {
|
|
const type = msg.type();
|
|
const text = msg.text();
|
|
const location = page.url();
|
|
|
|
if (type === 'error') {
|
|
addIssue({
|
|
category: 'CONSOLE',
|
|
severity: 'HIGH',
|
|
location,
|
|
message: text,
|
|
details: `Console error: ${text}`,
|
|
reproduction_steps: `Navigate to ${location}`,
|
|
});
|
|
console.log(`🔴 [CONSOLE ERROR] ${text}`);
|
|
} else if (type === 'warning') {
|
|
// Même les warnings sont capturés (comme demandé)
|
|
addIssue({
|
|
category: 'CONSOLE',
|
|
severity: 'MEDIUM',
|
|
location,
|
|
message: text,
|
|
details: `Console warning: ${text}`,
|
|
reproduction_steps: `Navigate to ${location}`,
|
|
});
|
|
console.log(`🟡 [CONSOLE WARNING] ${text}`);
|
|
}
|
|
});
|
|
|
|
// Page errors (uncaught exceptions)
|
|
page.on('pageerror', (error) => {
|
|
addIssue({
|
|
category: 'CONSOLE',
|
|
severity: 'CRITICAL',
|
|
location: page.url(),
|
|
message: error.message,
|
|
details: error.stack,
|
|
reproduction_steps: `Navigate to ${page.url()}`,
|
|
});
|
|
console.log(`🔴 [PAGE ERROR] ${error.message}`);
|
|
});
|
|
|
|
// Network errors (4xx, 5xx)
|
|
page.on('response', async (response) => {
|
|
const status = response.status();
|
|
const url = response.url();
|
|
const method = response.request().method();
|
|
|
|
if (status >= 400) {
|
|
const severity = status >= 500 ? 'CRITICAL' : status >= 400 ? 'HIGH' : 'MEDIUM';
|
|
|
|
// Essayer de récupérer le body de l'erreur pour plus de détails
|
|
let errorDetails = `Server responded with status ${status}`;
|
|
try {
|
|
const responseBody = await response.text().catch(() => '');
|
|
if (responseBody) {
|
|
try {
|
|
const parsed = JSON.parse(responseBody);
|
|
if (parsed && parsed.error) {
|
|
errorDetails = `${errorDetails}. Error: ${parsed.error}`;
|
|
} else if (parsed && parsed.message) {
|
|
errorDetails = `${errorDetails}. Message: ${parsed.message}`;
|
|
}
|
|
} catch {
|
|
// Si ce n'est pas du JSON, prendre un extrait du texte
|
|
if (responseBody.length < 200) {
|
|
errorDetails = `${errorDetails}. Response: ${responseBody.substring(0, 200)}`;
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore si on ne peut pas parser la réponse
|
|
}
|
|
|
|
addIssue({
|
|
category: 'NETWORK',
|
|
severity,
|
|
location: page.url(),
|
|
message: `HTTP ${status} - ${method} ${url}`,
|
|
details: errorDetails,
|
|
reproduction_steps: `Navigate to ${page.url()}`,
|
|
});
|
|
console.log(`🔴 [NETWORK ERROR] ${method} ${url} -> ${status}`);
|
|
}
|
|
});
|
|
|
|
// Failed requests (network failures)
|
|
page.on('requestfailed', (request) => {
|
|
const failure = request.failure();
|
|
if (failure) {
|
|
const url = request.url();
|
|
const method = request.method();
|
|
|
|
// Ne pas reporter les erreurs de favicon ou de ressources statiques non critiques
|
|
if (url.includes('favicon') || url.includes('.ico') || url.includes('chrome-extension')) {
|
|
return;
|
|
}
|
|
|
|
addIssue({
|
|
category: 'NETWORK',
|
|
severity: 'CRITICAL',
|
|
location: page.url(),
|
|
message: `Request failed: ${method} ${url}`,
|
|
details: failure.errorText || 'Network error',
|
|
reproduction_steps: `Navigate to ${page.url()}`,
|
|
});
|
|
console.log(`🔴 [REQUEST FAILED] ${method} ${url}: ${failure.errorText}`);
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// PHASE 2: Login Flow
|
|
// ============================================
|
|
|
|
console.log('🔍 [AUDIT] Step 1: Navigating to login...');
|
|
await page.goto(`${FRONTEND_URL}/login`, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
|
|
console.warn('⚠️ [AUDIT] Timeout on networkidle for login page');
|
|
});
|
|
|
|
// Attendre que le formulaire soit chargé
|
|
await page.waitForSelector('input[type="email"], input[name="email"]', {
|
|
timeout: 10000,
|
|
}).catch(() => {
|
|
addIssue({
|
|
category: 'UX',
|
|
severity: 'CRITICAL',
|
|
location: '/login',
|
|
message: 'Login form not found',
|
|
details: 'Email input field not visible',
|
|
reproduction_steps: 'Navigate to /login',
|
|
});
|
|
});
|
|
|
|
// Remplir le formulaire
|
|
const emailInput = page.locator('input[type="email"], input[name="email"]').first();
|
|
const passwordInput = page.locator('input[type="password"]').first();
|
|
const submitButton = page.locator('button[type="submit"], button:has-text("connecter"), button:has-text("login"), button:has-text("Se connecter")').first();
|
|
|
|
await emailInput.fill(TEST_EMAIL);
|
|
await passwordInput.fill(TEST_PASSWORD);
|
|
|
|
console.log('🔍 [AUDIT] Step 2: Submitting login form...');
|
|
|
|
// Attendre la navigation après login
|
|
await submitButton.click();
|
|
|
|
// Attendre soit la navigation, soit un message d'erreur
|
|
try {
|
|
await page.waitForURL(
|
|
(url) => url.pathname === '/dashboard' || url.pathname === '/',
|
|
{ timeout: 15000 }
|
|
);
|
|
report.loginSuccess = true;
|
|
console.log('✅ [AUDIT] Login successful, redirected to:', page.url());
|
|
} catch (error) {
|
|
// Vérifier si on est toujours sur /login ou si on a une erreur
|
|
const currentUrl = page.url();
|
|
const currentPath = new URL(currentUrl).pathname;
|
|
|
|
if (currentPath === '/login') {
|
|
report.loginSuccess = false;
|
|
addIssue({
|
|
category: 'NAVIGATION',
|
|
severity: 'CRITICAL',
|
|
location: '/login',
|
|
message: 'Login failed or did not redirect',
|
|
details: `Still on ${currentUrl} after login attempt. Check for error messages or network failures.`,
|
|
reproduction_steps: `Login with ${TEST_EMAIL}`,
|
|
});
|
|
console.error('❌ [AUDIT] Login failed or did not redirect');
|
|
|
|
// Si le login échoue, on génère quand même le rapport avec les erreurs capturées
|
|
report.allIssues = allIssues;
|
|
report.summary.totalIssues = allIssues.length;
|
|
report.summary.critical = allIssues.filter((i) => i.severity === 'CRITICAL').length;
|
|
report.summary.high = allIssues.filter((i) => i.severity === 'HIGH').length;
|
|
report.summary.medium = allIssues.filter((i) => i.severity === 'MEDIUM').length;
|
|
report.summary.low = allIssues.filter((i) => i.severity === 'LOW').length;
|
|
report.summary.byCategory.NETWORK = allIssues.filter((i) => i.category === 'NETWORK').length;
|
|
report.summary.byCategory.CONSOLE = allIssues.filter((i) => i.category === 'CONSOLE').length;
|
|
report.summary.byCategory.NAVIGATION = allIssues.filter((i) => i.category === 'NAVIGATION').length;
|
|
report.summary.byCategory.UX = allIssues.filter((i) => i.category === 'UX').length;
|
|
report.globalStatus = 'UNSTABLE';
|
|
|
|
// Sauvegarder le rapport même en cas d'échec
|
|
await page.evaluate((report) => {
|
|
(window as any).__auditReport = report;
|
|
}, report);
|
|
|
|
return;
|
|
} else {
|
|
// On a navigué ailleurs (peut-être une page d'erreur ou autre)
|
|
report.loginSuccess = false;
|
|
addIssue({
|
|
category: 'NAVIGATION',
|
|
severity: 'HIGH',
|
|
location: currentPath,
|
|
message: 'Login redirected to unexpected page',
|
|
details: `Expected /dashboard but got ${currentUrl}`,
|
|
reproduction_steps: `Login with ${TEST_EMAIL}`,
|
|
});
|
|
console.warn('⚠️ [AUDIT] Login redirected to unexpected page:', currentUrl);
|
|
}
|
|
}
|
|
|
|
// Attendre un peu pour que l'app se stabilise
|
|
await page.waitForTimeout(2000);
|
|
|
|
// ============================================
|
|
// PHASE 3: Navigation & Lazy Loading Check
|
|
// ============================================
|
|
|
|
console.log('🔍 [AUDIT] Step 3: Testing page navigation and lazy loading...');
|
|
|
|
// Dashboard (déjà chargé)
|
|
console.log(' → Checking /dashboard...');
|
|
// S'assurer qu'on est bien sur /dashboard
|
|
if (new URL(page.url()).pathname !== '/dashboard') {
|
|
await page.goto(`${FRONTEND_URL}/dashboard`, { waitUntil: 'domcontentloaded' });
|
|
}
|
|
const dashboardCheck = await waitForPageLoad(
|
|
page,
|
|
'/dashboard',
|
|
['[data-testid="dashboard"]', 'h1', 'main', '.container', 'nav', 'aside'],
|
|
);
|
|
report.pages.push(dashboardCheck);
|
|
|
|
// Profile page
|
|
console.log(' → Navigating to /profile...');
|
|
await page.goto(`${FRONTEND_URL}/profile`, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
|
await page.waitForTimeout(1000); // Attendre que le lazy loading se stabilise
|
|
const profileCheck = await waitForPageLoad(
|
|
page,
|
|
'/profile',
|
|
['h1', 'main', '.container', '[data-testid="profile"]', 'form', 'button'],
|
|
);
|
|
report.pages.push(profileCheck);
|
|
|
|
// Settings page
|
|
console.log(' → Navigating to /settings...');
|
|
await page.goto(`${FRONTEND_URL}/settings`, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
|
await page.waitForTimeout(1000); // Attendre que le lazy loading se stabilise
|
|
const settingsCheck = await waitForPageLoad(
|
|
page,
|
|
'/settings',
|
|
['h1:has-text("Paramètres"), h1:has-text("Settings"), h1', 'main', '.container', 'form', 'button'],
|
|
);
|
|
report.pages.push(settingsCheck);
|
|
|
|
// Library page
|
|
console.log(' → Navigating to /library...');
|
|
await page.goto(`${FRONTEND_URL}/library`, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
|
await page.waitForTimeout(1000); // Attendre que le lazy loading se stabilise
|
|
const libraryCheck = await waitForPageLoad(
|
|
page,
|
|
'/library',
|
|
['h1', 'main', '.container', '[data-testid="library"]', 'button', 'div'],
|
|
);
|
|
report.pages.push(libraryCheck);
|
|
|
|
// ============================================
|
|
// PHASE 4: Generate Report
|
|
// ============================================
|
|
|
|
report.allIssues = allIssues;
|
|
|
|
// Calculer le résumé
|
|
report.summary.totalIssues = allIssues.length;
|
|
report.summary.critical = allIssues.filter((i) => i.severity === 'CRITICAL').length;
|
|
report.summary.high = allIssues.filter((i) => i.severity === 'HIGH').length;
|
|
report.summary.medium = allIssues.filter((i) => i.severity === 'MEDIUM').length;
|
|
report.summary.low = allIssues.filter((i) => i.severity === 'LOW').length;
|
|
|
|
report.summary.byCategory.NETWORK = allIssues.filter((i) => i.category === 'NETWORK').length;
|
|
report.summary.byCategory.CONSOLE = allIssues.filter((i) => i.category === 'CONSOLE').length;
|
|
report.summary.byCategory.NAVIGATION = allIssues.filter((i) => i.category === 'NAVIGATION').length;
|
|
report.summary.byCategory.UX = allIssues.filter((i) => i.category === 'UX').length;
|
|
|
|
// Déterminer le statut global
|
|
if (
|
|
report.summary.critical > 0 ||
|
|
!report.loginSuccess ||
|
|
report.pages.some((p) => !p.loaded || !p.hasContent)
|
|
) {
|
|
report.globalStatus = 'UNSTABLE';
|
|
}
|
|
|
|
// Afficher le résumé dans la console
|
|
console.log('\n📊 [AUDIT] === AUDIT SUMMARY ===');
|
|
console.log(`Global Status: ${report.globalStatus}`);
|
|
console.log(`Login Success: ${report.loginSuccess}`);
|
|
console.log(`Pages Checked: ${report.pages.length}`);
|
|
console.log(`Total Issues: ${report.summary.totalIssues}`);
|
|
console.log(` - Critical: ${report.summary.critical}`);
|
|
console.log(` - High: ${report.summary.high}`);
|
|
console.log(` - Medium: ${report.summary.medium}`);
|
|
console.log(` - Low: ${report.summary.low}`);
|
|
console.log(`By Category:`);
|
|
console.log(` - NETWORK: ${report.summary.byCategory.NETWORK}`);
|
|
console.log(` - CONSOLE: ${report.summary.byCategory.CONSOLE}`);
|
|
console.log(` - NAVIGATION: ${report.summary.byCategory.NAVIGATION}`);
|
|
console.log(` - UX: ${report.summary.byCategory.UX}`);
|
|
|
|
// Sauvegarder le rapport dans la page pour récupération
|
|
await page.evaluate((report) => {
|
|
(window as any).__auditReport = report;
|
|
}, report);
|
|
|
|
// Assertions finales (ne pas faire échouer le test, juste logger)
|
|
if (report.globalStatus === 'UNSTABLE') {
|
|
console.error('❌ [AUDIT] Application is UNSTABLE');
|
|
} else {
|
|
console.log('✅ [AUDIT] Application appears STABLE');
|
|
}
|
|
});
|
|
|
|
test.afterEach(async ({ page }) => {
|
|
// Récupérer le rapport depuis la page
|
|
const savedReport = await page
|
|
.evaluate(() => {
|
|
return (window as any).__auditReport;
|
|
})
|
|
.catch(() => null);
|
|
|
|
if (savedReport) {
|
|
report = savedReport;
|
|
}
|
|
|
|
// Écrire les rapports dans des fichiers
|
|
// Utiliser process.cwd() car __dirname peut ne pas être disponible en ESM
|
|
const projectRoot = process.cwd();
|
|
|
|
// Rapport JSON
|
|
const jsonPath = join(projectRoot, 'RUNTIME_ISSUES.json');
|
|
writeFileSync(jsonPath, JSON.stringify(report.allIssues, null, 2));
|
|
console.log(`📄 [AUDIT] JSON report written to: ${jsonPath}`);
|
|
|
|
// Rapport Markdown
|
|
const mdPath = join(projectRoot, 'RUNTIME_AUDIT_REPORT.md');
|
|
const mdContent = generateMarkdownReport(report);
|
|
writeFileSync(mdPath, mdContent);
|
|
console.log(`📄 [AUDIT] Markdown report written to: ${mdPath}`);
|
|
});
|
|
});
|
|
|
|
function generateMarkdownReport(report: AuditReport): string {
|
|
const lines: string[] = [];
|
|
|
|
lines.push('# Runtime Audit Report');
|
|
lines.push('');
|
|
lines.push(`**Generated:** ${new Date().toISOString()}`);
|
|
lines.push('');
|
|
lines.push('---');
|
|
lines.push('');
|
|
|
|
// État Global
|
|
lines.push('## État Global');
|
|
lines.push('');
|
|
lines.push(`**Status:** ${report.globalStatus === 'STABLE' ? '✅ STABLE' : '❌ UNSTABLE'}`);
|
|
lines.push(`**Login Success:** ${report.loginSuccess ? '✅ Yes' : '❌ No'}`);
|
|
lines.push('');
|
|
|
|
// Parcours
|
|
lines.push('## Parcours Utilisateur');
|
|
lines.push('');
|
|
lines.push('| Page | Loaded | Has Content | Load Time (ms) |');
|
|
lines.push('|------|--------|-------------|----------------|');
|
|
for (const page of report.pages) {
|
|
const loaded = page.loaded ? '✅' : '❌';
|
|
const content = page.hasContent ? '✅' : '❌';
|
|
const loadTime = page.loadTime ? `${page.loadTime}ms` : 'N/A';
|
|
lines.push(`| ${page.path} | ${loaded} | ${content} | ${loadTime} |`);
|
|
}
|
|
lines.push('');
|
|
|
|
// Résumé des erreurs
|
|
lines.push('## Résumé des Erreurs');
|
|
lines.push('');
|
|
lines.push(`**Total Issues:** ${report.summary.totalIssues}`);
|
|
lines.push('');
|
|
lines.push('### Par Sévérité');
|
|
lines.push('');
|
|
lines.push(`- **CRITICAL:** ${report.summary.critical}`);
|
|
lines.push(`- **HIGH:** ${report.summary.high}`);
|
|
lines.push(`- **MEDIUM:** ${report.summary.medium}`);
|
|
lines.push(`- **LOW:** ${report.summary.low}`);
|
|
lines.push('');
|
|
|
|
lines.push('### Par Catégorie');
|
|
lines.push('');
|
|
lines.push(`- **NETWORK:** ${report.summary.byCategory.NETWORK}`);
|
|
lines.push(`- **CONSOLE:** ${report.summary.byCategory.CONSOLE}`);
|
|
lines.push(`- **NAVIGATION:** ${report.summary.byCategory.NAVIGATION}`);
|
|
lines.push(`- **UX:** ${report.summary.byCategory.UX}`);
|
|
lines.push('');
|
|
|
|
// Erreurs Console
|
|
const consoleErrors = report.allIssues.filter((i) => i.category === 'CONSOLE');
|
|
if (consoleErrors.length > 0) {
|
|
lines.push('## Erreurs Console');
|
|
lines.push('');
|
|
for (const error of consoleErrors) {
|
|
lines.push(`### ${error.id} - ${error.severity}`);
|
|
lines.push('');
|
|
lines.push(`- **Location:** ${error.location}`);
|
|
lines.push(`- **Message:** ${error.message}`);
|
|
if (error.details) {
|
|
lines.push(`- **Details:** ${error.details}`);
|
|
}
|
|
lines.push(`- **Reproduction:** ${error.reproduction_steps}`);
|
|
lines.push('');
|
|
}
|
|
}
|
|
|
|
// Erreurs Réseau
|
|
const networkErrors = report.allIssues.filter((i) => i.category === 'NETWORK');
|
|
if (networkErrors.length > 0) {
|
|
lines.push('## Erreurs Réseau');
|
|
lines.push('');
|
|
for (const error of networkErrors) {
|
|
lines.push(`### ${error.id} - ${error.severity}`);
|
|
lines.push('');
|
|
lines.push(`- **Location:** ${error.location}`);
|
|
lines.push(`- **Message:** ${error.message}`);
|
|
if (error.details) {
|
|
lines.push(`- **Details:** ${error.details}`);
|
|
}
|
|
lines.push(`- **Reproduction:** ${error.reproduction_steps}`);
|
|
lines.push('');
|
|
}
|
|
}
|
|
|
|
// Erreurs Navigation
|
|
const navErrors = report.allIssues.filter((i) => i.category === 'NAVIGATION');
|
|
if (navErrors.length > 0) {
|
|
lines.push('## Erreurs Navigation');
|
|
lines.push('');
|
|
for (const error of navErrors) {
|
|
lines.push(`### ${error.id} - ${error.severity}`);
|
|
lines.push('');
|
|
lines.push(`- **Location:** ${error.location}`);
|
|
lines.push(`- **Message:** ${error.message}`);
|
|
if (error.details) {
|
|
lines.push(`- **Details:** ${error.details}`);
|
|
}
|
|
lines.push(`- **Reproduction:** ${error.reproduction_steps}`);
|
|
lines.push('');
|
|
}
|
|
}
|
|
|
|
// Erreurs UX
|
|
const uxErrors = report.allIssues.filter((i) => i.category === 'UX');
|
|
if (uxErrors.length > 0) {
|
|
lines.push('## Erreurs UX');
|
|
lines.push('');
|
|
for (const error of uxErrors) {
|
|
lines.push(`### ${error.id} - ${error.severity}`);
|
|
lines.push('');
|
|
lines.push(`- **Location:** ${error.location}`);
|
|
lines.push(`- **Message:** ${error.message}`);
|
|
if (error.details) {
|
|
lines.push(`- **Details:** ${error.details}`);
|
|
}
|
|
lines.push(`- **Reproduction:** ${error.reproduction_steps}`);
|
|
lines.push('');
|
|
}
|
|
}
|
|
|
|
if (report.allIssues.length === 0) {
|
|
lines.push('## ✅ Aucune Erreur Détectée');
|
|
lines.push('');
|
|
lines.push('L\'application semble stable. Aucune erreur runtime, réseau ou d\'intégration n\'a été détectée.');
|
|
}
|
|
|
|
// Note sur les limitations du test
|
|
if (!report.loginSuccess) {
|
|
lines.push('---');
|
|
lines.push('');
|
|
lines.push('## ⚠️ Note sur les Limitations du Test');
|
|
lines.push('');
|
|
lines.push('Le test n\'a pas pu continuer au-delà de la page de login car le backend n\'était pas accessible.');
|
|
lines.push('Les pages protégées (Dashboard, Profile, Settings, Library) n\'ont donc pas pu être testées.');
|
|
lines.push('');
|
|
lines.push('**Pour un audit complet :**');
|
|
lines.push('1. Démarrer le backend API sur `http://localhost:8080`');
|
|
lines.push('2. Configurer CORS pour autoriser les requêtes depuis `http://localhost:3000`');
|
|
lines.push('3. Relancer le test avec `npx playwright test e2e/deep_audit.spec.ts`');
|
|
lines.push('');
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|