Adaptive Interfaces를 위한 Visual Regression: Crisis Mode가 실제로 다르게 보이는지 테스트
Source: Dev.to
소개
상태 관리는 테스트하기 쉽지만, 시각적 변형은 그렇지 않습니다. 위기 상황에서는 사용자가 실제로 보는 것이 전부입니다. 이 글에서는 비상(또는 “위기”) 모드가 단순히 불리언을 전환하는 것이 아니라 경험을 실제로 바꾸는지를 검증하는 방법을 설명합니다.
단위 테스트로 검증 가능한 것
- ✅ 상태가 올바르게 업데이트됨
- ✅ CSS 클래스가 적용됨
- ✅ 컴포넌트가 렌더링됨
단위 테스트로 검증할 수 없는 것
- ❌ 버튼이 실제로 더 크게 보임
- ❌ 대비 비율이 실제로 증가함
- ❌ 레이아웃이 실제로 단순화됨
- ❌ 텍스트가 실제로 읽기 쉬움
“테스트 통과”와 “사용자 도움” 사이의 차이는 시각적입니다. 스크린샷 테스트가 그 차이를 메워 줍니다.
시각 회귀 기법
일반 모드와 위기 모드에서 같은 컴포넌트를 캡처한 뒤, 의미 있게 다른지 확인합니다.
import { test, expect } from '@playwright/test';
test.describe('Crisis Mode Visual Transformations', () => {
test('emergency mode visually differs from normal mode', async ({ page }) => {
// Capture normal state
await page.goto('/pain-entry');
const normalScreenshot = await page.screenshot();
// Activate crisis mode
await page.evaluate(() => {
window.dispatchEvent(new CustomEvent('activate-crisis-mode', {
detail: { severity: 'emergency' }
}));
});
// Wait for visual transition
await page.waitForTimeout(500); // Allow CSS transitions
// Capture crisis state
const crisisScreenshot = await page.screenshot();
// Verify they're different (this is the key assertion)
expect(Buffer.compare(normalScreenshot, crisisScreenshot)).not.toBe(0);
});
test('emergency mode matches approved baseline', async ({ page }) => {
await page.goto('/pain-entry?crisis=emergency');
await expect(page).toHaveScreenshot('emergency-mode-pain-entry.png', {
maxDiffPixels: 100, // Allow minor anti-aliasing differences
});
});
});
컴포넌트‑레벨 피드백
test.describe('Touch Target Scaling', () => {
test('buttons scale up in emergency mode', async ({ page }) => {
await page.goto('/component-preview/button');
// Normal size
await expect(page.locator('[data-testid="primary-button"]'))
.toHaveScreenshot('button-normal.png');
// Emergency size
await page.evaluate(() => {
document.documentElement.setAttribute('data-crisis-mode', 'emergency');
});
await expect(page.locator('[data-testid="primary-button"]'))
.toHaveScreenshot('button-emergency.png');
});
});
여러 적응형 상태 다루기
표준 시각 회귀는 테스트당 하나의 베이스라인만 사용하지만, 적응형 인터페이스는 다양한 유효 상태를 가집니다. 예를 들어 통증‑추적 버튼의 유효 조합은 다음과 같습니다:
| 모드 | 대비 | 글꼴 크기 | 터치 대상 | 베이스라인 |
|---|---|---|---|---|
| 일반 | 일반 | 중간 | 표준 | A |
| 일반 | 높음 | 중간 | 표준 | B |
| 위기 | 일반 | 크게 | 초대형 | C |
| 위기 | 높음 | 크게 | 초대형 | D |
| … | … | … | … | … |
매트릭스 테스트 예시
const PREFERENCE_MATRIX = [
{ mode: 'normal', contrast: 'normal', fontSize: 'medium' },
{ mode: 'normal', contrast: 'high', fontSize: 'medium' },
{ mode: 'normal', contrast: 'normal', fontSize: 'large' },
{ mode: 'crisis', contrast: 'normal', fontSize: 'large' },
{ mode: 'crisis', contrast: 'high', fontSize: 'large' },
];
for (const prefs of PREFERENCE_MATRIX) {
test(`pain entry form - ${prefs.mode}/${prefs.contrast}/${prefs.fontSize}`, async ({ page }) => {
// Set preferences via URL params or localStorage
await page.goto(
`/pain-entry?mode=${prefs.mode}&contrast=${prefs.contrast}&fontSize=${prefs.fontSize}`
);
// Each combination has its own baseline
const baselineName = `pain-entry-${prefs.mode}-${prefs.contrast}-${prefs.fontSize}.png`;
await expect(page).toHaveScreenshot(baselineName);
});
}
Storybook & Chromatic
Storybook을 사용한다면, Chromatic이 매트릭스 테스트를 깔끔하게 처리합니다.
// Button.stories.tsx
export default {
title: 'Components/Button',
component: Button,
};
export const Normal = () => Save Entry;
export const Emergency = () => (
Save Entry
);
export const HighContrast = () => (
Save Entry
);
export const EmergencyHighContrast = () => (
Save Entry
);
Chromatic은 각 스토리를 별도의 베이스라인으로 캡처해, 시각적 변화가 의도된 것인지 확인합니다.
접근성 구조 검사
시각적 변화는 의미론적 구조를 유지해야 합니다. 다음 테스트들은 DOM 구조가 올바르게 적응했는지 검증합니다.
test.describe('Crisis Mode Accessibility Structure', () => {
test('normal mode has full navigation structure', async ({ page }) => {
await page.goto('/pain-entry');
await expect(page.locator('main')).toMatchAriaSnapshot(`
- main:
- heading "Log Pain Entry" [level=1]
- navigation "Entry sections":
- link "Basic Info"
- link "Location"
- link "Symptoms"
- link "Triggers"
- link "Medications"
- form "Pain entry form":
- group "Pain Level":
- slider "Pain intensity"
- group "Location":
- button "Select body areas"
- group "Notes":
- textbox "Additional notes"
- button "Save Entry"
- button "Cancel"
`);
});
test('emergency mode simplifies to essentials', async ({ page }) => {
await page.goto('/pain-entry?crisis=emergency');
// Emergency mode should have FEWER elements, not more
await expect(page.locator('main')).toMatchAriaSnapshot(`
- main:
- heading "Quick Pain Log" [level=1]
- form "Simplified pain entry":
- group "How bad?":
- slider "Pain level"
- group "Where?":
- button "Tap body location"
- button "Save Now"
- button "Need Help?"
`);
});
});
test('emergency mode reduces cognitive load', async ({ page }) => {
// Count interactive elements in normal mode
await page.goto('/pain-entry');
const normalInteractive = await page
.locator('[role="button"], [role="link"], input, select, textarea')
.count();
// Count in emergency mode
await page.goto('/pain-entry?crisis=emergency');
const emergencyInteractive = await page
.locator('[role="button"], [role="link"], input, select, textarea')
.count();
// Emergency mode should have fewer interactive elements
expect(emergencyInteractive).toBeLessThan(normalInteractive);
// But not zero – core functionality must remain
expect(emergencyInteractive).toBeGreaterThan(2);
});
대비 검증
고대비 모드는 대비 비율이 WCAG 기준을 충족하지 않으면 의미가 없습니다. 자동화 테스트로 이를 검증할 수 있습니다.
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Contrast Verification', () => {
test('normal mode meets WCAG AA (4.5:1)', async ({ page }) => {
await page.goto('/pain-entry');
const results = await new AxeBuilder({ page })
.withRules(['color-contrast'])
.analyze();
expect(results.violations).toHaveLength(0);
});
test('high contrast mode exceeds WCAG AAA (7:1)', async ({ page }) => {
await page.goto('/pain-entry?contrast=high');
const results = await new AxeBuilder({ page })
.withRules(['color-contrast-enhanced']) // AAA level
.analyze();
expect(results.violations).toHaveLength(0);
});
});
결론
시각 회귀 테스트는 적응형 인터페이스, 특히 “위기 모드”가 더 큰 터치 대상, 높은 대비, 단순화된 레이아웃을 신뢰성 있게 제공해야 할 때 필수적입니다. 스크린샷 비교, 매트릭스 테스트, Storybook/Chromatic 베이스라인, ARIA 구조 스냅샷, 대비 분석을 결합하면 UI가 단순히 상태 플래그를 전환하는 수준을 넘어, 위급 상황에 처한 사용자를 실제로 더 안전하게 도울 수 있음을 보장할 수 있습니다.