Communitygithub.com

joo6077/design-test

UI コードとデザイントークンを分析し、デザイン品質テストを自動生成します。

design-test とは?

このスキルは、React、Vue、Svelte、Angular、HTML/CSS などの UI プロジェクトにおいて、デザイン品質を自動的にテストするための包括的なフレームワークを提供します。プロジェクトの UI スタックとデザインシステムを検出し、アクセシビリティ(WCAG 2.2 AA 基準)、視覚的回帰、デザイントークンの一貫性、レスポンシブレイアウトに関するテストを自動生成します。Playwright と axe-core を主なテストツールとして使用し、Storybook の有無に応じてテスト戦略を適応させます。このスキルは、Gotcha リストで定義された多数の注意点(例:空のスナップショットを PASS と誤認しない、最初のベースライン生成をテスト合格と見なさない、決定伝播マニフェストによるカバレッジ検証など)に従い、堅牢で信頼性の高いテストを生成します。特に、視覚的回帰テストでは、コンテンツ存在のアサーションを必須とし、スコープ指定されたスナップショットによる部分変更検証、ダークモードテスト、APCA コントラストアルゴリズムの情報的利用など、高度なテストパターンをサポートします。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/joo6077/claude-plugins/tree/HEAD/design-kit/skills/design-test

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Gotchas

  1. スタック検出なしでのテスト生成禁止 — React/Vue/Svelte/Flutter/HTML など、プロジェクトのフレームワークを最初に検出せよ。Flutter プロジェクトに Playwright テストを生成してはならない。
  2. WCAG 2.2 AA 基準未適用の禁止 — アクセシビリティテストは必ず WCAG 2.2 AA を基準とする。コントラスト比 4.5:1(本文)、3:1(大きなテキスト/非テキスト UI)、タッチターゲット 24×24px(AA) / 44×44px(AAA)。出典: design-kit design-audit Gotcha #3
  3. axe-core ルールセットのバージョン確認 — @axe-core/playwright または axe-core を使用する場合、プロジェクトにインストールされたバージョンのルールセットを確認せよ。メジャーバージョン間でルールに差異がある。
  4. 視覚的回帰テストの threshold 未設定禁止 — ピクセル単位の比較は、アンチエイリアシングやフォントレンダリングの違いにより false positive が頻発する。maxDiffPixelRatio: 0.01 のような threshold が必須。
  5. トークン検証におけるハードコード検出範囲の明確化 — CSS で #ff0000、16px、400 のような raw value があっても、即 FAIL ではない。reset/normalize CSS、外部ライブラリのコードは除外せよ。
  6. レスポンシブテストにおけるブレークポイントの出典確認 — プロジェクトのデザインシステムで定義されたブレークポイントを使用せよ。任意に 320/768/1024 を設定してはならない。
  7. ダークモードテストの欠落禁止 — デザイントークンにダークモードのマッピングがある場合、必ず両方のモードでテストする。prefers-color-scheme: dark メディアクエリまたはトークンクラスの切り替えを使用せよ。
  8. Storybook 依存の確認 — 視覚的回帰テストで Storybook を前提としてならない。Storybook がない場合は、Playwright のページスクリーンショットで代替する。
  9. APCA コントラストアルゴリズムは情報提供のみ — WCAG 2.2 AA (4.5:1 WCAG2 公式) が判定基準。APCA Lc は追加情報提供用としてのみ含め、PASS/FAIL の判定基準として使用してはならない。出典: design-guide Gotcha #10
  10. 初回実行ベースライン生成を「テスト合格」と見なすことの禁止(negative control 必須) — Playwright はスナップショットがない場合、実際の画面をベースラインとして 自動記録し、その実行を合格として処理する。したがって、--update-snapshots 直後の green は何も証明しない — 何とも比較されていないからである(証拠有効性検証 2 の有効化失敗)。ベースラインを作成した後は、必ず negative control を 1 回実行せよ: 対象要素に意図的な変更(例:背景色を 1 段階変更)を加えた状態でテストを実行し 失敗することを確認し、元に戻して合格することを確認する。この 2 回の実行の出力を証拠として引用するまでは、視覚的回帰テストが動作していると報告してはならない。出典: Playwright Visual Comparisons (初回実行時「A snapshot doesn't exist ... writing actual」)、harness/docs/guides/qa-evaluation-guide.md §Evidence Validity Gate 検査 2·3。
  11. 空のスナップショット/空ページは PASS の証拠ではない — レンダリングに失敗して空の画面がキャプチャされても、視覚的回帰テストはベースラインと同一であれば合格する。視覚テストには コンテンツ存在のアサーションを一緒に生成 せよ(核となる要素の visible、リスト項目数 ≥ 1、コンテナの高さ > 0)。スナップショット比較のみのテストファイルは、unbounded-height collapse のような欠陥を通過させる — 実際の事故例である。詳細: ../../references/visual-change-protocol.md §3。
  12. 部分変更の検証はスコープ指定スナップショットで — 意図外領域の変化を検出 — 「ボーダーだけ変えた」を検証する際に、ページ全体のスナップショット 1 つだけを使うと、背景まで変わったことを区別できない。変更対象要素の locator 単位のスナップショット と 周辺領域のスナップショット を分離して生成し、対象は変化し、周辺は変化していないことをそれぞれ判定せよ。周辺領域のスナップショットが失敗した場合、それは回帰である。詳細: ../../references/visual-change-protocol.md §2。
  13. prefers-reduced-motion: reduce は「アニメーション除去」ではない — この設定は、前庭誘発性トリガー(scale·pan など移動感を与えるモーション)を より穏やかな代替手段に置き換える ことを意味し、すべてのトランジションをなくすことを意味しない。したがって、reduced-motion テストを「アニメーション数 0」として assert してはならない。transform: scale/translate ベースのモーションが消えるか、opacity/dissolve に置き換えられたかを検証せよ。出典: MDN prefers-reduced-motion (Baseline 2020-01)。
  14. ゴールデンのみの決定伝播テストは FAIL — Decision Propagation Manifest Coverage — 確定したデザイン決定がどの画面に反映されるべきかは、.design/decisions.yaml が列挙する (decision_id → required_surfaces[] → golden + assertions)。この manifest が存在する場合は、カバレッジ規則 4 条に従ってテストを生成せよ。核心は golden のみで、核となる要素の visible / count / height アサーションがない場合は FAIL である — 空の画面もベースラインと同じであれば合格するため、スナップショット単独では「ユーザーが見る」ことを証明できない(Gotcha 11 と同じ根源)。excluded_surfaces に理由なく欠落した画面は、レビューされたのではなく カバレッジの空白 である。スキーマ・ルール・実行可能なチェッカー: ../../references/visual-change-protocol.md §6 Decision Propagation Manifest。
  15. 証拠にチャネル名を付けよ — スナップショットの存在はユーザー観測ではない — artifact_snapshot / dom_snapshot / browser_user_visible / device_user_visible は強度が異なる。PASS 文には viewport · route/state · visible locator · count/height · screenshot/golden id の 5 要素が含まれなければならず、一つでも欠ければ [未検証] である。チャネル定義: ../../references/visual-change-protocol.md §7 Evidence Channels。ユーザーが失敗を報告した場合の優先順位規約は harness/docs/guides/skill-design-guide.md §3.8 が原本である — テスト合格を根拠に反論してはならない。

プロセス

Step 0: プロジェクト検出

プロジェクトルートから UI スタックとデザインシステムを検出する:

検出対象スタックテストツール
package.json + ReactReactPlaywright + axe-core + Storybook (任意)
package.json + VueVuePlaywright + axe-core
package.json + SvelteSveltePlaywright + axe-core
package.json + AngularAngularPlaywright + axe-core
pubspec.yaml + FlutterFlutter→ flutter-test スキルにリダイレクト
*.html (静的サイト)HTML/CSSPlaywright + axe-core + pa11y

追加検出:

  • デザイントークン: **/tokens/**、**/theme/**、**/design-system/**、CSS カスタムプロパティ (--)
  • Storybook: .storybook/、*.stories.*
  • Tailwind: tailwind.config.*
  • CSS-in-JS: styled-components、emotion、vanilla-extract

Step 1: 対象分析

$ARGUMENTS から対象とテストタイプをパースする。

タイプ未指定時の自動推論:

対象特性テストタイプ
トークンファイル、テーマ設定token (一貫性検証)
コンポーネント、ページa11y (アクセシビリティ) + visual (視覚的回帰)
レイアウト、グリッドresponsive (レスポンシブ)
プロジェクト全体全タイプ

Step 2: 既存パターン探索

  • 既存のアクセシビリティテスト: *.a11y.test.*、axe-core import
  • 既存の視覚テスト: *.visual.test.*、__snapshots__/、.loki/
  • Playwright 設定: playwright.config.*
  • Storybook テスト: *.stories.* の有無
  • デザイントークン検証: 既存の lint ルール、stylelint 設定

Step 3: トークン一貫性テスト生成

デザイントークンファイルを分析し、一貫性を検証するテストを生成する。

// tests/design/tokens.test.ts
import { describe, it, expect } from 'vitest'
import fs from 'fs'

describe('Design Token Consistency', () => {
  // CSS カスタムプロパティのパース
  const cssContent = fs.readFileSync('src/styles/tokens.css', 'utf-8')
  const tokens = [...cssContent.matchAll(/--([^:]+):\s*([^;]+)/g)]

  it('all color tokens use consistent format (hex/oklch/hsl)', () => {
    const colorTokens = tokens.filter(([, name]) => name.includes('color'))
    const formats = new Set(
      colorTokens.map(([, , value]) => {
        if (value.trim().startsWith('#')) return 'hex'
        if (value.trim().startsWith('oklch')) return 'oklch'
        if (value.trim().startsWith('hsl')) return 'hsl'
        return 'other'
      })
    )
    expect(formats.size).toBeLessThanOrEqual(1)
  })

  it('spacing tokens follow scale (base * multiplier)', () => {
    const spacingTokens = tokens
      .filter(([, name]) => name.includes('spacing'))
      .map(([, , value]) => parseFloat(value))
      .sort((a, b) => a - b)

    for (let i = 1; i < spacingTokens.length; i++) {
      const ratio = spacingTokens[i] / spacingTokens[i - 1]
      expect(ratio).toBeGreaterThan(1)
    }
  })

  it('no hardcoded values in component files', () => {
    const componentCss = fs.readFileSync('src/components/Button.css', 'utf-8')
    const rawColors = [...componentCss.matchAll(/#[0-9a-fA-F]{3,8}/g)]
    const rawPx = [...componentCss.matchAll(/:\s*\d+px/g)]
    // reset、normalize、外部ライブラリファイルは検査対象から除外
    expect(rawColors).toEqual([])
    expect(rawPx).toEqual([])
  })

  it('dark mode tokens cover all light mode tokens', () => {
    const lightNames = tokens
      .filter(([, name]) => !name.includes('dark'))
      .map(([, name]) => name.replace('light-', ''))
    const darkNames = tokens
      .filter(([, name]) => name.includes('dark'))
      .map(([, name]) => name.replace('dark-', ''))
    const missing = lightNames.filter(n => !darkNames.includes(n))
    expect(missing).toEqual([])
  })
})

Step 4: アクセシビリティテスト生成

// tests/design/a11y.test.ts
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'

const pages = [
  { name: 'Home', path: '/' },
  { name: 'Settings', path: '/settings' },
  // Step 1 で検出されたルート一覧
]

for (const page of pages) {
  test.describe(`${page.name} Accessibility`, () => {
    test('should pass axe-core WCAG 2.2 AA', async ({ page: p }) => {
      await p.goto(page.path)
      const results = await new AxeBuilder({ page: p })
        .withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
        .analyze()
      expect(results.violations).toEqual([])
    })

    test('all interactive elements have accessible names', async ({ page: p }) => {
      await p.goto(page.path)
      const buttons = await p.locator('button, [role="button"]').all()
      for (const btn of buttons) {
        const name = await btn.getAttribute('aria-label')
          ?? await btn.textContent()
        expect(name?.trim()).toBeTruthy()
      }
    })

    test('color contrast meets 4.5:1 ratio', async ({ page: p }) => {
      await p.goto(page.path)
      const results = await new AxeBuilder({ page: p })
        .withRules(['color-contrast'])
        .analyze()
      expect(results.violations).toEqual([])
    })

    test('touch targets are at least 24x24px', async ({ page: p }) => {
      await p.setViewportSize({ width: 375, height: 812 })
      await p.goto(page.path)
      const targets = await p.locator(
        'button, a, input, select, [role="button"], [role="link"]'
      ).all()
      for (const target of targets) {
        const box = await target.boundingBox()
        if (box) {
          expect(box.width).toBeGreaterThanOrEqual(24)
          expect(box.height).toBeGreaterThanOrEqual(24)
        }
      }
    })
  })
}

// ダークモードのアクセシビリティ(トークンにダークモードがある場合)
test('dark mode passes color contrast', async ({ page }) => {
  await page.emulateMedia({ colorScheme: 'dark' })
  await page.goto('/')
  const results = await new AxeBuilder({ page })
    .withRules(['color-contrast'])
    .analyze()
  expect(results.violations).toEqual([])
})

Step 5: 視覚的回帰テスト生成

// tests/design/visual-regression.test.ts
import { test, expect } from '@playwright/test'

const viewports = [
  { name: 'mobile', width: 375, height: 812 },
  { name: 'tablet', width: 768, height: 1024 },
  { name: 'desktop', width: 1440, height: 900 },
]

const pages = [
  { name: 'Home', path: '/' },
  // Step 1 で検出された主要ページ
]

for (const vp of viewports) {
  for (const pg of pages) {
    test(`${pg.name} - ${vp.name} visual snapshot`, async ({ page }) => {
      await page.setViewportSize({ width: vp.width, height: vp.height })
      await page.goto(pg.path)
      await page.waitForLoadState('networkidle')

      // Gotcha 11 — 空のレンダリングがベースラインと一致して合格するのを防ぐコンテンツ存在アサーション。
      // スナップショット比較の「前に」実行することで、空の画面がベースラインとして固定されるのも同時に防ぐ。
      const main = page.locator('main, [role="main"]').first()
      await expect(main).toBeVisible()
      const box = await main.boundingBox()
      expect(box?.height ?? 0).toBeGreaterThan(0)

      await expect(page).toHaveScreenshot(
        `${pg.name}-${vp.name}.png`,
        { maxDiffPixelRatio: 0.01 }
      )
    })
  }
}

// Storybook がある場合 — コンポーネント単位の視覚テスト
// Storybook 検出時のみこのセクションを生成

部分変更(スコープ指定)検証 — 特定の要素の特定の属性のみを変更した変更を検証する際は、対象と周辺を分離する (Gotcha 12)。周辺のスナップショットが失敗した場合は、意図外の領域が変化したことになるため、回帰として扱う。

// tests/design/scoped-change.test.ts
import { test, expect } from '@playwright/test'

test.describe('Scoped visual change — 対象のみ変化、周辺は不変', () => {
  test('対象要素スナップショット(変更許容)', async ({ page }) => {
    await page.goto('/')
    await expect(page.getByTestId('target-card'))
      .toHaveScreenshot('target-card.png', { maxDiffPixelRatio: 0.01 })
  })

  // 周辺領域は「変化してはならない」ため、対象より厳格な threshold を使用する。
  // ただし Gotcha 4 に従い 0 は使用しない — アンチエイリアシング・フォントレンダリングの違いで常時失敗する。
  test('周辺領域スナップショット(変更禁止 — 失敗時は回帰)', async ({ page }) => {
    await page.goto('/')
    await expect(page.getByTestId('surrounding-panel'))
      .toHaveScreenshot('surrounding-panel.png', { maxDiffPixelRatio: 0.001 })
  })
})

Step 5-b: 決定伝播テスト生成(.design/decisions.yaml がある場合のみ)

manifest がない場合はこのステップをスキップするが、「該当なし」ではなく「manifest 不在」として報告する (チェッカーが NO_MANIFEST + exit 3 を出力する理由と同じ — 対象 0 件と合格は異なる)。

manifest がある場合は、status: approved の decision_id ごとに required_surfaces[] を反復し、surface ごとにテスト 1 つを 生成する。status: superseded(代替済み)の決定はテストを作成しない — その画面の場所は superseded_by が指す approved 決定が担う。スキーマとカバレッジ規則 4 条は ../../references/visual-change-protocol.md §6 が原本であり ここで再定義しない。生成ルールは 3 つ:

  1. surface の route_or_entry · state · viewport_or_container をテストセットアップにそのまま移す。
  2. assertions[] を golden 比較より先に 実行する — 空のレンダリングがベースラインとして固定されるのを防ぐ。
  3. selectors[] は対象 locator スナップショットに、その外側は周辺領域スナップショットに使用する(Gotcha 12 と対になる)。
// tests/design/decision-DEC-20260813-001.test.ts — manifest から生成
import { test, expect } from '@playwright/test'

test('DEC-20260813-001 → dashboard.desktop.main', async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 })   // viewport_or_container
  await page.goto('/dashboard')                              // route_or_entry
  // assertions[] — golden 比較より先に(Gotcha 11·14)
  const main = page.locator('main').first()
  await expect(main).toBeVisible()
  await expect(page.locator("[data-surface='group-list'] > *")).not.toHaveCount(0)
  expect((await main.boundingBox())?.height ?? 0).toBeGreaterThan(0)
  // golden — 上記 assertion を通過した後にのみ意味を持つ
  await expect(main).toHaveScreenshot(
    'DEC-20260813-001/dashboard.desktop.main.png', { maxDiffPixelRatio: 0.01 }
  )
})

生成後、カバレッジチェッカー(§6)を実行して終了コード 0 を確認する。終了コード 2(SCHEMA_ERROR)は入力の形状が間違っていて判定できなかったものであり、FAIL 行がなく violations=0 と表示されても合格とは見なさない。 終了コード 3(NO_SURFACE · NO_DECISION)は NO_MANIFEST と同様に対象 0 件として報告する。golden のみで assertions が空の surface が残っている場合は、それは FAIL であり、テストファイルを作成したという事実がカバレッジを代替しない。

excluded_surfaces に挙げられた画面はテストを作成しないが、報告には理由とともに列挙する。

Step 6: レスポンシブレイアウトテスト生成

// tests/design/responsive.test.ts
import { test, expect } from '@playwright/test'

// プロジェクトのデザインシステムのブレークポイントを使用
const breakpoints = [
  { name: 'sm', width: 640 },
  { name: 'md', width: 768 },
  { name: 'lg', width: 1024 },
  { name: 'xl', width: 1280 },
]

test.describe('Responsive Layout', () => {
  for (const bp of breakpoints) {
    test(`no horizontal overflow at ${bp.name} (${bp.width}px)`, async ({ page }) => {
      await page.setViewportSize({ width: bp.width, height: 900 })
      await page.goto('/')
      const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth)
      const clientWidth = await page.evaluate(() => document.documentElement.clientWidth)
      expect(scrollWidth).toBeLessThanOrEqual(clientWidth)
    })
  }

  for (const bp of breakpoints) {
    test(`navigation layout is responsive at ${bp.name}`, async ({ page }) => {
      await page.setViewportSize({ width: bp.width, height: 900 })
      await page.goto('/')
      const nav = page.locator('nav')
      await expect(nav).toBeVisible()
      // モバイルではハンバーガーメニュー、デスクトップでは横並びなどを確認
      if (bp.width < 768) {
        // モバイル: メニューが隠れているか、ハンバーガーボタンがある
        const menuButton = page.locator('[aria-label="Open menu"], .hamburger')
        await expect(menuButton).toBeVisible()
      } else {
        // デスクトップ: メニュー項目が表示されている
        const menuItems = nav.locator('a, button')
        const count = await menuItems.count()
        expect(count).toBeGreaterThan(0)
      }
    })
  }
})

パフォーマンスとカバレッジの注意:

  • トークン一貫性テストには vitest、アクセシビリティと視覚的回帰テストには Playwright を使用する。
  • 全 AP をカバーするスタンドアロンのテストファイルを生成し、既存のテストスイートに統合する。
  • 大規模プロジェクトでは、ページごと、コンポーネントごとにテストファイルを分割する。
  • 既存のテストパターン(.a11y.test.ts など)が存在する場合は、それに従う。

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

関連スキル