diff --git a/scripts/lighthouse-audit-all-locales.js b/scripts/lighthouse-audit-all-locales.js new file mode 100644 index 0000000..e3bd883 --- /dev/null +++ b/scripts/lighthouse-audit-all-locales.js @@ -0,0 +1,296 @@ +#!/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); +});