#!/usr/bin/env node /** * Lighthouse Testing Script * * This script builds the production bundle, starts the server, and runs Lighthouse tests. * Usage: * npm run lighthouse:build - Test production build * npm run lighthouse:dev - Test dev server (less accurate) */ const { execSync, spawn } = require("child_process") const chromeLauncher = require("chrome-launcher") const path = require("path") // Dynamic import for lighthouse (ES module) let lighthouse async function getLighthouse() { if (!lighthouse) { const lighthouseModule = await import("lighthouse") lighthouse = lighthouseModule.default || lighthouseModule } return lighthouse } const PORT = 3000 const BASE_URL = `http://localhost:${PORT}` const TEST_PAGES = ["/", "/vending-machines", "/services", "/service-areas"] // Check if we should test dev server const isDev = process.argv.includes("--dev") async function buildProduction() { if (isDev) { console.log("āš ļø Testing dev server (less accurate for Lighthouse scores)") return } console.log("šŸ“¦ Building production bundle...") try { execSync("pnpm run build", { stdio: "inherit", cwd: path.join(__dirname, ".."), }) console.log("āœ… Build complete\n") } catch (error) { console.error("āŒ Build failed:", error.message) process.exit(1) } } async function startServer() { console.log( `šŸš€ Starting ${isDev ? "dev" : "production"} server on port ${PORT}...` ) const command = isDev ? "pnpm run dev" : "pnpm run start" const serverProcess = spawn(command, [], { shell: true, stdio: "pipe", cwd: path.join(__dirname, ".."), }) // Wait for server to be ready await new Promise((resolve, reject) => { const timeout = setTimeout(() => { serverProcess.kill() reject(new Error("Server startup timeout")) }, 60000) serverProcess.stdout.on("data", (data) => { const output = data.toString() if ( output.includes("ready") || output.includes("Local:") || output.includes("started server") ) { clearTimeout(timeout) // Give server a moment to fully start setTimeout(resolve, 2000) } }) serverProcess.stderr.on("data", (data) => { const output = data.toString() if ( output.includes("ready") || output.includes("Local:") || output.includes("started server") ) { clearTimeout(timeout) setTimeout(resolve, 2000) } }) }) console.log("āœ… Server ready\n") return serverProcess } async function runLighthouse(url, options = {}) { const deviceType = options.formFactor || "desktop" console.log(`šŸ” Running Lighthouse on ${url} (${deviceType})...`) const lighthouseFn = await getLighthouse() const chrome = await chromeLauncher.launch({ chromeFlags: ["--headless"] }) const runnerResult = await lighthouseFn(url, { port: chrome.port, output: "json", onlyCategories: ["performance", "accessibility", "best-practices", "seo"], ...options, }) await chrome.kill() const scores = { performance: Math.round( runnerResult.lhr.categories.performance.score * 100 ), accessibility: Math.round( runnerResult.lhr.categories.accessibility.score * 100 ), "best-practices": Math.round( runnerResult.lhr.categories["best-practices"].score * 100 ), seo: Math.round(runnerResult.lhr.categories.seo.score * 100), } const metrics = { fcp: runnerResult.lhr.audits["first-contentful-paint"].numericValue, lcp: runnerResult.lhr.audits["largest-contentful-paint"].numericValue, cls: runnerResult.lhr.audits["cumulative-layout-shift"].numericValue, tbt: runnerResult.lhr.audits["total-blocking-time"].numericValue, si: runnerResult.lhr.audits["speed-index"].numericValue, } // Get failing audits for debugging const failingAudits = Object.values(runnerResult.lhr.audits) .filter((audit) => audit.score !== null && audit.score < 1) .map((audit) => ({ id: audit.id, title: audit.title, score: audit.score, displayValue: audit.displayValue, })) .sort((a, b) => (a.score || 0) - (b.score || 0)) return { scores, metrics, lhr: runnerResult.lhr, deviceType, failingAudits } } async function testPages() { const results = [] // Test desktop first console.log("\n" + "=".repeat(60)) console.log("šŸ–„ļø TESTING DESKTOP") console.log("=".repeat(60) + "\n") for (const page of TEST_PAGES) { const url = `${BASE_URL}${page}` try { const result = await runLighthouse(url, { formFactor: "desktop", throttling: { rttMs: 40, throughputKbps: 10 * 1024, cpuSlowdownMultiplier: 1, }, screenEmulation: { mobile: false, width: 1350, height: 940, deviceScaleFactor: 1, }, }) results.push({ page, ...result, device: "desktop" }) console.log(`\nšŸ“Š Desktop Results for ${page}:`) console.log(` Performance: ${result.scores.performance}/100`) console.log(` Accessibility: ${result.scores.accessibility}/100`) console.log(` Best Practices: ${result.scores["best-practices"]}/100`) console.log(` SEO: ${result.scores.seo}/100`) console.log(` FCP: ${result.metrics.fcp.toFixed(0)}ms`) console.log(` LCP: ${result.metrics.lcp.toFixed(0)}ms`) console.log(` CLS: ${result.metrics.cls.toFixed(3)}`) console.log(` TBT: ${result.metrics.tbt.toFixed(0)}ms`) console.log(` SI: ${result.metrics.si.toFixed(0)}ms`) // Show top failing audits if scores aren't 100% if ( result.scores.performance < 100 || result.scores.accessibility < 100 || result.scores["best-practices"] < 100 || result.scores.seo < 100 ) { const topFailures = result.failingAudits.slice(0, 5) if (topFailures.length > 0) { console.log(` Top Issues:`) topFailures.forEach((audit) => { console.log( ` - ${audit.title} (${(audit.score * 100).toFixed(0)}%)` ) }) } } console.log() } catch (error) { console.error(`āŒ Failed to test ${page} (desktop):`, error.message) results.push({ page, error: error.message, device: "desktop" }) } } // Test mobile console.log("\n" + "=".repeat(60)) console.log("šŸ“± TESTING MOBILE") console.log("=".repeat(60) + "\n") for (const page of TEST_PAGES) { const url = `${BASE_URL}${page}` try { const result = await runLighthouse(url, { formFactor: "mobile", throttling: { rttMs: 150, throughputKbps: 1.6 * 1024, cpuSlowdownMultiplier: 4, }, screenEmulation: { mobile: true, width: 412, height: 823, deviceScaleFactor: 2.625, }, }) results.push({ page, ...result, device: "mobile" }) console.log(`\nšŸ“Š Mobile Results for ${page}:`) console.log(` Performance: ${result.scores.performance}/100`) console.log(` Accessibility: ${result.scores.accessibility}/100`) console.log(` Best Practices: ${result.scores["best-practices"]}/100`) console.log(` SEO: ${result.scores.seo}/100`) console.log(` FCP: ${result.metrics.fcp.toFixed(0)}ms`) console.log(` LCP: ${result.metrics.lcp.toFixed(0)}ms`) console.log(` CLS: ${result.metrics.cls.toFixed(3)}`) console.log(` TBT: ${result.metrics.tbt.toFixed(0)}ms`) console.log(` SI: ${result.metrics.si.toFixed(0)}ms`) // Show top failing audits if scores aren't 100% if ( result.scores.performance < 100 || result.scores.accessibility < 100 || result.scores["best-practices"] < 100 || result.scores.seo < 100 ) { const topFailures = result.failingAudits.slice(0, 5) if (topFailures.length > 0) { console.log(` Top Issues:`) topFailures.forEach((audit) => { console.log( ` - ${audit.title} (${(audit.score * 100).toFixed(0)}%)` ) }) } } console.log() } catch (error) { console.error(`āŒ Failed to test ${page} (mobile):`, error.message) results.push({ page, error: error.message, device: "mobile" }) } } return results } function checkScores(results) { console.log("\n" + "=".repeat(60)) console.log("šŸ“ˆ FINAL SCORES SUMMARY") console.log("=".repeat(60) + "\n") let allPassed = true const minScore = 100 // Group results by device const desktopResults = results.filter((r) => r.device === "desktop") const mobileResults = results.filter((r) => r.device === "mobile") console.log("šŸ–„ļø DESKTOP RESULTS:\n") for (const result of desktopResults) { if (result.error) { console.log(`āŒ ${result.page}: ERROR - ${result.error}\n`) allPassed = false continue } const { scores } = result const passed = scores.performance >= minScore && scores.accessibility >= minScore && scores["best-practices"] >= minScore && scores.seo >= minScore if (passed) { console.log(`āœ… ${result.page}: All scores at 100%`) } else { console.log(`āŒ ${result.page}:`) if (scores.performance < minScore) console.log(` Performance: ${scores.performance}/100`) if (scores.accessibility < minScore) console.log(` Accessibility: ${scores.accessibility}/100`) if (scores["best-practices"] < minScore) console.log(` Best Practices: ${scores["best-practices"]}/100`) if (scores.seo < minScore) console.log(` SEO: ${scores.seo}/100`) allPassed = false } console.log() } console.log("\nšŸ“± MOBILE RESULTS:\n") for (const result of mobileResults) { if (result.error) { console.log(`āŒ ${result.page}: ERROR - ${result.error}\n`) allPassed = false continue } const { scores } = result const passed = scores.performance >= minScore && scores.accessibility >= minScore && scores["best-practices"] >= minScore && scores.seo >= minScore if (passed) { console.log(`āœ… ${result.page}: All scores at 100%`) } else { console.log(`āŒ ${result.page}:`) if (scores.performance < minScore) console.log(` Performance: ${scores.performance}/100`) if (scores.accessibility < minScore) console.log(` Accessibility: ${scores.accessibility}/100`) if (scores["best-practices"] < minScore) console.log(` Best Practices: ${scores["best-practices"]}/100`) if (scores.seo < minScore) console.log(` SEO: ${scores.seo}/100`) allPassed = false } console.log() } if (allPassed) { console.log( "šŸŽ‰ All pages passed with 100% scores on both desktop and mobile!\n" ) } else { console.log( "āš ļø Some pages did not achieve 100% scores. Review the results above.\n" ) process.exit(1) } } async function main() { try { await buildProduction() const serverProcess = await startServer() try { const results = await testPages() checkScores(results) } finally { console.log("šŸ›‘ Stopping server...") serverProcess.kill() } } catch (error) { console.error("āŒ Error:", error.message) process.exit(1) } } main()