Adaptive Interfaces를 위한 Visual Regression: Crisis Mode가 실제로 다르게 보이는지 테스트

발행: (2025년 12월 13일 오후 11:00 GMT+9)
7 min read
원문: Dev.to

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가 단순히 상태 플래그를 전환하는 수준을 넘어, 위급 상황에 처한 사용자를 실제로 더 안전하게 도울 수 있음을 보장할 수 있습니다.

Back to Blog

관련 글

더 보기 »