Rocky_Mountain_Vending/scripts/lighthouse-test.js
DMleadgen 46d973904b
Initial commit: Rocky Mountain Vending website
Next.js website for Rocky Mountain Vending company featuring:
- Product catalog with Stripe integration
- Service areas and parts pages
- Admin dashboard with Clerk authentication
- SEO optimized pages with JSON-LD structured data

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 16:22:15 -07:00

342 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();