Communitygithub.com

joo6077/design-test

Analysiert UI-Code und Design-Tokens und generiert automatisch Tests für die Designqualität.

Was ist design-test?

Dieses Skill für KI-Agenten analysiert UI-Code und Design-Tokens, um automatisch Tests für die Designqualität zu generieren. Es unterstützt gängige Frameworks wie React, Vue, Svelte, Angular und statisches HTML (Flutter wird an einen anderen Skill weitergeleitet) und erstellt Tests für Zugänglichkeit (WCAG 2.2 AA), visuelle Regression, Token-Konsistenz, responsives Layout und Entscheidungsweitergabe. Der Skill folgt einem strukturierten Prozess: Projekterkennung (UI-Stack, Designsystem, Storybook), automatische Ableitung der Testtypen, Analyse vorhandener Testmuster und Generierung von Playwright-basierten Tests mit axe-core. Er enthält strenge Prüfungen und Fallstricke (Gotchas), die typische Fehler wie fehlende Baseline-Erstellung, leere Snapshots, unbegründete Bestehensmeldungen und inkorrekte Dark-Mode-Tests verhindern. Ein besonderer Fokus liegt auf der Validierung von Design-Entscheidungen (Decision Propagation) und der korrekten Dokumentation von Beweisketten. Der Skill ist ideal für Entwicklerteams, die konsistente Designqualität über mehrere Oberflächen hinweg sicherstellen wollen, und lässt sich in CI/CD-Pipelines integrieren.

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

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Gotchas

  1. Keine Testgenerierung ohne Stack-Erkennung — Erkenne zuerst das Projekt-Framework (React/Vue/Svelte/Flutter/HTML). Erstelle keine Playwright-Tests für Flutter-Projekte.
  2. Keine Nichteinhaltung des WCAG 2.2 AA-Standards — Barrierefreiheitstests MÜSSEN auf WCAG 2.2 AA basieren. Kontrastverhältnis 4.5:1 (Fließtext), 3:1 (großer Text/nicht-textuelle UI), Touch-Ziele 24×24px (AA) / 44×44px (AAA). Quelle: design-kit design-audit Gotcha #3
  3. Prüfe die Version des axe-core-Regelsatzes — Bei Verwendung von @axe-core/playwright oder axe-core prüfe den Regelsatz der im Projekt installierten Version. Es gibt Regelunterschiede zwischen Hauptversionen.
  4. Kein visueller Regressionstest ohne Threshold — Pixelgenaue Vergleiche führen aufgrund von Anti-Aliasing und Font-Rendering-Unterschieden häufig zu False Positives. Ein Threshold wie maxDiffPixelRatio: 0.01 ist Pflicht.
  5. Definiere den Erkennungsbereich für Hardcoding bei Token-Validierung klar — Ein roher Wert wie #ff0000, 16px oder 400 in CSS führt nicht automatisch zu FAIL. Code von Reset/Normalize-CSS und externen Bibliotheken ist ausgeschlossen.
  6. Prüfe die Quelle der Breakpoints bei responsiven Tests — Verwende die im Designsystem des Projekts definierten Breakpoints. Füge nicht willkürlich 320/768/1024 ein.
  7. Kein Fehlen von Dark-Mode-Tests — Wenn das Design-Token eine Dark-Mode-Zuordnung enthält, teste in BEIDEN Modi. Verwende die prefers-color-scheme: dark Media Query oder Token-Klassenwechsel.
  8. Prüfe Abhängigkeit von Storybook — Gehe bei visuellen Regressionstests nicht von Storybook aus. Wenn kein Storybook vorhanden ist, ersetze es durch Playwright-Seitenscreenshots.
  9. APCA-Kontrastalgorithmus ist informativ — Der WCAG 2.2 AA (4.5:1 WCAG2-Formel) ist das Bewertungskriterium. APCA Lc wird nur als Zusatzinformation bereitgestellt und NICHT als PASS/FAIL-Kriterium verwendet. Quelle: design-guide Gotcha #10
  10. Keine erste Ausführung der Baseline als "Test bestanden" melden (Negative-Kontrolle ist Pflicht) — Wenn kein Snapshot vorhanden ist, zeichnet Playwright den tatsächlichen Bildschirm automatisch als Baseline auf und wertet diese Ausführung als bestanden. Daher beweist ein grünes Ergebnis DIREKT NACH --update-snapshots nichts — es wurde mit nichts verglichen (Aktivierung der Beweisevalidierung 2 fehlgeschlagen). Nachdem die Baseline erstellt wurde, MUSS eine Negative-Kontrolle durchgeführt werden: Führe den Test mit einer absichtlichen Änderung des Zielelements (z. B. Hintergrundfarbe um eine Stufe ändern) aus, um sicherzustellen, dass er FEHLSCHLÄGT, und nach dem Zurücksetzen, dass er bestanden wird. Melde einen visuellen Regressionstest erst als funktionierend, wenn die Ausgaben dieser beiden Ausführungen als Beweis zitiert werden. Quelle: Playwright Visual Comparisons (erste Ausführung: "A snapshot doesn't exist ... writing actual"), harness/docs/guides/qa-evaluation-guide.md §Evidence Validity Gate Test 2·3.
  11. Leere Snapshots/leere Seiten sind kein PASS-Beweis — Selbst wenn ein leerer Bildschirm aufgrund eines Rendering-Fehlers aufgenommen wird, besteht ein visueller Regressionstest, wenn er mit der Baseline identisch ist. Visuelle Tests MÜSSEN zusammen mit Inhalts-Existenz-Assertions erstellt werden (Sichtbarkeit der Kernelemente, Anzahl der Listeneinträge ≥ 1, Containerhöhe > 0). Testdateien mit nur Snapshot-Vergleichen lassen Fehler wie einen Zusammenbruch mit unbegrenzter Höhe durch — das ist ein realer Vorfall. Details: ../../references/visual-change-protocol.md §3.
  12. Validierung partieller Änderungen mit Scoped Snapshots — Erkennung von Änderungen außerhalb der Absicht — Wenn „nur der Rahmen geändert“ wurde, und ein einzelner Seiten-Snapshot verwendet wird, kann nicht unterschieden werden, ob sich auch der Hintergrund geändert hat. Erstelle separate Locator-basierte Snapshots des zu ändernden Elements und Snapshots der Umgebung. Bestimme, ob sich das Ziel geändert hat und die Umgebung nicht. Wenn der Umgebungs-Snapshot fehlschlägt, ist es eine Regression. Details: ../../references/visual-change-protocol.md §2.
  13. prefers-reduced-motion: reduce bedeutet NICHT "Animationen entfernen" — Diese Einstellung bedeutet, dass vestibuläre Auslöser (Bewegungen wie Skalieren, Schwenken, die Bewegungsgefühl vermitteln) durch sanftere Alternativen ersetzt werden sollen, nicht, dass alle Übergänge entfernt werden sollen. Daher ASSERTiere einen „Reduced-Motion“-Test nicht mit „Anzahl der Animationen 0“. Prüfe, ob transform: scale/translate-basierte Bewegungen verschwunden oder durch Opazität/Dissolve ersetzt wurden. Quelle: MDN prefers-reduced-motion (Baseline 2020-01).
  14. Ein Entscheidungsweitergabetest mit nur einem Goldenen ist FAIL — Decision Propagation Manifest Coverage — Welche Oberflächen eine bestätigte Designentscheidung widerspiegeln müssen, wird in .design/decisions.yaml aufgelistet (decision_id → required_surfaces[] → goldene + assertions). Wenn dieses Manifest vorhanden ist, generiere Tests gemäß Coverage-Regel 4. Der Kern ist: Ein Goldener allein, ohne sichtbare / count / height-Assertion für Kernelemente, ist FAIL — da ein leerer Bildschirm, der mit der Baseline identisch ist, bestanden wird, beweist ein Snapshot allein nicht, „dass der Benutzer es sieht“ (gleiche Wurzel wie Gotcha 11). Eine Oberfläche, die ohne Grund in excluded_surfaces fehlt, ist kein Review, sondern eine Coverage-Lücke. Schema, Regeln und ausführbarer Checker: ../../references/visual-change-protocol.md §6 Decision Propagation Manifest.
  15. Beweise mit Kanalnamen versehen — Die Existenz eines Snapshots ist keine Benutzerbeobachtung — artifact_snapshot / dom_snapshot / browser_user_visible / device_user_visible haben unterschiedliche Stärken. Ein PASS-Satz MUSS die 5 Elemente enthalten: Viewport · Route/State · Sichtbarer Locator · Count/Height · Screenshot/Golden-ID. Fehlt eines, ist es [nicht verifiziert]. Kanalkennung: ../../references/visual-change-protocol.md §7 Evidence Channels. Die Prioritätskonvention für Benutzer-Fehlerberichte ist in harness/docs/guides/skill-design-guide.md §3.8 festgelegt — Widerlege sie nicht mit Testbestehen als Grundlage.

Prozess

Schritt 0: Projekterkennung

Erkenne den UI-Stack und das Designsystem im Projektstammverzeichnis:

ErkennungszielStackTestwerkzeug
package.json + ReactReactPlaywright + axe-core + Storybook (optional)
package.json + VueVuePlaywright + axe-core
package.json + SvelteSveltePlaywright + axe-core
package.json + AngularAngularPlaywright + axe-core
pubspec.yaml + FlutterFlutter→ Weiterleitung zum flutter-test-Skill
*.html (statische Seite)HTML/CSSPlaywright + axe-core + pa11y

Zusätzliche Erkennung:

  • Design-Token: **/tokens/**, **/theme/**, **/design-system/**, CSS-Custom-Properties (--)
  • Storybook: .storybook/, *.stories.*
  • Tailwind: tailwind.config.*
  • CSS-in-JS: styled-components, emotion, vanilla-extract

Schritt 1: Zielanalyse

Analysiere das Ziel und den Testtyp aus $ARGUMENTS.

Automatische Ableitung bei nicht angegebenem Typ:

ZielmerkmalTesttyp
Token-Datei, Theme-Konfigurationtoken (Konsistenzprüfung)
Komponente, Seitea11y (Barrierefreiheit) + visual (visuelle Regression)
Layout, Gridresponsive (responsiv)
Gesamtes ProjektAlle Typen

Schritt 2: Vorhandene Muster erkunden

  • Vorhandene Barrierefreiheitstests: *.a11y.test.*, axe-core-Import
  • Vorhandene visuelle Tests: *.visual.test.*, __snapshots__/, .loki/
  • Playwright-Konfiguration: playwright.config.*
  • Storybook-Tests: Vorhandensein von *.stories.*
  • Design-Token-Validierung: Vorhandene Lint-Regeln, stylelint-Konfiguration

Schritt 3: Generierung von Token-Konsistenztests

Analysiere Design-Token-Dateien und generiere Tests zur Konsistenzprüfung.

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

describe('Design Token Consistency', () => {
  // CSS Custom Properties parsen
  const cssContent = fs.readFileSync('src/styles/tokens.css', 'utf-8')
  const tokens = [...cssContent.matchAll(/--([^:]+):\s*([^;]+)/g)]

  it('alle Farb-Token verwenden ein konsistentes 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('Abstands-Token folgen einer Skala (Basis * Multiplikator)', () => {
    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('keine hartcodierten Werte in Komponentendateien', () => {
    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- und externe Bibliotheksdateien sind von der Prüfung ausgeschlossen
    expect(rawColors).toEqual([])
    expect(rawPx).toEqual([])
  })

  it('Dark-Mode-Token decken alle Light-Mode-Token ab', () => {
    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([])
  })
})

Schritt 4: Generierung von Barrierefreiheitstests

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

const pages = [
  { name: 'Startseite', path: '/' },
  { name: 'Einstellungen', path: '/settings' },
  // Liste der in Schritt 1 erkannten Routen
]

for (const page of pages) {
  test.describe(`${page.name} Barrierefreiheit`, () => {
    test('sollte axe-core WCAG 2.2 AA bestehen', 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('alle interaktiven Elemente haben zugängliche Namen', 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('Farbkontrast erfüllt 4.5:1-Verhältnis', 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-Ziele sind mindestens 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)
        }
      }
    })
  })
}

// Dark Mode Barrierefreiheit (wenn Token einen Dark Mode haben)
test('Dark Mode besteht Farbkontrast', async ({ page }) => {
  await page.emulateMedia({ colorScheme: 'dark' })
  await page.goto('/')
  const results = await new AxeBuilder({ page })
    .withRules(['color-contrast'])
    .analyze()
  expect(results.violations).toEqual([])
})

Schritt 5: Generierung von visuellen Regressionstests

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

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

const pages = [
  { name: 'Startseite', path: '/' },
  // In Schritt 1 erkannte Hauptseiten
]

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

      // Gotcha 11 — Verhindert, dass ein leeres Rendering, das mit der Baseline identisch ist,
      // als bestanden gilt. Inhalts-Existenz-Assertion "vor" dem Snapshot-Vergleich ausführen,
      // um auch zu verhindern, dass ein leerer Bildschirm als Baseline gesetzt wird.
      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 }
      )
    })
  }
}

// Wenn Storybook vorhanden ist — komponentenweise visuelle Tests
// Diesen Abschnitt nur generieren, wenn Storybook erkannt wurde

Validierung partieller Änderungen (scoped) — Trenne Ziel und Umgebung, wenn nur eine bestimmte Eigenschaft eines Elements geändert wurde (Gotcha 12). Wenn der Umgebungs-Snapshot fehlschlägt, hat sich ein unbeabsichtigter Bereich geändert, also ist es eine Regression.

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

test.describe('Scoped visuelle Änderung — Ziel ändert sich, Umgebung bleibt', () => {
  test('Ziel-Element Snapshot (Änderung erlaubt)', async ({ page }) => {
    await page.goto('/')
    await expect(page.getByTestId('target-card'))
      .toHaveScreenshot('target-card.png', { maxDiffPixelRatio: 0.01 })
  })

  // Der umgebende Bereich "darf sich nicht ändern", daher einen strengeren Threshold als für das Ziel verwenden.
  // Aber gemäß Gotcha 4 nicht 0 verwenden — aufgrund von Anti-Aliasing und Font-Rendering-Unterschieden immer fehlschlagen.
  test('Umgebungs-Snapshot (keine Änderung erlaubt — Fehler = Regression)', async ({ page }) => {
    await page.goto('/')
    await expect(page.getByTestId('surrounding-panel'))
      .toHaveScreenshot('surrounding-panel.png', { maxDiffPixelRatio: 0.001 })
  })
})

Schritt 5-b: Generierung von Entscheidungsweitergabetests (nur wenn .design/decisions.yaml vorhanden ist)

Wenn kein Manifest vorhanden ist, überspringe diesen Schritt, melde es aber als "Manifest fehlt" und nicht als "nicht zutreffend" (aus dem gleichen Grund, aus dem der Checker NO_MANIFEST + exit 3 ausgibt — 0 Ziele ist nicht gleich Bestehen).

Wenn ein Manifest vorhanden ist, iteriere für jede decision_id mit status: approved über required_surfaces[] und generiere einen Test pro Surface. Entscheidungen mit status: superseded (ersetzt) erzeugen keine Tests — deren Platz auf der Oberfläche wird von der approved-Entscheidung übernommen, auf die superseded_by verweist. Das Schema und die Coverage-Regel 4 sind in ../../references/visual-change-protocol.md §6 verbindlich festgelegt und werden hier nicht neu definiert. Es gibt drei Generierungsregeln:

  1. Kopiere den route_or_entry · state · viewport_or_container des Surface direkt in das Test-Setup.
  2. Führe assertions[] vor dem Golden-Vergleich aus — um zu verhindern, dass ein leeres Rendering als Baseline gesetzt wird.
  3. Verwende selectors[] für den Ziel-Locator-Snapshot und den Rest für den Umgebungs-Snapshot (gepaart mit Gotcha 12).
// tests/design/decision-DEC-20260813-001.test.ts — aus Manifest generiert
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[] — vor Golden-Vergleich (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 — nur sinnvoll, wenn die obigen Assertions bestanden wurden
  await expect(main).toHaveScreenshot(
    'DEC-20260813-001/dashboard.desktop.main.png', { maxDiffPixelRatio: 0.01 }
  )
})

Führe nach der Generierung den Coverage-Checker (§6) aus und prüfe, dass der Exit-Code 0 ist. Exit-Code 2 (SCHEMA_ERROR) bedeutet, dass die Eingabeform falsch war und keine Entscheidung getroffen werden konnte; selbst wenn violations=0 ausgegeben wird, gilt es nicht als bestanden. Exit-Code 3 (NO_SURFACE · NO_DECISION) meldet wie NO_MANIFEST 0 Ziele. Wenn ein Surface nur einen golden Eintrag, aber leere assertions hat, ist es FAIL, und die Tatsache, dass eine Testdatei erstellt wurde, ersetzt nicht die Coverage.

Oberflächen, die in excluded_surfaces aufgeführt sind, erhalten keinen Test, müssen aber im Bericht mit Begründung aufgelistet werden.

Schritt 6: Generierung von responsiven Layouttests

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

// Breakpoints aus dem Designsystem des Projekts verwenden
const breakpoints = [
  { name: 'sm', width: 640 },
  { name: 'md', width: 768 },
  { name: 'lg', width: 1024 },
  { name: 'xl', width: 1280 },
]

test.describe('Responsives Layout', () => {
  for (const bp of breakpoints) {
    test(`kein horizontaler Überlauf bei ${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)
      e
    })
  }
})

(Hinweis: Der Codeblock wurde wie im Original unterbrochen)

Individual skills in this repo

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

Verwandte Skills