Communitygithub.com

jooe6077/design-test

Analyse le code UI et les tokens de design pour générer automatiquement des tests de qualité de conception.

Qu'est-ce que design-test ?

Cette compétence (ou « skill ») permet de générer automatiquement des suites de tests de qualité de conception pour des projets d'interface utilisateur. Elle détecte d'abord la stack technique du projet (React, Vue, Svelte, Angular, HTML/CSS statique, etc.) et son système de design (tokens, Storybook, Tailwind, CSS-in-JS). Ensuite, en fonction de la cible spécifiée (un composant, une page, des tokens ou le projet entier), elle crée des tests adaptés : tests de cohérence des tokens de design, tests d'accessibilité conformes aux critères WCAG 2.2 AA (via axe-core), tests de régression visuelle avec Playwright, tests responsives basés sur les breakpoints du projet, et tests de propagation des décisions de conception. La compétence intègre de nombreuses règles de robustesse (les « Gotchas ») pour éviter les faux positifs, les erreurs de configuration et garantir que les tests prouvent réellement la qualité visuelle et fonctionnelle de l'interface. Elle est conçue pour les projets React, Vue, Svelte, Angular et HTML statique, et redirige vers une autre compétence pour les projets Flutter.

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Pièges à éviter (Gotchas)

  1. Interdiction de générer des tests sans détection de la stack — Détectez d'abord le framework du projet (React/Vue/Svelte/Flutter/HTML, etc.). Ne créez pas de tests Playwright pour un projet Flutter.
  2. Application obligatoire des critères WCAG 2.2 AA — Les tests d'accessibilité doivent strictement respecter WCAG 2.2 AA. Ratio de contraste 4.5:1 (texte normal), 3:1 (grand texte / UI non textuelle), cibles tactiles 24×24px (AA) / 44×44px (AAA). Source : design-kit design-audit Gotcha #3.
  3. Vérification de la version de la règle axe-core — Lors de l'utilisation de @axe-core/playwright ou axe-core, vérifiez l'ensemble des règles de la version installée dans le projet. Il existe des différences de règles entre les versions majeures.
  4. Définition obligatoire d'un seuil pour les tests de régression visuelle — La comparaison pixel par pixel génère fréquemment des faux positifs à cause de l'anti-aliasing et des différences de rendu des polices. Un seuil comme maxDiffPixelRatio: 0.01 est obligatoire.
  5. Clarification de la portée de la détection de valeurs codées en dur dans les tokens — La présence de valeurs brutes comme #ff0000, 16px, 400 dans le CSS n'entraîne pas systématiquement un ÉCHEC. Excluez les CSS de reset/normalize et le code des bibliothèques externes.
  6. Vérification de la source des points de rupture pour les tests responsifs — Utilisez les points de rupture définis dans le système de design du projet. N'utilisez pas arbitrairement 320/768/1024.
  7. Ne pas omettre les tests en mode sombre — Si les tokens de design incluent une cartographie pour le mode sombre, testez obligatoirement les deux modes. Utilisez la requête média prefers-color-scheme: dark ou la commutation de classe de tokens.
  8. Vérification de la dépendance à Storybook — Ne présumez pas que Storybook est présent pour les tests de régression visuelle. Si Storybook est absent, remplacez-le par des captures d'écran de page avec Playwright.
  9. L'algorithme de contraste APCA est informatif — Le critère de décision est WCAG 2.2 AA (formule 4.5:1 WCAG2). L'APCA Lc est inclus uniquement à titre informatif supplémentaire et ne doit pas être utilisé comme critère PASS/FAIL. Source : design-guide Gotcha #10.
  10. Interdiction de considérer la création de la ligne de base initiale comme un « test réussi » (contrôle négatif obligatoire) — Playwright, si aucun instantané n'existe, enregistre automatiquement l'écran réel comme ligne de base et considère cette exécution comme réussie. Par conséquent, un résultat vert immédiatement après --update-snapshots ne prouve rien — car il n'y a eu aucune comparaison (échec de l'activation de la validation de la preuve 2). Après avoir créé la ligne de base, vous devez effectuer un contrôle négatif : exécutez le test avec une modification intentionnelle de l'élément cible (par exemple, changement de couleur de fond d'un cran) pour vérifier qu'il échoue, puis rétablissez la modification et vérifiez la réussite. Ne signalez pas que le test de régression visuelle fonctionne avant d'avoir cité les sorties de ces deux exécutions comme preuves. Source : Playwright Visual Comparisons (lors de la première exécution : "A snapshot doesn't exist ... writing actual"), harness/docs/guides/qa-evaluation-guide.md §Checks de validation de preuve 2 et 3.
  11. Un instantané vide / une page vide ne sont pas une preuve de RÉUSSITE — Même si le rendu échoue et qu'un écran vide est capturé, le test de régression visuelle réussit s'il est identique à la ligne de base. Générez également une assertion de présence de contenu dans les tests visuels (élément clé visible, nombre d'éléments de liste ≥ 1, hauteur du conteneur > 0). Un fichier de test contenant uniquement une comparaison d'instantanés laisse passer des défauts comme un effondrement de hauteur non bornée — c'est un incident réel. Détails : ../../references/visual-change-protocol.md §3.
  12. La vérification d'un changement partiel nécessite des instantanés ciblés (scoped) — détection des changements dans les zones non intentionnelles — Pour vérifier "seule la bordure a changé", l'utilisation d'un seul instantané de page entière ne permet pas de distinguer si l'arrière-plan a également changé. Créez séparément un instantané au niveau du localisateur de l'élément modifié et un instantané de la zone environnante. Évaluez que la cible a changé et que l'environnement n'a pas changé. Si l'instantané de la zone environnante échoue, il s'agit d'une régression. Détails : ../../references/visual-change-protocol.md §2.
  13. prefers-reduced-motion: reduce ne signifie pas "supprimer l'animation" — Ce paramètre indique de remplacer les déclencheurs vestibulaires (mouvements donnant une sensation de déplacement comme les échelles/translations) par des alternatives plus douces, et non de supprimer toutes les transitions. Par conséquent, n'utilisez pas une assertion "nombre d'animations = 0" pour le test reduced-motion. Vérifiez que les animations basées sur transform: scale/translate ont disparu ou ont été remplacées par des opacités/dissolutions. Source : MDN prefers-reduced-motion (Baseline 2020-01).
  14. Un test de propagation de décision qui n'a que le golden est un ÉCHEC — Couverture du manifeste de propagation de décision — Le fichier .design/decisions.yaml énumère les surfaces sur lesquelles une décision de conception approuvée doit être répercutée (decision_id → required_surfaces[] → golden + assertions). Si ce manifeste existe, générez les tests conformément à la règle de couverture 4. Le point clé est le suivant : s'il n'y a qu'un golden et aucune assertion visible / count / height pour les éléments clés, c'est un ÉCHEC — car un écran vide identique à la ligne de base réussit, et un instantané seul ne prouve pas que "l'utilisateur voit" (même racine que le Gotcha 11). Les surfaces manquantes dans excluded_surfaces sans raison ne sont pas examinées, ce sont des lacunes de couverture. Schéma, règles et vérificateur exécutable : ../../references/visual-change-protocol.md §6 Decision Propagation Manifest.
  15. Ajoutez le nom du canal à la preuve — l'existence d'un instantané n'est pas une observation utilisateur — artifact_snapshot / dom_snapshot / browser_user_visible / device_user_visible n'ont pas la même force. Une phrase RÉUSSITE doit contenir les 5 éléments suivants : viewport · route/état · localisateur visible · count/height · identifiant d'instantané/golden. S'il en manque un, c'est [non vérifié]. Définition des canaux : ../../references/visual-change-protocol.md §7 Evidence Channels. Le protocole de priorité en cas de signalement d'échec par l'utilisateur est la référence harness/docs/guides/skill-design-guide.md §3.8 — ne contestez pas en vous basant sur la réussite du test.

Processus

Étape 0 : Détection du projet

Depuis la racine du projet, détectez la stack UI et le système de design :

Cible de détectionStackOutil de test
package.json + ReactReactPlaywright + axe-core + Storybook (optionnel)
package.json + VueVuePlaywright + axe-core
package.json + SvelteSveltePlaywright + axe-core
package.json + AngularAngularPlaywright + axe-core
pubspec.yaml + FlutterFlutter→ Rediriger vers la compétence flutter-test
*.html (site statique)HTML/CSSPlaywright + axe-core + pa11y

Détection supplémentaire :

  • Tokens de design : **/tokens/**, **/theme/**, **/design-system/**, propriétés personnalisées CSS (--)
  • Storybook : .storybook/, *.stories.*
  • Tailwind : tailwind.config.*
  • CSS-in-JS : styled-components, emotion, vanilla-extract

Étape 1 : Analyse de la cible

Analysez la cible et le type de test à partir de $ARGUMENTS.

Inférence automatique en cas de type non spécifié :

Caractéristique de la cibleType de test
Fichier de tokens, configuration de thèmetoken (validation de cohérence)
Composant, pagea11y (accessibilité) + visual (régression visuelle)
Mise en page, grilleresponsive (adaptatif)
Projet entierTous les types

Étape 2 : Exploration des motifs existants

  • Tests d'accessibilité existants : *.a11y.test.*, importation axe-core
  • Tests visuels existants : *.visual.test.*, __snapshots__/, .loki/
  • Configuration Playwright : playwright.config.*
  • Tests Storybook : Présence de *.stories.*
  • Validation des tokens de design : Règles lint existantes, configuration stylelint

Étape 3 : Génération des tests de cohérence des tokens

Analysez les fichiers de tokens de design et générez un test de validation de leur cohérence.

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

describe('Cohérence des tokens de design', () => {
  // Analyse des propriétés personnalisées CSS
  const cssContent = fs.readFileSync('src/styles/tokens.css', 'utf-8')
  const tokens = [...cssContent.matchAll(/--([^:]+):\s*([^;]+)/g)]

  it('tous les tokens de couleur utilisent un format cohérent (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('les tokens d\'espacement suivent une échelle (base * multiplicateur)', () => {
    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('aucune valeur codée en dur dans les fichiers de composants', () => {
    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)]
    // Les fichiers reset, normalize et les bibliothèques externes sont exclus de la vérification
    expect(rawColors).toEqual([])
    expect(rawPx).toEqual([])
  })

  it('les tokens du mode sombre couvrent tous les tokens du mode clair', () => {
    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([])
  })
})

Étape 4 : Génération des tests d'accessibilité

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

const pages = [
  { name: 'Accueil', path: '/' },
  { name: 'Paramètres', path: '/settings' },
  // Liste des routes détectées à l'étape 1
]

for (const page of pages) {
  test.describe(`${page.name} Accessibilité`, () => {
    test('doit réussir les règles 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('tous les éléments interactifs ont un nom accessible', 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('le contraste des couleurs respecte le ratio 4.5:1', async ({ page: p }) => {
      await p.goto(page.path)
      const results = await new AxeBuilder({ page: p })
        .withRules(['color-contrast'])
        .analyze()
      expect(results.violations).toEqual([])
    })

    test('les cibles tactiles font au moins 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)
        }
      }
    })
  })
}

// Accessibilité en mode sombre (lorsque les tokens incluent le mode sombre)
test('le mode sombre réussit le contraste des couleurs', async ({ page }) => {
  await page.emulateMedia({ colorScheme: 'dark' })
  await page.goto('/')
  const results = await new AxeBuilder({ page })
    .withRules(['color-contrast'])
    .analyze()
  expect(results.violations).toEqual([])
})

Étape 5 : Génération des tests de régression visuelle

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

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

const pages = [
  { name: 'Accueil', path: '/' },
  // Pages principales détectées à l'étape 1
]

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

      // Gotcha 11 — Empêche un rendu vide de réussir en correspondant à la ligne de base.
      // Doit être exécuté "avant" la comparaison d'instantané pour également empêcher
      // qu'un écran vide ne se solidifie comme ligne de base.
      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 }
      )
    })
  }
}

// En présence de Storybook — Tests visuels par composant
// Ne générer cette section que si Storybook est détecté

Validation partielle (ciblée, scoped) — Pour valider un changement ne portant que sur des attributs spécifiques d'un élément particulier, séparez la cible et son environnement (Gotcha 12). Si l'instantané de l'environnement échoue, cela signifie qu'une zone non intentionnelle a changé, et doit être traité comme une régression.

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

test.describe('Changement visuel ciblé — seule la cible change, l\'environnement reste inchangé', () => {
  test('Instantané de l\'élément cible (changement autorisé)', async ({ page }) => {
    await page.goto('/')
    await expect(page.getByTestId('target-card'))
      .toHaveScreenshot('target-card.png', { maxDiffPixelRatio: 0.01 })
  })

  // L'environnement "ne doit pas" changer, utilisez donc un seuil plus strict que pour la cible.
  // Cependant, conformément au Gotcha 4, n'utilisez pas 0 — cela échouerait systématiquement
  // à cause des différences d'anti-aliasing et de rendu des polices.
  test('Instantané de l\'environnement (changement interdit — échec = régression)', async ({ page }) => {
    await page.goto('/')
    await expect(page.getByTestId('surrounding-panel'))
      .toHaveScreenshot('surrounding-panel.png', { maxDiffPixelRatio: 0.001 })
  })
})

Étape 5-b : Génération des tests de propagation de décision (uniquement si .design/decisions.yaml existe)

Si le manifeste est absent, sautez cette étape mais signalez-le comme "manifeste absent" et non comme "sans objet" (pour la même raison que le vérificateur produit NO_MANIFEST + code de sortie 3 — 0 cibles n'est pas une réussite).

Si le manifeste existe, pour chaque decision_id avec status: approved, parcourez required_surfaces[] et générez 1 test par surface. Les décisions avec status: superseded (remplacées) ne donnent pas lieu à des tests — la surface concernée est reprise par la décision approved pointée par superseded_by. Le schéma et la règle de couverture 4 sont la référence dans ../../references/visual-change-protocol.md §6 et ne sont pas redéfinis ici. Il y a trois règles de génération :

  1. Copiez route_or_entry · state · viewport_or_container de la surface dans la configuration du test.
  2. Exécutez assertions[] avant la comparaison golden — cela empêche qu'un rendu vide ne soit figé comme ligne de base.
  3. Utilisez selectors[] pour l'instantané du localisateur cible et le reste de la zone pour l'instantané de l'environnement (en lien avec Gotcha 12).
// tests/design/decision-DEC-20260813-001.test.ts — généré à partir du manifeste
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[] — avant la comparaison 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 — n'a de sens qu'après avoir réussi l'assertion ci-dessus
  await expect(main).toHaveScreenshot(
    'DEC-20260813-001/dashboard.desktop.main.png', { maxDiffPixelRatio: 0.01 }
  )
})

Après la génération, exécutez le vérificateur de couverture (§6) et vérifiez le code de sortie 0. Le code de sortie 2 (SCHEMA_ERROR) signifie que la forme d'entrée est incorrecte et empêche le jugement ; il ne doit pas être interprété comme une réussite même si violations=0 est affiché. Le code de sortie 3 (NO_SURFACE · NO_DECISION) est traité comme NO_MANIFEST, signalant 0 cible. S'il reste une surface avec golden mais sans assertions, c'est un ÉCHEC, et le fait d'avoir créé un fichier de test ne remplace pas la couverture.

Les surfaces répertoriées dans excluded_surfaces ne donnent pas lieu à des tests, mais le rapport doit les énumérer avec leur raison.

Étape 6 : Génération des tests de mise en page adaptative (responsive)

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

// Utilisez les points de rupture du système de design du projet
const breakpoints = [
  { name: 'sm', width: 640 },
  { name: 'md', width: 768 },
  { name: 'lg', width: 1024 },
  { name: 'xl', width: 1280 },
]

test.describe('Mise en page adaptative', () => {
  for (const bp of breakpoints) {
    test(`pas de débordement horizontal à ${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)
    })
  }
})

Étape 7 : Synthèse et rapport

Générez un rapport au format suivant (traduisez le contenu du rapport qui apparaîtrait dans la sortie) :

📋 Rapport de tests de conception — [Nom du projet]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

✅ Tests de tokens : 4 tests (tous réussis)
  → 4 tests de cohérence des tokens de conception

✅ Tests d'accessibilité : 8 tests (tous réussis)
  → 4 pages × 2 tests (WCAG 2.2 AA + contraste des couleurs)
  → Mode sombre testé

✅ Tests de régression visuelle : 12 tests (tous réussis)
  → 4 pages × 3 formats (mobile, tablette, desktop)
  → Seuil : maxDiffPixelRatio=0.01

⚠️  Tests de décision : 1 manifeste trouvé, 1 test généré
  → DEC-20260813-001 : 1 surface (dashboard.desktop.main) ✓
  → Aucune surface exclue

📊 Résumé : 25 tests, 25 réussis, 0 échecs

Étape 8 : Vérification de la validité des preuves (ne pas sauter)

Avant de marquer la tâche comme terminée, exécutez les 3 contrôles de validation définis dans harness/docs/guides/qa-evaluation-guide.md §Evidence Validity Gate :

  1. Preuve d'observabilité : chaque assertion PASS mentionne-t-elle les 5 éléments (viewport · route/état · localisateur visible · count/height · identifiant d'instantané/golden) ?
  2. Preuve de causalité : le contrôle négatif a-t-il été effectué ? La modification a-t-elle systématiquement entraîné un échec, et le rétablissement a-t-il systématiquement entraîné une réussite ?
  3. Preuve d'absence de régression : les instantanés ont-ils été replacés dans le contrôle de version afin que les exécutions suivantes soient comparées à la même ligne de base ?

Individual skills in this repo

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

Skills associés