Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"eslint-plugin-react-refresh": "^0.4.14",
"eslint-plugin-storybook": "^0.10.2",
"globals": "^15.11.0",
"playwright-lighthouse": "^4.0.0",
"lighthouse": "^12.8.0",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.8",
Expand Down
155 changes: 155 additions & 0 deletions client/src/tests/lighthouse/lighthouse.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { Page, chromium } from '@playwright/test';
import lighthouse, { Result } from 'lighthouse';
import { LIGHTHOUSE_CONFIG } from './lighthouse.config';
import { TestCase, LighthouseResult, Score, CategoryName, Categories, Metrics, MetricName } from './lighthouse.type';

const results: LighthouseResult[] = [];

export const compare = (thresholds: Record<CategoryName, Score>, newValue: Record<CategoryName, Score>) => {
const errors: string[] = [];
const results: string[] = [];
const CATEGORY_NAMES: CategoryName[] = ['performance', 'accessibility', 'best-practices', 'seo'];

CATEGORY_NAMES.forEach((key) => {
const thresholdValue = thresholds[key];
const actualValue = newValue[key];

if (thresholdValue > actualValue) {
errors.push(`${key} record is ${actualValue} and is under the ${thresholdValue} threshold`);
} else {
results.push(`${key} record is ${actualValue} and desired threshold was ${thresholdValue}`);
}
});

return { errors, results };
};

const runAudit = async (url: string, pageName: string) => {
const options = {
port: LIGHTHOUSE_CONFIG.port,
onlyCategories: Object.keys(LIGHTHOUSE_CONFIG.thresholds),
output: 'html' as const,
};

try {
const result = await lighthouse(url, options);

if (!result) throw new Error('Lighthouse audit failed');

// HTML ๋ณด๊ณ ์„œ ์ €์žฅ
if (LIGHTHOUSE_CONFIG.reports.formats.html) {
if (!existsSync(LIGHTHOUSE_CONFIG.reports.directory))
mkdirSync(LIGHTHOUSE_CONFIG.reports.directory, { recursive: true });
const reportContent = Array.isArray(result.report) ? result.report[0] : result.report;
writeFileSync(join(LIGHTHOUSE_CONFIG.reports.directory, `${pageName}.html`), reportContent);
}

return result.lhr;
} catch (error) {
console.error('Lighthouse audit failed:', error);
}
};

const getMetricScore = (result: Result, metricName: MetricName) => {
const audit = result.audits[metricName];
return {
displayValue: audit?.displayValue || '',
score: Math.round((audit?.score || 0) * 100),
};
};

const extractResults = (result: Result, pageName: string) => {
const categories: Categories = {
performance: { score: Math.round((result.categories.performance?.score || 0) * 100) },
accessibility: { score: Math.round((result.categories.accessibility?.score || 0) * 100) },
'best-practices': { score: Math.round((result.categories['best-practices']?.score || 0) * 100) },
seo: { score: Math.round((result.categories.seo?.score || 0) * 100) },
};

const metrics: Metrics = {
FCP: getMetricScore(result, 'first-contentful-paint'),
LCP: getMetricScore(result, 'largest-contentful-paint'),
TBT: getMetricScore(result, 'total-blocking-time'),
CLS: getMetricScore(result, 'cumulative-layout-shift'),
SI: getMetricScore(result, 'speed-index'),
};

return {
pageName,
categories,
metrics,
};
};

const saveResult = (result: LighthouseResult) => {
results.push(result);
persistResults();
};

const persistResults = () => {
if (!existsSync(LIGHTHOUSE_CONFIG.reports.directory))
mkdirSync(LIGHTHOUSE_CONFIG.reports.directory, { recursive: true });
writeFileSync(join(LIGHTHOUSE_CONFIG.reports.directory, 'results.json'), JSON.stringify(results, null, 2));
};

export const printResult = (result: LighthouseResult) => {
console.log(`\n-----${result.pageName}-----`);
console.log('\n๐Ÿ“Š Categories:');
console.table(result.categories);
console.log('\nโšก Metrics:');
console.table(result.metrics);

// ์ž„๊ณ„๊ฐ’ ์ฒดํฌ (categories๋งŒ)
const categoryScores: Record<CategoryName, Score> = {
performance: result.categories.performance.score,
accessibility: result.categories.accessibility.score,
'best-practices': result.categories['best-practices'].score,
seo: result.categories.seo.score,
};

const comparison = compare(LIGHTHOUSE_CONFIG.thresholds, categoryScores);

if (comparison.results.length > 0) {
console.log('\nโœ… Passed thresholds:');
comparison.results.forEach((res) => console.log(` ${res}`));
}

if (comparison.errors.length > 0) {
console.log('\nโŒ Failed thresholds:');
comparison.errors.forEach((error) => console.log(` ${error}`));
}
};

export const initBrowser = async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
return { browser, context, page };
};

export const navigateToPage = async (page: Page, { url, setup }: TestCase) => {
await page.goto(url);
if (setup) await setup(page);
await page.waitForLoadState('networkidle');
return page.url();
};

export const runPerformanceTest = async (testCase: TestCase): Promise<LighthouseResult | undefined> => {
const { browser, page } = await initBrowser();
try {
const finalURL = await navigateToPage(page, testCase);
const audit = await runAudit(finalURL, testCase.pageName);
if (!audit) return;

const result = extractResults(audit, testCase.pageName);
saveResult(result);
printResult(result);
return result;
} catch (err) {
console.error(`[Error] Failed performance test:`, err);
} finally {
await browser.close();
}
};
19 changes: 14 additions & 5 deletions client/src/tests/lighthouse/lighthouse.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import test from '@playwright/test';
import test, { expect } from '@playwright/test';
import { Page } from '@playwright/test';
import { BASE_URL } from './lighthouse.config';
import { BASE_URL, LIGHTHOUSE_CONFIG } from './lighthouse.config';
import { runPerformanceTest } from './lighthouse.helper';
import { TestCase } from './lighthouse.type';
import { runPerformanceTest } from './lighthouse.util';

export const testCases: TestCase[] = [
const testCases: TestCase[] = [
{
url: BASE_URL,
pageName: 'MainPage',
Expand All @@ -22,7 +22,16 @@ export const testCases: TestCase[] = [
test.describe('Lighthouse Performance Tests', () => {
for (const testCase of testCases) {
test(`${testCase.pageName} Performance Check`, async () => {
await runPerformanceTest(testCase);
const result = await runPerformanceTest(testCase);
expect(result).toBeTruthy();

const { categories } = result!;
const { thresholds } = LIGHTHOUSE_CONFIG;

expect(categories.performance.score).toBeGreaterThanOrEqual(thresholds.performance);
expect(categories.accessibility.score).toBeGreaterThanOrEqual(thresholds.accessibility);
expect(categories['best-practices'].score).toBeGreaterThanOrEqual(thresholds['best-practices']);
expect(categories.seo.score).toBeGreaterThanOrEqual(thresholds.seo);
});
}
});
20 changes: 11 additions & 9 deletions client/src/tests/lighthouse/lighthouse.type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Page } from '@playwright/test';

type CategoryName = 'performance' | 'accessibility' | 'best-practices' | 'seo';
export interface TestCase {
url: string;
pageName: string;
setup?: (page: Page) => Promise<void>;
}

export type CategoryName = 'performance' | 'accessibility' | 'best-practices' | 'seo';

export type MetricName =
| 'first-contentful-paint'
Expand All @@ -11,13 +17,15 @@ export type MetricName =

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

export type Score = number;

export interface MetricValue {
displayValue: string;
score: number;
score: Score;
}

export interface CategoryValue {
score: number;
score: Score;
}

export type Categories = Record<CategoryName, CategoryValue>;
Expand All @@ -28,9 +36,3 @@ export interface LighthouseResult {
categories: Categories;
metrics: Metrics;
}

export interface TestCase {
url: string;
pageName: string;
setup?: (page: Page) => Promise<void>;
}
100 changes: 0 additions & 100 deletions client/src/tests/lighthouse/lighthouse.util.ts

This file was deleted.

Loading