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