Rocky_Mountain_Vending/scripts/lighthouse-test.js

379 lines
11 KiB
JavaScript

#!/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()