デザインシステムのビジュアルテスト:コンポーネントごとのアプローチ
デザインシステムは一貫性を約束しますが、ビジュアルテストなしでは200以上のコンポーネントがどこでも正しくレンダリングされることを祈るだけです。体系的なアプローチを紹介します。
デザインシステムテストのギャップ
デザインシステムには200以上のコンポーネントストーリーを持つStorybookがあります。すべてのコンポーネントにはpropsと動作のユニットテストがあります。しかし、variant="destructive"とsize="sm"を持つButtonコンポーネントが正しい赤い背景と32pxの高さでレンダリングされることを実際に検証している人はいますか?
これがビジュアルテストが埋めるギャップです。すべてのコンポーネントの、すべてのバリアントの、すべてのブレークポイントでのビジュアル出力を検証します。
テストマトリクス
典型的なデザインシステムのボタンコンポーネントのテストマトリクスは以下のようになります:
| 次元 | 値 |
|---|---|
| バリアント | default, secondary, destructive, outline, ghost, link |
| サイズ | sm, md, lg |
| 状態 | default, hover, focus, disabled, loading |
| コンテンツ | テキスト, アイコン, テキスト+アイコン |
| テーマ | ライト, ダーク |
つまり 6 × 3 × 5 × 3 × 2 = 540のビジュアル状態が単一のコンポーネントに存在します。システム内の数十のコンポーネントを掛けると、手動ビジュアルQAが不可能な理由がすぐに理解できます。
Storybookとの自動化
Storybookはデザインシステムのビジュアルテストの自然な統合ポイントです。各ストーリーがビジュアル状態を表します:
// Button.stories.tsx
export const AllVariants: Story = {
render: () => (
<div className="flex flex-col gap-4">
{(['default', 'secondary', 'destructive', 'outline'] as const).map(
(variant) => (
<div key={variant} className="flex items-center gap-2">
<Button variant={variant} size="sm">Small</Button>
<Button variant={variant} size="default">Default</Button>
<Button variant={variant} size="lg">Large</Button>
</div>
)
)}
</div>
),
}
次に、ビジュアルテストツールを使って、すべてのPRですべてのストーリーのスクリーンショットをキャプチャして比較します。
テーマバリアントの処理
デザインシステムは通常、ライトテーマとダークテーマをサポートします。ビジュアルテストは両方をカバーする必要があります:
const themes = ['light', 'dark'] as const
for (const theme of themes) {
test(`button renders in ${theme} mode`, async ({ page }) => {
await page.goto(`/iframe.html?id=button--all-variants`)
await page.evaluate((t) => {
document.documentElement.classList.toggle('dark', t === 'dark')
}, theme)
await expect(page).toHaveScreenshot(`button-${theme}.png`)
})
}
トークンの検証
ビジュアル比較の先に、コンポーネントが正しいデザイントークンを使用しているかを検証できます:
test('button uses correct design tokens', async ({ page }) => {
await page.goto('/iframe.html?id=button--default')
const button = page.locator('button')
const styles = await button.evaluate((el) => {
const computed = getComputedStyle(el)
return {
backgroundColor: computed.backgroundColor,
borderRadius: computed.borderRadius,
fontWeight: computed.fontWeight,
padding: computed.padding,
}
})
expect(styles.borderRadius).toBe('8px')
expect(styles.fontWeight).toBe('500')
})
これにより、ビジュアルレベルだけでなくトークンレベルでCSSドリフトをキャッチできます。
レスポンシブコンポーネントテスト
コンポーネントはすべてのビューポートサイズで動作する必要があります。モバイルで水平から垂直レイアウトに切り替わるカードコンポーネントの場合:
test('card switches layout at mobile', async ({ page }) => {
// デスクトップ - 水平レイアウト
await page.setViewportSize({ width: 1024, height: 768 })
await page.goto('/iframe.html?id=card--responsive')
await expect(page.locator('.card')).toHaveScreenshot('card-desktop.png')
// モバイル - 垂直レイアウト
await page.setViewportSize({ width: 375, height: 812 })
await expect(page.locator('.card')).toHaveScreenshot('card-mobile.png')
})
承認ワークフロー
ビジュアルテストが変更を検出したとき、その変更が意図的かどうかを誰かが判断する必要があります。デザインシステムでは、このワークフローに以下を含めるべきです:
- エンジニアリングレビュー — コードの正確性
- デザインレビュー — 見た目の正確性
- 自動チェック — アクセシビリティ(コントラスト比、テキストサイズ)
ChromaticのようなツールはこのワークフローをPRプロセスに直接統合し、ワンクリック承認でサイドバイサイドの比較を表示します。
カバレッジの測定
どのコンポーネントにビジュアルテストがあり、どのコンポーネントにないかを追跡します:
# ストーリーとビジュアルテストカバレッジを比較
total_stories=$(find src -name "*.stories.tsx" | wc -l)
tested_stories=$(grep -rl "toHaveScreenshot" tests/ | wc -l)
echo "Visual test coverage: $tested_stories/$total_stories"
コアコンポーネント(ボタン、インプット、カード、ナビゲーション)は100%のカバレッジを、複合コンポーネントは少なくとも80%のカバレッジを目指しましょう。
ScanUで始める
これらのテクニックを本番環境で適用するには、注力するページセットから始めて、意味のあるUI変更のたびにベースラインスクリーンショット比較を実行しましょう。プランは料金、実装の詳細はFAQ、プロダクトの機能は機能で確認できます。