[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
---
|
||||
id: testing-visual-regression
|
||||
title: Visual Regression — Percy / Chromatic / Playwright
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [testing, visual, vrt, screenshot, vibe-coding]
|
||||
tech_stack: { language: "TS / Playwright / Storybook", applicable_to: ["Frontend"] }
|
||||
applied_in: []
|
||||
aliases: [visual regression, VRT, Chromatic, Percy, Applitools, screenshot diff]
|
||||
---
|
||||
|
||||
# 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
|
||||
```ts
|
||||
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,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```bash
|
||||
# 첫 run — baseline 생성
|
||||
npx playwright test --update-snapshots
|
||||
|
||||
# 이후 비교
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
### Storybook + Chromatic
|
||||
```bash
|
||||
yarn add -D chromatic
|
||||
```
|
||||
|
||||
```yaml
|
||||
# .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 단위
|
||||
```tsx
|
||||
// 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
|
||||
```tsx
|
||||
export default {
|
||||
component: Card,
|
||||
parameters: {
|
||||
chromatic: {
|
||||
viewports: [320, 768, 1280],
|
||||
modes: { light: { theme: 'light' }, dark: { theme: 'dark' } },
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Flake 방어
|
||||
```tsx
|
||||
// 시간 / random / animation 고정
|
||||
parameters: {
|
||||
chromatic: {
|
||||
pauseAnimationAtEnd: true,
|
||||
delay: 500, // 데이터 load 기다림
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// 시간 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
|
||||
```ts
|
||||
await expect(page).toHaveScreenshot('home.png', {
|
||||
maxDiffPixelRatio: 0.005, // 0.5% pixel 차이까지 OK (anti-aliasing)
|
||||
threshold: 0.1, // pixel 차이 정도
|
||||
});
|
||||
```
|
||||
|
||||
### Mask (변경 영역 가리기)
|
||||
```ts
|
||||
await expect(page).toHaveScreenshot({
|
||||
mask: [page.locator('.timestamp'), page.locator('[data-dynamic]')],
|
||||
});
|
||||
```
|
||||
|
||||
→ 시간 / random 부분 mask.
|
||||
|
||||
### Cross-browser
|
||||
```ts
|
||||
// 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 기반)
|
||||
```ts
|
||||
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.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Testing_Playwright_Advanced]]
|
||||
- [[Frontend_A11y_Testing]]
|
||||
- [[Frontend_Design_Tokens]]
|
||||
Reference in New Issue
Block a user