379 lines
11 KiB
JavaScript
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()
|