Skip to content

Commit f292120

Browse files
authored
refactor: Migrate from playwright-lighthouse to lighthouse direct usage (#53)
1 parent 9f4cbd5 commit f292120

File tree

6 files changed

+1005
-361
lines changed

6 files changed

+1005
-361
lines changed

client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"eslint-plugin-react-refresh": "^0.4.14",
6363
"eslint-plugin-storybook": "^0.10.2",
6464
"globals": "^15.11.0",
65-
"playwright-lighthouse": "^4.0.0",
65+
"lighthouse": "^12.8.0",
6666
"postcss": "^8.4.47",
6767
"prettier": "^3.3.3",
6868
"prettier-plugin-tailwindcss": "^0.6.8",
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
2+
import { join } from 'path';
3+
import { Page, chromium } from '@playwright/test';
4+
import lighthouse, { Result } from 'lighthouse';
5+
import { LIGHTHOUSE_CONFIG } from './lighthouse.config';
6+
import { TestCase, LighthouseResult, Score, CategoryName, Categories, Metrics, MetricName } from './lighthouse.type';
7+
8+
const results: LighthouseResult[] = [];
9+
10+
export const compare = (thresholds: Record<CategoryName, Score>, newValue: Record<CategoryName, Score>) => {
11+
const errors: string[] = [];
12+
const results: string[] = [];
13+
const CATEGORY_NAMES: CategoryName[] = ['performance', 'accessibility', 'best-practices', 'seo'];
14+
15+
CATEGORY_NAMES.forEach((key) => {
16+
const thresholdValue = thresholds[key];
17+
const actualValue = newValue[key];
18+
19+
if (thresholdValue > actualValue) {
20+
errors.push(`${key} record is ${actualValue} and is under the ${thresholdValue} threshold`);
21+
} else {
22+
results.push(`${key} record is ${actualValue} and desired threshold was ${thresholdValue}`);
23+
}
24+
});
25+
26+
return { errors, results };
27+
};
28+
29+
const runAudit = async (url: string, pageName: string) => {
30+
const options = {
31+
port: LIGHTHOUSE_CONFIG.port,
32+
onlyCategories: Object.keys(LIGHTHOUSE_CONFIG.thresholds),
33+
output: 'html' as const,
34+
};
35+
36+
try {
37+
const result = await lighthouse(url, options);
38+
39+
if (!result) throw new Error('Lighthouse audit failed');
40+
41+
// HTML 보고서 저장
42+
if (LIGHTHOUSE_CONFIG.reports.formats.html) {
43+
if (!existsSync(LIGHTHOUSE_CONFIG.reports.directory))
44+
mkdirSync(LIGHTHOUSE_CONFIG.reports.directory, { recursive: true });
45+
const reportContent = Array.isArray(result.report) ? result.report[0] : result.report;
46+
writeFileSync(join(LIGHTHOUSE_CONFIG.reports.directory, `${pageName}.html`), reportContent);
47+
}
48+
49+
return result.lhr;
50+
} catch (error) {
51+
console.error('Lighthouse audit failed:', error);
52+
}
53+
};
54+
55+
const getMetricScore = (result: Result, metricName: MetricName) => {
56+
const audit = result.audits[metricName];
57+
return {
58+
displayValue: audit?.displayValue || '',
59+
score: Math.round((audit?.score || 0) * 100),
60+
};
61+
};
62+
63+
const extractResults = (result: Result, pageName: string) => {
64+
const categories: Categories = {
65+
performance: { score: Math.round((result.categories.performance?.score || 0) * 100) },
66+
accessibility: { score: Math.round((result.categories.accessibility?.score || 0) * 100) },
67+
'best-practices': { score: Math.round((result.categories['best-practices']?.score || 0) * 100) },
68+
seo: { score: Math.round((result.categories.seo?.score || 0) * 100) },
69+
};
70+
71+
const metrics: Metrics = {
72+
FCP: getMetricScore(result, 'first-contentful-paint'),
73+
LCP: getMetricScore(result, 'largest-contentful-paint'),
74+
TBT: getMetricScore(result, 'total-blocking-time'),
75+
CLS: getMetricScore(result, 'cumulative-layout-shift'),
76+
SI: getMetricScore(result, 'speed-index'),
77+
};
78+
79+
return {
80+
pageName,
81+
categories,
82+
metrics,
83+
};
84+
};
85+
86+
const saveResult = (result: LighthouseResult) => {
87+
results.push(result);
88+
persistResults();
89+
};
90+
91+
const persistResults = () => {
92+
if (!existsSync(LIGHTHOUSE_CONFIG.reports.directory))
93+
mkdirSync(LIGHTHOUSE_CONFIG.reports.directory, { recursive: true });
94+
writeFileSync(join(LIGHTHOUSE_CONFIG.reports.directory, 'results.json'), JSON.stringify(results, null, 2));
95+
};
96+
97+
export const printResult = (result: LighthouseResult) => {
98+
console.log(`\n-----${result.pageName}-----`);
99+
console.log('\n📊 Categories:');
100+
console.table(result.categories);
101+
console.log('\n⚡ Metrics:');
102+
console.table(result.metrics);
103+
104+
// 임계값 체크 (categories만)
105+
const categoryScores: Record<CategoryName, Score> = {
106+
performance: result.categories.performance.score,
107+
accessibility: result.categories.accessibility.score,
108+
'best-practices': result.categories['best-practices'].score,
109+
seo: result.categories.seo.score,
110+
};
111+
112+
const comparison = compare(LIGHTHOUSE_CONFIG.thresholds, categoryScores);
113+
114+
if (comparison.results.length > 0) {
115+
console.log('\n✅ Passed thresholds:');
116+
comparison.results.forEach((res) => console.log(` ${res}`));
117+
}
118+
119+
if (comparison.errors.length > 0) {
120+
console.log('\n❌ Failed thresholds:');
121+
comparison.errors.forEach((error) => console.log(` ${error}`));
122+
}
123+
};
124+
125+
export const initBrowser = async () => {
126+
const browser = await chromium.launch();
127+
const context = await browser.newContext();
128+
const page = await context.newPage();
129+
return { browser, context, page };
130+
};
131+
132+
export const navigateToPage = async (page: Page, { url, setup }: TestCase) => {
133+
await page.goto(url);
134+
if (setup) await setup(page);
135+
await page.waitForLoadState('networkidle');
136+
return page.url();
137+
};
138+
139+
export const runPerformanceTest = async (testCase: TestCase): Promise<LighthouseResult | undefined> => {
140+
const { browser, page } = await initBrowser();
141+
try {
142+
const finalURL = await navigateToPage(page, testCase);
143+
const audit = await runAudit(finalURL, testCase.pageName);
144+
if (!audit) return;
145+
146+
const result = extractResults(audit, testCase.pageName);
147+
saveResult(result);
148+
printResult(result);
149+
return result;
150+
} catch (err) {
151+
console.error(`[Error] Failed performance test:`, err);
152+
} finally {
153+
await browser.close();
154+
}
155+
};

client/src/tests/lighthouse/lighthouse.test.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import test from '@playwright/test';
1+
import test, { expect } from '@playwright/test';
22
import { Page } from '@playwright/test';
3-
import { BASE_URL } from './lighthouse.config';
3+
import { BASE_URL, LIGHTHOUSE_CONFIG } from './lighthouse.config';
4+
import { runPerformanceTest } from './lighthouse.helper';
45
import { TestCase } from './lighthouse.type';
5-
import { runPerformanceTest } from './lighthouse.util';
66

7-
export const testCases: TestCase[] = [
7+
const testCases: TestCase[] = [
88
{
99
url: BASE_URL,
1010
pageName: 'MainPage',
@@ -22,7 +22,16 @@ export const testCases: TestCase[] = [
2222
test.describe('Lighthouse Performance Tests', () => {
2323
for (const testCase of testCases) {
2424
test(`${testCase.pageName} Performance Check`, async () => {
25-
await runPerformanceTest(testCase);
25+
const result = await runPerformanceTest(testCase);
26+
expect(result).toBeTruthy();
27+
28+
const { categories } = result!;
29+
const { thresholds } = LIGHTHOUSE_CONFIG;
30+
31+
expect(categories.performance.score).toBeGreaterThanOrEqual(thresholds.performance);
32+
expect(categories.accessibility.score).toBeGreaterThanOrEqual(thresholds.accessibility);
33+
expect(categories['best-practices'].score).toBeGreaterThanOrEqual(thresholds['best-practices']);
34+
expect(categories.seo.score).toBeGreaterThanOrEqual(thresholds.seo);
2635
});
2736
}
2837
});

client/src/tests/lighthouse/lighthouse.type.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { Page } from '@playwright/test';
22

3-
type CategoryName = 'performance' | 'accessibility' | 'best-practices' | 'seo';
3+
export interface TestCase {
4+
url: string;
5+
pageName: string;
6+
setup?: (page: Page) => Promise<void>;
7+
}
8+
9+
export type CategoryName = 'performance' | 'accessibility' | 'best-practices' | 'seo';
410

511
export type MetricName =
612
| 'first-contentful-paint'
@@ -11,13 +17,15 @@ export type MetricName =
1117

1218
export type MetricNickname = 'FCP' | 'LCP' | 'TBT' | 'CLS' | 'SI';
1319

20+
export type Score = number;
21+
1422
export interface MetricValue {
1523
displayValue: string;
16-
score: number;
24+
score: Score;
1725
}
1826

1927
export interface CategoryValue {
20-
score: number;
28+
score: Score;
2129
}
2230

2331
export type Categories = Record<CategoryName, CategoryValue>;
@@ -28,9 +36,3 @@ export interface LighthouseResult {
2836
categories: Categories;
2937
metrics: Metrics;
3038
}
31-
32-
export interface TestCase {
33-
url: string;
34-
pageName: string;
35-
setup?: (page: Page) => Promise<void>;
36-
}

client/src/tests/lighthouse/lighthouse.util.ts

Lines changed: 0 additions & 100 deletions
This file was deleted.

0 commit comments

Comments
 (0)