4.9 KiB
4.9 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| testing-visual-regression | Visual Regression — Percy / Chromatic / Playwright | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Visual Regression Testing
디자인 변경 = unit test 가 못 잡음. screenshot 비교 = pixel diff. Chromatic (Storybook), Percy (Browserstack), Applitools (AI), Playwright snapshot.
📖 핵심 개념
- Baseline: 첫 screenshot.
- Diff: PR 가 baseline 과 다름 → 사람 review.
- Approved: 의도된 변경 → 새 baseline.
- Cross-browser / device: 같은 컴포넌트 다양한 환경.
💻 코드 패턴
Playwright snapshot
import { test, expect } from '@playwright/test';
test('home page visual', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('home.png', {
maxDiffPixelRatio: 0.01,
fullPage: true,
});
});
# 첫 run — baseline 생성
npx playwright test --update-snapshots
# 이후 비교
npx playwright test
Storybook + Chromatic
yarn add -D chromatic
# .github/workflows/chromatic.yml
- run: yarn build-storybook
- uses: chromaui/action@v1
with:
projectToken: ${{ secrets.CHROMATIC_TOKEN }}
→ 각 story 자동 screenshot + diff. PR 에 review UI.
Story 기준 component 단위
// Button.stories.tsx
import { Button } from './Button';
export default { component: Button };
export const Primary = { args: { variant: 'primary', children: 'Click' } };
export const Disabled = { args: { variant: 'primary', disabled: true, children: 'Click' } };
export const Long = { args: { children: 'Very long button text here' } };
→ Chromatic 가 모든 story 자동 snapshot.
Multi-viewport / theme
export default {
component: Card,
parameters: {
chromatic: {
viewports: [320, 768, 1280],
modes: { light: { theme: 'light' }, dark: { theme: 'dark' } },
},
},
};
Flake 방어
// 시간 / random / animation 고정
parameters: {
chromatic: {
pauseAnimationAtEnd: true,
delay: 500, // 데이터 load 기다림
},
}
// 시간 mock (Playwright)
await page.evaluate(() => {
const fixedDate = new Date('2026-05-09T10:00:00Z').valueOf();
Date.now = () => fixedDate;
});
// Font ready
await page.waitForLoadState('networkidle');
await page.evaluate(() => document.fonts.ready);
Threshold
await expect(page).toHaveScreenshot('home.png', {
maxDiffPixelRatio: 0.005, // 0.5% pixel 차이까지 OK (anti-aliasing)
threshold: 0.1, // pixel 차이 정도
});
Mask (변경 영역 가리기)
await expect(page).toHaveScreenshot({
mask: [page.locator('.timestamp'), page.locator('[data-dynamic]')],
});
→ 시간 / random 부분 mask.
Cross-browser
// playwright.config.ts
projects: [
{ name: 'chromium', use: devices['Desktop Chrome'] },
{ name: 'firefox', use: devices['Desktop Firefox'] },
{ name: 'webkit', use: devices['Desktop Safari'] },
],
→ 각 browser 별 baseline.
Applitools (AI 기반)
import { Eyes, Target } from '@applitools/eyes-playwright';
test('home', async ({ page }) => {
const eyes = new Eyes();
await eyes.open(page, 'My App', 'home');
await page.goto('/');
await eyes.check('main', Target.window().fully());
await eyes.close();
});
→ AI 가 의미 있는 변경 / 노이즈 구분.
Workflow
1. PR 만듦
2. Chromatic / Percy 가 자동 screenshot
3. PR comment 에 diff URL
4. Designer / dev review
5. Approve = 새 baseline / Reject = 코드 fix
Approve 정책
- 모든 변경 디자이너 review 또는
- Spec 변경 시만 review, 일반 = dev approve.
🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| Storybook 사용 중 | Chromatic |
| 브라우저 / 디바이스 다양 | Percy |
| 정밀 + 의미 분석 | Applitools |
| 자체 / 무료 | Playwright snapshot |
| Storybook 없음 | Playwright + 페이지 스크린샷 |
| Email rendering | Mailosaur / Litmus |
❌ 안티패턴
- Animation 안 정지: 매번 다른 frame.
- 시간 / random data: noise. mock.
- External 이미지 / font load: lazy. preload + ready check.
- 모든 page snapshot: 비용 폭발. 핵심 + storybook component.
- Threshold 0: false positive. 0.5% 정도.
- Baseline 매번 update: 의도 모름. 변경 review 필수.
- Cross-browser 무시: Safari rendering 차이.
🤖 LLM 활용 힌트
- Storybook + Chromatic = 컴포넌트 단위 강력.
- Pause animation + mask dynamic + font ready 3종.
- Threshold 0.5% + cross-browser.