Skip to content

Commit a9f358d

Browse files
authored
test: Add Lighthouse performance tests using Playwright (#18)
* test: Add Lighthouse performance tests using Playwright - Implement Lighthouse performance tests for MainPage and LobbyPage - Include Lighthouse audit with performance metrics and scores - Add sessionStorage manipulation and URL navigation handling * chore: Update Lighthouse CI workflow steps * fix: fix lighthouse workflow * fix: Fix lighthouse workflow * fix: Fix lighthouse workflow * fix: Fix lighthouse test * fix: Fix lighthouse test * fix: Fix lighthouse test * fix: Fix lighthouse workflow * chore: Change Playwright test configuration - Configure Playwright tests with a 'list' reporter and remote debugging on port 9222. - Add 'lighthouse' test project with a match for 'lighthouse.test.ts'. - Web server runs on port 4173 using 'pnpm start', with server reuse in non-CI environments. * test: Change Lighthouse performance tests - Capture performance, accessibility, best-practices, and SEO scores. - Extract and display key metrics including FCP, LCP, TBT, CLS, and SI. - Results are saved in a structured JSON format in the .lighthouse directory. * chore: Change Lighthouse CI workflow - Results are uploaded as artifacts for review. * chore: Add PR comment about lighthouse score * fix: Fix conflict * fix: Fix conflict * fix: Fix conflict * refactor: Improve lighthouse test structure and organization - Split files by responsibility - Separate utilities - Organize test cases and configurations * refactor: Combine testCases and test code * fix: Fix conflicts
1 parent 11f8742 commit a9f358d

File tree

10 files changed

+1258
-4
lines changed

10 files changed

+1258
-4
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
name: Lighthouse CI
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
types:
8+
- opened
9+
- synchronize
10+
paths:
11+
- "client/**"
12+
- "core/**"
13+
- ".github/workflows/lighthouse-*.yml"
14+
15+
concurrency:
16+
group: ${{ github.workflow }}-${{ github.ref }}
17+
cancel-in-progress: true
18+
19+
jobs:
20+
lighthouse-ci:
21+
runs-on: ubuntu-latest
22+
permissions:
23+
contents: read
24+
pull-requests: write
25+
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@v4
29+
30+
- name: Setup pnpm
31+
uses: pnpm/action-setup@v3
32+
with:
33+
version: 9
34+
run_install: false
35+
36+
- name: Setup Node
37+
uses: actions/setup-node@v4
38+
with:
39+
node-version: 20
40+
cache: "pnpm"
41+
cache-dependency-path: "**/pnpm-lock.yaml"
42+
43+
- name: Install all dependencies
44+
run: pnpm install --frozen-lockfile
45+
46+
- name: Build core package
47+
run: |
48+
echo "Building core package..."
49+
pnpm --filter @troublepainter/core build
50+
echo "Core build output:"
51+
ls -la core/dist/
52+
53+
- name: Verify core build
54+
run: |
55+
if [ ! -f "core/dist/index.mjs" ]; then
56+
echo "Core build failed - index.mjs not found"
57+
exit 1
58+
fi
59+
60+
- name: Install Playwright browsers
61+
working-directory: ./client
62+
run: npx playwright install
63+
64+
- name: Run Lighthouse audit
65+
working-directory: ./client
66+
id: lighthouse
67+
env:
68+
VITE_API_URL: ${{ secrets.VITE_API_URL }}
69+
VITE_SOCKET_URL: ${{ secrets.VITE_SOCKET_URL }}
70+
run: |
71+
pnpm build
72+
pnpm lighthouse
73+
74+
- uses: actions/upload-artifact@v4
75+
if: ${{ !cancelled() }}
76+
with:
77+
name: lighthouse-report
78+
path: |
79+
client/.lighthouse/MainPage.html
80+
client/.lighthouse/LobbyPage.html
81+
retention-days: 30
82+
83+
- name: Create PR Comment
84+
uses: actions/github-script@v6
85+
with:
86+
script: |
87+
const fs = require('fs')
88+
const results = JSON.parse(fs.readFileSync('client/.lighthouse/results.json', 'utf8'))
89+
90+
const getEmoji = (score) => {
91+
if (score >= 90) return '🟢'
92+
else if (score >= 50) return '🟡';
93+
else return '🔴'
94+
}
95+
96+
let comment = '### 🚦 Lighthouse Audit Results\n\n';
97+
98+
results.forEach(result => {
99+
comment += `<details>\n<summary>${result.pageName}</summary>\n\n`
100+
101+
//Categories
102+
comment += '#### Category Scores\n\n';
103+
comment += '| Category | Score |\n|----------|-------|\n';
104+
Object.entries(result.categories).forEach(([key, value]) => {
105+
const emoji = getEmoji(value.score);
106+
comment += `| ${key} | ${emoji} ${value.score} |\n`
107+
})
108+
109+
// Metrics
110+
comment += '\n#### Core Web Vitals & Metrics\n\n';
111+
comment += '| Metric | Value | Score |\n|---------|--------|-------|\n';
112+
Object.entries(result.metrics).forEach(([key, value]) => {
113+
const emoji = getEmoji(value.score);
114+
comment += `| ${key} | ${value.displayValue} | ${emoji} ${value.score} |\n`;
115+
})
116+
117+
comment += '</details>\n\n\n';
118+
})
119+
120+
github.rest.issues.createComment({
121+
owner: context.repo.owner,
122+
repo: context.repo.repo,
123+
issue_number: context.issue.number,
124+
body: comment
125+
})

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ lerna-debug.log*
5252
# Tests
5353
/coverage
5454
/.nyc_output
55+
.lighthouse
56+
test-results
5557

5658
# IDEs and editors
5759
/.idea

client/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"check": "pnpm format:check && pnpm lint:strict",
1616
"fix": "pnpm format && pnpm lint:fix",
1717
"storybook": "storybook dev -p 6006",
18-
"build-storybook": "storybook build"
18+
"build-storybook": "storybook build",
19+
"lighthouse": "playwright test --project=lighthouse"
1920
},
2021
"dependencies": {
2122
"@lottiefiles/dotlottie-react": "^0.10.1",
@@ -32,6 +33,7 @@
3233
"devDependencies": {
3334
"@chromatic-com/storybook": "^3.2.2",
3435
"@eslint/js": "^9.13.0",
36+
"@playwright/test": "^1.49.1",
3537
"@storybook/addon-essentials": "^8.4.1",
3638
"@storybook/addon-interactions": "^8.4.1",
3739
"@storybook/addon-onboarding": "^8.4.1",
@@ -60,6 +62,7 @@
6062
"eslint-plugin-react-refresh": "^0.4.14",
6163
"eslint-plugin-storybook": "^0.10.2",
6264
"globals": "^15.11.0",
65+
"playwright-lighthouse": "^4.0.0",
6366
"postcss": "^8.4.47",
6467
"prettier": "^3.3.3",
6568
"prettier-plugin-tailwindcss": "^0.6.8",

client/playwright.config.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { defineConfig } from '@playwright/test';
2+
3+
export default defineConfig({
4+
reporter: 'list',
5+
use: {
6+
launchOptions: {
7+
args: ['--remote-debugging-port=9222'],
8+
headless: true,
9+
},
10+
},
11+
projects: [
12+
{
13+
name: 'lighthouse',
14+
testMatch: 'lighthouse/lighthouse.test.ts',
15+
},
16+
],
17+
webServer: {
18+
command: 'pnpm start',
19+
port: 4173,
20+
reuseExistingServer: !process.env.CI,
21+
},
22+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const BASE_URL = 'http://localhost:4173';
2+
export const LIGHTHOUSE_CONFIG = {
3+
port: 9222,
4+
thresholds: {
5+
performance: 0,
6+
accessibility: 0,
7+
'best-practices': 0,
8+
seo: 0,
9+
},
10+
reports: {
11+
formats: { html: true },
12+
directory: './.lighthouse',
13+
},
14+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import test from '@playwright/test';
2+
import { Page } from '@playwright/test';
3+
import { BASE_URL } from './lighthouse.config';
4+
import { TestCase } from './lighthouse.type';
5+
import { runPerformanceTest } from './lighthouse.util';
6+
7+
export const testCases: TestCase[] = [
8+
{
9+
url: BASE_URL,
10+
pageName: 'MainPage',
11+
},
12+
{
13+
url: BASE_URL,
14+
pageName: 'LobbyPage',
15+
setup: async (page: Page) => {
16+
await page.getByRole('button', { name: '방 만들기' }).click();
17+
await page.waitForURL(`${BASE_URL}/lobby/*`);
18+
},
19+
},
20+
];
21+
22+
test.describe('Lighthouse Performance Tests', () => {
23+
for (const testCase of testCases) {
24+
test(`${testCase.pageName} Performance Check`, async () => {
25+
await runPerformanceTest(testCase);
26+
});
27+
}
28+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Page } from '@playwright/test';
2+
3+
type CategoryName = 'performance' | 'accessibility' | 'best-practices' | 'seo';
4+
5+
export type MetricName =
6+
| 'first-contentful-paint'
7+
| 'largest-contentful-paint'
8+
| 'total-blocking-time'
9+
| 'cumulative-layout-shift'
10+
| 'speed-index';
11+
12+
export type MetricNickname = 'FCP' | 'LCP' | 'TBT' | 'CLS' | 'SI';
13+
14+
export interface MetricValue {
15+
displayValue: string;
16+
score: number;
17+
}
18+
19+
export interface CategoryValue {
20+
score: number;
21+
}
22+
23+
export type Categories = Record<CategoryName, CategoryValue>;
24+
export type Metrics = Record<MetricNickname, MetricValue>;
25+
26+
export interface LighthouseResult {
27+
pageName: string;
28+
categories: Categories;
29+
metrics: Metrics;
30+
}
31+
32+
export interface TestCase {
33+
url: string;
34+
pageName: string;
35+
setup?: (page: Page) => Promise<void>;
36+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
2+
import { join } from 'path';
3+
import { Page, chromium } from '@playwright/test';
4+
import { playAudit, playwrightLighthouseResult } from 'playwright-lighthouse';
5+
import { LIGHTHOUSE_CONFIG } from './lighthouse.config';
6+
import { Categories, LighthouseResult, MetricName, Metrics, TestCase } from './lighthouse.type';
7+
8+
const RESULTS_DIR = './.lighthouse';
9+
const results: LighthouseResult[] = [];
10+
11+
export const runAudit = async (page: Page, pageName: string): Promise<playwrightLighthouseResult> => {
12+
return playAudit({
13+
page,
14+
...LIGHTHOUSE_CONFIG,
15+
reports: {
16+
...LIGHTHOUSE_CONFIG.reports,
17+
name: pageName,
18+
},
19+
});
20+
};
21+
22+
export const getMetricScore = (result: playwrightLighthouseResult, metricName: MetricName) => {
23+
const audit = result.lhr.audits[metricName];
24+
return {
25+
displayValue: audit.displayValue || '',
26+
score: (audit.score || 0) * 100,
27+
};
28+
};
29+
30+
export const extractResults = (result: playwrightLighthouseResult) => {
31+
const categories: Categories = {
32+
performance: { score: (result.lhr.categories.performance?.score || 0) * 100 },
33+
accessibility: { score: (result.lhr.categories.accessibility?.score || 0) * 100 },
34+
'best-practices': { score: (result.lhr.categories['best-practices']?.score || 0) * 100 },
35+
seo: { score: (result.lhr.categories.seo?.score || 0) * 100 },
36+
};
37+
38+
const metrics: Metrics = {
39+
FCP: getMetricScore(result, 'first-contentful-paint'),
40+
LCP: getMetricScore(result, 'largest-contentful-paint'),
41+
TBT: getMetricScore(result, 'total-blocking-time'),
42+
CLS: getMetricScore(result, 'cumulative-layout-shift'),
43+
SI: getMetricScore(result, 'speed-index'),
44+
};
45+
46+
return { categories, metrics };
47+
};
48+
49+
export const saveResult = (result: LighthouseResult) => {
50+
results.push(result);
51+
persistResults();
52+
};
53+
54+
export const persistResults = () => {
55+
if (!existsSync(RESULTS_DIR)) {
56+
mkdirSync(RESULTS_DIR, { recursive: true });
57+
}
58+
writeFileSync(join(RESULTS_DIR, 'results.json'), JSON.stringify(results, null, 2));
59+
};
60+
61+
export const printScores = (result: LighthouseResult) => {
62+
console.log(`\n-----${result.pageName}-----`);
63+
console.table(result.categories);
64+
console.table(result.metrics);
65+
};
66+
67+
export const initBrowser = async () => {
68+
const browser = await chromium.launch();
69+
const context = await browser.newContext();
70+
const page = await context.newPage();
71+
return { browser, context, page };
72+
};
73+
74+
export const navigateToPage = async (page: Page, { url, setup }: TestCase) => {
75+
await page.goto(url);
76+
if (setup) {
77+
await setup(page);
78+
}
79+
};
80+
81+
export const collectMetrics = async (page: Page, pageName: string) => {
82+
const rawResults = await runAudit(page, pageName);
83+
return {
84+
pageName,
85+
...extractResults(rawResults),
86+
};
87+
};
88+
89+
export const runPerformanceTest = async (config: TestCase) => {
90+
const { browser, page } = await initBrowser();
91+
92+
try {
93+
await navigateToPage(page, config);
94+
const testResults = await collectMetrics(page, config.pageName);
95+
printScores(testResults);
96+
saveResult(testResults);
97+
} finally {
98+
await browser.close();
99+
}
100+
};

client/tsconfig.app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@
2929
"@/*": ["./src/*"]
3030
}
3131
},
32-
"include": ["src/**/*.ts", "src/**/*.tsx", ".storybook/**/*", "vite.config.ts"],
32+
"include": ["src/**/*.ts", "src/**/*.tsx", ".storybook/**/*", "vite.config.ts", "playwright.config.ts"],
3333
"exclude": ["node_modules", "dist"]
3434
}

0 commit comments

Comments
 (0)