main
mjs 137 lines 3.53 KB
Raw
1 #!/usr/bin/env node
2
3 import { execSync } from 'node:child_process';
4 import { mkdir, writeFile } from 'node:fs/promises';
5 import { existsSync } from 'node:fs';
6 import { join } from 'node:path';
7
8 const args = process.argv.slice(2);
9
10 function getArg(name, fallback) {
11 const index = args.indexOf(name);
12 return index !== -1 && args[index + 1] ? args[index + 1] : fallback;
13 }
14
15 const BASE_URL = getArg('--base', 'http://localhost:1313/SquadScope').replace(/\/$/, '');
16 const OUTPUT_DIR = join('screenshots', 'lighthouse-results');
17 const THRESHOLDS = {
18 accessibility: 0.95,
19 bestPractices: 0.95,
20 cls: 0.1,
21 };
22
23 const PAGES = [
24 { key: 'home', path: '/' },
25 { key: 'weekly', path: '/weekly/2026/w22/' },
26 { key: 'monthly', path: '/monthly/2026/05/' },
27 { key: 'yearly', path: '/yearly/2026/' },
28 ];
29
30 function ensureDir(path) {
31 if (!existsSync(path)) {
32 return mkdir(path, { recursive: true });
33 }
34
35 return Promise.resolve();
36 }
37
38 function runLighthouse(url) {
39 const command = [
40 'npx -y lighthouse',
41 JSON.stringify(url),
42 '--quiet',
43 '--output=json',
44 '--output-path=stdout',
45 '--only-categories=accessibility,best-practices,performance',
46 '--chrome-flags="--headless --no-sandbox"',
47 '--form-factor=mobile',
48 ].join(' ');
49
50 const output = execSync(command, {
51 cwd: process.cwd(),
52 encoding: 'utf8',
53 maxBuffer: 20 * 1024 * 1024,
54 stdio: ['ignore', 'pipe', 'pipe'],
55 });
56
57 return JSON.parse(output);
58 }
59
60 function getScores(report) {
61 return {
62 accessibility: report.categories.accessibility?.score ?? 0,
63 bestPractices: report.categories['best-practices']?.score ?? 0,
64 cls: report.audits['cumulative-layout-shift']?.numericValue ?? Number.POSITIVE_INFINITY,
65 };
66 }
67
68 function getFailures(scores) {
69 const failures = [];
70
71 if (scores.accessibility < THRESHOLDS.accessibility) {
72 failures.push(`a11y ${(scores.accessibility * 100).toFixed(0)} < 95`);
73 }
74
75 if (scores.bestPractices < THRESHOLDS.bestPractices) {
76 failures.push(`best ${(scores.bestPractices * 100).toFixed(0)} < 95`);
77 }
78
79 if (scores.cls > THRESHOLDS.cls) {
80 failures.push(`cls ${scores.cls.toFixed(3)} > 0.100`);
81 }
82
83 return failures;
84 }
85
86 function formatPercent(score) {
87 return `${(score * 100).toFixed(0)}%`;
88 }
89
90 function formatCls(value) {
91 return Number.isFinite(value) ? value.toFixed(3) : 'n/a';
92 }
93
94 async function main() {
95 await ensureDir(OUTPUT_DIR);
96
97 const results = [];
98
99 for (const page of PAGES) {
100 const url = `${BASE_URL}${page.path}`;
101 const report = runLighthouse(url);
102 const scores = getScores(report);
103 const failures = getFailures(scores);
104 const result = {
105 page: page.key,
106 url,
107 accessibility: scores.accessibility,
108 bestPractices: scores.bestPractices,
109 cls: scores.cls,
110 ok: failures.length === 0,
111 failures,
112 };
113
114 results.push(result);
115 await writeFile(join(OUTPUT_DIR, `${page.key}.json`), JSON.stringify(report, null, 2));
116 }
117
118 await writeFile(join(OUTPUT_DIR, 'summary.json'), JSON.stringify({ baseUrl: BASE_URL, thresholds: THRESHOLDS, results }, null, 2));
119
120 console.log(`Lighthouse gates for ${BASE_URL}`);
121 console.table(results.map(result => ({
122 page: result.page,
123 accessibility: formatPercent(result.accessibility),
124 bestPractices: formatPercent(result.bestPractices),
125 cls: formatCls(result.cls),
126 status: result.ok ? 'PASS' : `FAIL (${result.failures.join(', ')})`,
127 })));
128
129 if (results.some(result => !result.ok)) {
130 process.exit(1);
131 }
132 }
133
134 main().catch(error => {
135 console.error(error instanceof Error ? error.message : error);
136 process.exit(1);
137 });