Skip to content

Commit 91cab8d

Browse files
anth-volkclaude
andcommitted
Add Playwright visual regression testing
Self-hosted full-page E2E visual testing alongside existing Chromatic/Storybook component tests. Uses Docker run-server pattern for consistent Linux rendering locally and in CI, with zero cost on public repos. - Add @playwright/test with Docker browser server config - Add homepage visual smoke test with baseline screenshot - Add visual-tests job to PR and push CI workflows - Add pw-test, pw-update, pw-report Makefile targets Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 657cfe9 commit 91cab8d

11 files changed

Lines changed: 207 additions & 5 deletions

File tree

.github/workflows/pr.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,48 @@ jobs:
113113
done
114114
if [ "$FAILED" = "1" ]; then exit 1; fi
115115
116+
visual-tests:
117+
name: Visual regression
118+
runs-on: ubuntu-latest
119+
container:
120+
image: mcr.microsoft.com/playwright:v1.58.2-noble
121+
options: --ipc=host
122+
123+
steps:
124+
- name: Check out repository
125+
uses: actions/checkout@v4
126+
127+
- name: Set up Bun
128+
uses: oven-sh/setup-bun@v2
129+
130+
- name: Install dependencies
131+
run: bun install --frozen-lockfile
132+
133+
- name: Build design system
134+
run: bun run design-system:build
135+
136+
- name: Run visual tests
137+
working-directory: ./app
138+
run: npx playwright test --grep @visual
139+
env:
140+
HOME: /root
141+
142+
- name: Upload HTML report
143+
uses: actions/upload-artifact@v4
144+
if: always()
145+
with:
146+
name: playwright-report
147+
path: app/playwright-report/
148+
retention-days: 14
149+
150+
- name: Upload test results
151+
uses: actions/upload-artifact@v4
152+
if: failure()
153+
with:
154+
name: visual-test-results
155+
path: app/test-results/
156+
retention-days: 7
157+
116158
build:
117159
name: Build
118160
runs-on: ubuntu-latest

.github/workflows/push.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,48 @@ jobs:
104104
done
105105
if [ "$FAILED" = "1" ]; then exit 1; fi
106106
107+
visual-tests:
108+
name: Visual regression
109+
runs-on: ubuntu-latest
110+
container:
111+
image: mcr.microsoft.com/playwright:v1.58.2-noble
112+
options: --ipc=host
113+
114+
steps:
115+
- name: Check out repository
116+
uses: actions/checkout@v4
117+
118+
- name: Set up Bun
119+
uses: oven-sh/setup-bun@v2
120+
121+
- name: Install dependencies
122+
run: bun install --frozen-lockfile
123+
124+
- name: Build design system
125+
run: bun run design-system:build
126+
127+
- name: Run visual tests
128+
working-directory: ./app
129+
run: npx playwright test --grep @visual
130+
env:
131+
HOME: /root
132+
133+
- name: Upload HTML report
134+
uses: actions/upload-artifact@v4
135+
if: always()
136+
with:
137+
name: playwright-report
138+
path: app/playwright-report/
139+
retention-days: 14
140+
141+
- name: Upload test results
142+
uses: actions/upload-artifact@v4
143+
if: failure()
144+
with:
145+
name: visual-test-results
146+
path: app/test-results/
147+
retention-days: 7
148+
107149
build:
108150
name: Build
109151
runs-on: ubuntu-latest

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ coverage/
99
*.lcov
1010
.nyc_output
1111

12+
# Playwright
13+
app/test-results/
14+
app/playwright-report/
15+
app/blob-report/
16+
1217
# Production
1318
dist/
1419
dist-*/

Makefile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: help install dev build test lint format clean deploy
1+
.PHONY: help install dev build test lint format clean deploy pw-test pw-update pw-report
22

33
help:
44
@echo "Available commands:"
@@ -11,6 +11,9 @@ help:
1111
@echo " make format - Format code"
1212
@echo " make clean - Clean build artifacts"
1313
@echo " make deploy - Build and deploy to GitHub Pages"
14+
@echo " make pw-test - Run Playwright visual tests"
15+
@echo " make pw-update - Update visual test baselines"
16+
@echo " make pw-report - Open Playwright HTML report"
1417

1518
install:
1619
bun install
@@ -41,3 +44,12 @@ clean:
4144

4245
deploy: build
4346
@echo "Build complete. GitHub Actions will handle deployment"
47+
48+
pw-test:
49+
cd app && npx playwright test --grep @visual
50+
51+
pw-update:
52+
cd app && npx playwright test --grep @visual --update-snapshots
53+
54+
pw-report:
55+
cd app && bunx playwright show-report
74.6 KB
Loading
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test.describe('Homepage', () => {
4+
test('renders correctly @visual', async ({ page }) => {
5+
await page.goto('/');
6+
await page.waitForLoadState('networkidle');
7+
await expect(page).toHaveScreenshot('homepage.png', {
8+
fullPage: true,
9+
});
10+
});
11+
});

app/e2e/visual/screenshot.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* Applied during Playwright visual screenshots to eliminate flaky dynamic content. */
2+
3+
*,
4+
*::before,
5+
*::after {
6+
animation-duration: 0s !important;
7+
animation-delay: 0s !important;
8+
transition-duration: 0s !important;
9+
transition-delay: 0s !important;
10+
scroll-behavior: auto !important;
11+
}

app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"devDependencies": {
7272
"@eslint/js": "^9.29.0",
7373
"@ianvs/prettier-plugin-sort-imports": "^4.4.2",
74+
"@playwright/test": "^1.58.2",
7475
"@storybook/addon-essentials": "^8.6.14",
7576
"@storybook/react": "^8.6.12",
7677
"@storybook/react-vite": "^8.6.12",

app/playwright.config.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
3+
const isCI = !!process.env.CI;
4+
const PW_VERSION = '1.58.2';
5+
const PW_IMAGE = `mcr.microsoft.com/playwright:v${PW_VERSION}-noble`;
6+
const PW_SERVER_PORT = 3200;
7+
8+
export default defineConfig({
9+
testDir: './e2e',
10+
fullyParallel: true,
11+
forbidOnly: isCI,
12+
retries: isCI ? 2 : 0,
13+
workers: isCI ? 1 : undefined,
14+
reporter: isCI ? [['html', { open: 'never' }], ['github']] : [['html', { open: 'on-failure' }]],
15+
16+
snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}-{projectName}{ext}',
17+
18+
expect: {
19+
toHaveScreenshot: {
20+
maxDiffPixelRatio: 0.005,
21+
threshold: 0.2,
22+
animations: 'disabled',
23+
caret: 'hide',
24+
scale: 'css',
25+
stylePath: './e2e/visual/screenshot.css',
26+
},
27+
},
28+
29+
use: {
30+
baseURL: isCI ? 'http://localhost:3000' : 'http://host.docker.internal:3000',
31+
trace: 'on-first-retry',
32+
timezoneId: 'America/New_York',
33+
...(!isCI && {
34+
connectOptions: {
35+
wsEndpoint: `ws://127.0.0.1:${PW_SERVER_PORT}/`,
36+
},
37+
}),
38+
},
39+
40+
webServer: [
41+
{
42+
command: 'VITE_APP_MODE=website npx vite',
43+
url: 'http://localhost:3000',
44+
reuseExistingServer: !isCI,
45+
timeout: 120_000,
46+
},
47+
...(!isCI
48+
? [
49+
{
50+
command: `docker run --rm --init -p ${PW_SERVER_PORT}:${PW_SERVER_PORT} ${PW_IMAGE} npx playwright run-server --port ${PW_SERVER_PORT} --host 0.0.0.0`,
51+
url: `http://localhost:${PW_SERVER_PORT}`,
52+
timeout: 120_000,
53+
reuseExistingServer: !isCI,
54+
gracefulShutdown: { signal: 'SIGTERM' as const, timeout: 10_000 },
55+
},
56+
]
57+
: []),
58+
],
59+
60+
projects: [
61+
{
62+
name: 'chromium',
63+
use: {
64+
...devices['Desktop Chrome'],
65+
viewport: { width: 1280, height: 720 },
66+
},
67+
},
68+
],
69+
});

app/vite.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,5 +123,6 @@ export default defineConfig({
123123
globals: true,
124124
environment: 'jsdom',
125125
setupFiles: './vitest.setup.mjs',
126+
exclude: ['e2e/**', 'node_modules/**'],
126127
},
127128
});

0 commit comments

Comments
 (0)