#!/usr/bin/env node /** * Lighthouse Audit for All Locales * * Runs Lighthouse performance audits against all locales (de, en, sr) * and key pages using Playwright-Lighthouse integration. * * Usage: node scripts/lighthouse-audit-all-locales.js * * Requirements: * - Dev server running on localhost:3000 (npm run dev) * - Chromium installed via Playwright * * Thresholds: * - Performance: 90% * - Accessibility: 90% * - Best Practices: 90% * - SEO: 90% * * Note: Performance and SEO scores may be lower in development mode. * Accessibility and Best Practices should always meet thresholds. */ import { chromium } from 'playwright'; import { playAudit } from 'playwright-lighthouse'; // Configuration const BASE_URL = 'http://localhost:3000'; const LOCALES = ['de', 'en', 'sr']; const PAGES = [ { path: '', name: 'Homepage' }, { path: '/about', name: 'About' }, { path: '/portfolio', name: 'Portfolio' }, { path: '/contact', name: 'Contact' }, ]; // Thresholds - relaxed for dev mode, stricter for prod const THRESHOLDS = { performance: 50, // Lower in dev mode (expected 47-69%) accessibility: 90, // Should pass in dev mode 'best-practices': 90, // Should pass in dev mode seo: 75, // Lower in dev mode (expected 75-83%) }; // Production thresholds (what we aim for in production) const PRODUCTION_THRESHOLDS = { performance: 90, accessibility: 90, 'best-practices': 90, seo: 90, }; // Results storage const results = []; let totalPassed = 0; let totalFailed = 0; // Formatting helpers function formatScore(score, isDevMode = true) { const percentage = Math.round(score * 100); const threshold = isDevMode ? THRESHOLDS : PRODUCTION_THRESHOLDS; if (percentage >= 90) return `\x1b[32m${percentage}% āœ“\x1b[0m`; // Green if (percentage >= threshold.performance) return `\x1b[33m${percentage}% ~\x1b[0m`; // Yellow return `\x1b[31m${percentage}% āœ—\x1b[0m`; // Red } function formatCategoryScore(score, category, isDevMode = true) { const percentage = Math.round(score * 100); const threshold = isDevMode ? THRESHOLDS[category] : PRODUCTION_THRESHOLDS[category]; const passed = percentage >= threshold; if (passed) { return `\x1b[32m${percentage}% āœ“\x1b[0m`; } return `\x1b[31m${percentage}% āœ—\x1b[0m`; } async function runAudit(browser, url, pageName, locale) { console.log(`\n šŸ“Š Auditing ${pageName} (${locale})...`); const page = await browser.newPage(); try { // Navigate to the page await page.goto(url, { waitUntil: 'networkidle' }); await page.waitForLoadState('domcontentloaded'); // Run Lighthouse audit const auditResult = await playAudit({ page, port: 9222, thresholds: THRESHOLDS, reports: { formats: { json: false, html: false, csv: false }, name: `audit-${locale}-${pageName.toLowerCase()}`, directory: './lighthouse-reports', }, disableLogs: true, }); // Extract scores const scores = { performance: auditResult.lhr.categories.performance.score, accessibility: auditResult.lhr.categories.accessibility.score, 'best-practices': auditResult.lhr.categories['best-practices'].score, seo: auditResult.lhr.categories.seo.score, }; // Check if passes dev mode thresholds const accessibilityPass = scores.accessibility * 100 >= THRESHOLDS.accessibility; const bestPracticesPass = scores['best-practices'] * 100 >= THRESHOLDS['best-practices']; const passed = accessibilityPass && bestPracticesPass; // Store result results.push({ url, pageName, locale, scores, passed, }); if (passed) { totalPassed++; } else { totalFailed++; } // Display inline results console.log(` Performance: ${formatCategoryScore(scores.performance, 'performance')}`); console.log(` Accessibility: ${formatCategoryScore(scores.accessibility, 'accessibility')}`); console.log(` Best Practices: ${formatCategoryScore(scores['best-practices'], 'best-practices')}`); console.log(` SEO: ${formatCategoryScore(scores.seo, 'seo')}`); } catch (error) { console.error(` āŒ Error auditing ${url}: ${error.message}`); results.push({ url, pageName, locale, scores: null, passed: false, error: error.message, }); totalFailed++; } finally { await page.close(); } } async function checkDevServer() { try { const response = await fetch(`${BASE_URL}/de`); return response.ok; } catch { return false; } } async function main() { console.log('\n╔════════════════════════════════════════════════════════════╗'); console.log('ā•‘ Lighthouse Performance Audit - All Locales ā•‘'); console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n'); console.log('šŸ“‹ Configuration:'); console.log(` Base URL: ${BASE_URL}`); console.log(` Locales: ${LOCALES.join(', ')}`); console.log(` Pages: ${PAGES.map(p => p.name).join(', ')}`); console.log(` Total Audits: ${LOCALES.length * PAGES.length}`); console.log('\nšŸ“Š Thresholds (Dev Mode):'); console.log(` Performance: ${THRESHOLDS.performance}% (production: ${PRODUCTION_THRESHOLDS.performance}%)`); console.log(` Accessibility: ${THRESHOLDS.accessibility}%`); console.log(` Best Practices: ${THRESHOLDS['best-practices']}%`); console.log(` SEO: ${THRESHOLDS.seo}% (production: ${PRODUCTION_THRESHOLDS.seo}%)`); // Check if dev server is running console.log('\nšŸ” Checking dev server...'); const serverRunning = await checkDevServer(); if (!serverRunning) { console.error('\nāŒ Dev server not responding at', BASE_URL); console.log(' Please start the dev server: npm run dev'); process.exit(1); } console.log(' āœ“ Dev server is running'); // Launch browser with remote debugging port console.log('\nšŸš€ Launching Chromium...'); const browser = await chromium.launch({ args: ['--remote-debugging-port=9222'], headless: true, }); console.log(' āœ“ Browser launched'); const startTime = Date.now(); try { // Run audits for each locale for (const locale of LOCALES) { console.log(`\nā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”`); console.log(`│ Locale: ${locale.toUpperCase()} │`); console.log(`ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜`); for (const page of PAGES) { const url = `${BASE_URL}/${locale}${page.path}`; await runAudit(browser, url, page.name, locale); } } } finally { await browser.close(); } const duration = ((Date.now() - startTime) / 1000).toFixed(1); // Summary console.log('\n╔════════════════════════════════════════════════════════════╗'); console.log('ā•‘ AUDIT SUMMARY ā•‘'); console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n'); // Results table console.log('ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”'); console.log('│ Locale │ Page │ Perf │ A11y │ BP │ SEO │'); console.log('ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”¤'); for (const result of results) { if (result.scores) { const perf = Math.round(result.scores.performance * 100).toString().padStart(3); const a11y = Math.round(result.scores.accessibility * 100).toString().padStart(3); const bp = Math.round(result.scores['best-practices'] * 100).toString().padStart(3); const seo = Math.round(result.scores.seo * 100).toString().padStart(3); const locale = result.locale.padEnd(7); const page = result.pageName.padEnd(12); console.log(`│ ${locale} │ ${page} │ ${perf}% │ ${a11y}% │ ${bp}% │ ${seo}% │`); } else { console.log(`│ ${result.locale.padEnd(7)} │ ${result.pageName.padEnd(12)} │ ERR │ ERR │ ERR │ ERR │`); } } console.log('ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”˜'); // Calculate averages const validResults = results.filter(r => r.scores); if (validResults.length > 0) { const avgPerf = validResults.reduce((sum, r) => sum + r.scores.performance, 0) / validResults.length; const avgA11y = validResults.reduce((sum, r) => sum + r.scores.accessibility, 0) / validResults.length; const avgBP = validResults.reduce((sum, r) => sum + r.scores['best-practices'], 0) / validResults.length; const avgSEO = validResults.reduce((sum, r) => sum + r.scores.seo, 0) / validResults.length; console.log('\nšŸ“ˆ Average Scores:'); console.log(` Performance: ${formatCategoryScore(avgPerf, 'performance')}`); console.log(` Accessibility: ${formatCategoryScore(avgA11y, 'accessibility')}`); console.log(` Best Practices: ${formatCategoryScore(avgBP, 'best-practices')}`); console.log(` SEO: ${formatCategoryScore(avgSEO, 'seo')}`); } // Pass/Fail summary console.log('\nšŸ“‹ Summary:'); console.log(` Total Audits: ${results.length}`); console.log(` Duration: ${duration}s`); // Check critical thresholds (Accessibility and Best Practices should always pass) const a11yResults = validResults.filter(r => Math.round(r.scores.accessibility * 100) >= 90); const bpResults = validResults.filter(r => Math.round(r.scores['best-practices'] * 100) >= 90); console.log('\nšŸŽÆ Threshold Compliance (Dev Mode):'); console.log(` Accessibility >= 90%: ${a11yResults.length}/${validResults.length} pages ${a11yResults.length === validResults.length ? 'āœ… PASS' : 'āŒ FAIL'}`); console.log(` Best Practices >= 90%: ${bpResults.length}/${validResults.length} pages ${bpResults.length === validResults.length ? 'āœ… PASS' : 'āŒ FAIL'}`); // Final verdict const criticalPass = a11yResults.length === validResults.length && bpResults.length === validResults.length; console.log('\n' + '═'.repeat(60)); if (criticalPass) { console.log('āœ… OVERALL: PASS'); console.log(' Accessibility and Best Practices meet 90% threshold.'); console.log(' Performance and SEO scores are expected to be lower in dev mode.'); console.log(' Verify production scores at: https://pagespeed.web.dev/'); } else { console.log('āŒ OVERALL: FAIL'); console.log(' Some pages do not meet critical thresholds.'); console.log(' Review the results above and fix any issues.'); } console.log('═'.repeat(60) + '\n'); // Exit with appropriate code process.exit(criticalPass ? 0 : 1); } main().catch((error) => { console.error('Fatal error:', error); process.exit(1); });