Advertencias
- No generar pruebas sin detectar el stack — Detecta primero el framework del proyecto: React/Vue/Svelte/Flutter/HTML. No generes pruebas de Playwright para un proyecto Flutter.
- No omitir los criterios WCAG 2.2 AA — Las pruebas de accesibilidad deben basarse estrictamente en WCAG 2.2 AA. Proporción de contraste: 4.5:1 (texto normal), 3:1 (texto grande/UI no textual). Tamaño mínimo de objetivo táctil: 24×24px (AA) / 44×44px (AAA). Fuente: design-kit design-audit Gotcha #3.
- Verificar la versión del conjunto de reglas de axe-core — Al usar
@axe-core/playwrightoaxe-core, verifica la versión instalada en el proyecto; hay diferencias en las reglas entre versiones mayores. - No omitir el umbral en pruebas de regresión visual — La comparación píxel a píxel produce falsos positivos frecuentes debido al anti-aliasing y diferencias en el renderizado de fuentes. Es obligatorio usar un umbral como
maxDiffPixelRatio: 0.01. - Definir claramente el alcance de la detección de valores hardcodeados en la validación de tokens — La presencia de valores raw como
#ff0000,16px,400en CSS no implica automáticamente un error. Se deben excluir los archivos de reset/normalize CSS y los de librerías externas. - Verificar la fuente de los breakpoints en pruebas responsivas — Usa los breakpoints definidos en el sistema de diseño del proyecto. No introduzcas arbitrariamente 320/768/1024.
- No omitir las pruebas en modo oscuro — Si los tokens de diseño tienen mapeo para modo oscuro, prueba en ambos modos, usando la media query
prefers-color-scheme: darko el cambio de clase de token. - Verificar la dependencia de Storybook — No asumas la existencia de Storybook para las pruebas de regresión visual. Si no hay Storybook, usa capturas de pantalla de página de Playwright como alternativa.
- El algoritmo de contraste APCA es solo informativo — El criterio de aprobación es WCAG 2.2 AA (fórmula oficial 4.5:1). El valor APCA Lc solo se incluye como información adicional; no lo uses como criterio de PASS/FAIL. Fuente: design-guide Gotcha #10.
- No considerar la creación de la línea base en la primera ejecución como una "prueba aprobada" (control negativo obligatorio) — Playwright, si no existe una instantánea, la captura automáticamente como línea base y la ejecución se marca como aprobada. Por lo tanto, un resultado verde inmediatamente después de
--update-snapshotsno demuestra nada: no se ha comparado con nada (fallo en la activación de la validación de evidencia 2). Después de crear la línea base, realiza obligatoriamente un control negativo: aplica una modificación intencional al elemento objetivo (por ejemplo, cambiar un color de fondo en 1 paso) y ejecuta la prueba para verificar que falla; luego revierte el cambio y verifica que pasa. No informes que la prueba de regresión visual funciona hasta que puedas citar la salida de estas dos ejecuciones como evidencia. Fuente: Playwright Visual Comparisons ("A snapshot doesn't exist ... writing actual" en la primera ejecución),harness/docs/guides/qa-evaluation-guide.md§Evidence Validity Gate checks 2 y 3. - Una instantánea vacía o una página vacía no son prueba de PASS — Incluso si el renderizado falla y se captura una pantalla vacía, la prueba de regresión visual pasará si coincide con la línea base. Incluye siempre una aserción de existencia de contenido junto con la prueba visual (elemento principal visible, número de elementos de lista ≥ 1, altura del contenedor > 0). Un archivo de prueba con solo comparación de instantáneas puede pasar por alto defectos como un colapso de altura no acotado (un caso real). Detalles:
../../references/visual-change-protocol.md§3. - La validación de cambios parciales debe hacerse con instantáneas delimitadas (scoped snapshots) — detectar cambios en áreas no intencionadas — Al validar "solo cambié el borde", usar una sola instantánea de toda la página no permite distinguir si el fondo también cambió. Genera instantáneas por localizador del elemento modificado e instantáneas separadas del área circundante. El elemento objetivo debe cambiar, el área circundante no. Si la instantánea del área circundante falla, es una regresión. Detalles:
../../references/visual-change-protocol.md§2. prefers-reduced-motion: reduceno significa "eliminar animaciones" — Esta configuración indica que se deben reemplazar los movimientos vestibulares (escala, desplazamiento, etc.) por alternativas más suaves, no eliminar todas las transiciones. Por lo tanto, no hagas una aserción de "0 animaciones" en la prueba de reduced-motion. Verifica que las animaciones basadas entransform: scale/translatehayan desaparecido o hayan sido reemplazadas por transiciones de opacidad/disolución. Fuente: MDN prefers-reduced-motion (Baseline 2020-01).- Una prueba de propagación de decisiones que solo tiene golden es un FAIL — Cobertura del Manifiesto de Propagación de Decisiones — El archivo
.design/decisions.yamlenumera qué superficies deben reflejar una decisión de diseño aprobada (decision_id→required_surfaces[]→ golden + assertions). Si existe este manifiesto, genera las pruebas siguiendo las 4 reglas de cobertura. El punto clave es: si solo hay golden y faltan aserciones de visibilidad / conteo / altura de elementos clave, es un FAIL, porque una pantalla vacía que coincide con la línea base no demuestra que "el usuario la ve" (misma raíz que Gotcha 11). Las superficies enexcluded_surfacessin razón no son revisadas, sino lagunas de cobertura. Esquema, reglas y verificador ejecutable:../../references/visual-change-protocol.md§6 Decision Propagation Manifest. - Etiqueta los canales de evidencia — la existencia de una instantánea no es una observación del usuario —
artifact_snapshot/dom_snapshot/browser_user_visible/device_user_visibletienen diferentes intensidades. Una sentencia PASS debe incluir 5 elementos: viewport · ruta/estado · localizador visible · conteo/altura · id de captura/golden. Si falta uno, es[no verificado]. Definición de canales:../../references/visual-change-protocol.md§7 Evidence Channels. El protocolo de prioridad cuando un usuario informa un fallo esharness/docs/guides/skill-design-guide.md§3.8; no lo refutes basándote en que la prueba pasó.
Proceso
Paso 0: Detección del proyecto
Detecta el stack de UI y el sistema de diseño en la raíz del proyecto:
| Objetivo | Stack | Herramienta de prueba |
|---|---|---|
package.json + React | React | Playwright + axe-core + Storybook (opcional) |
package.json + Vue | Vue | Playwright + axe-core |
package.json + Svelte | Svelte | Playwright + axe-core |
package.json + Angular | Angular | Playwright + axe-core |
pubspec.yaml + Flutter | Flutter | → Redirigir al skill flutter-test |
*.html (sitio estático) | HTML/CSS | Playwright + axe-core + pa11y |
Detección adicional:
- Tokens de diseño:
**/tokens/**,**/theme/**,**/design-system/**, propiedades personalizadas CSS (--) - Storybook:
.storybook/,*.stories.* - Tailwind:
tailwind.config.* - CSS-in-JS: styled-components, emotion, vanilla-extract
Paso 1: Análisis del objetivo
Analiza el objetivo y el tipo de prueba desde $ARGUMENTS.
Si no se especifica el tipo, se infiere automáticamente:
| Característica del objetivo | Tipo de prueba |
|---|---|
| Archivo de tokens, configuración de tema | token (validación de consistencia) |
| Componente, página | a11y (accesibilidad) + visual (regresión visual) |
| Diseño, grid | responsive (adaptable) |
| Proyecto completo | Todos los tipos |
Paso 2: Exploración de patrones existentes
- Pruebas de accesibilidad existentes:
*.a11y.test.*, importación de axe-core - Pruebas visuales existentes:
*.visual.test.*,__snapshots__/,.loki/ - Configuración de Playwright:
playwright.config.* - Pruebas de Storybook: existencia de
*.stories.* - Validación de tokens de diseño: reglas lint existentes, configuración de stylelint
Paso 3: Generación de pruebas de consistencia de tokens
Analiza los archivos de tokens de diseño y genera pruebas para verificar la consistencia.
// tests/design/tokens.test.ts
import { describe, it, expect } from 'vitest'
import fs from 'fs'
describe('Consistencia de Tokens de Diseño', () => {
// Parseo de propiedades personalizadas CSS
const cssContent = fs.readFileSync('src/styles/tokens.css', 'utf-8')
const tokens = [...cssContent.matchAll(/--([^:]+):\s*([^;]+)/g)]
it('todos los tokens de color usan un formato consistente (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('los tokens de espaciado siguen una escala (base * multiplicador)', () => {
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 hay valores hardcodeados en los archivos de componente', () => {
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)]
// Excluir archivos de reset, normalize y librerías externas de la comprobación
expect(rawColors).toEqual([])
expect(rawPx).toEqual([])
})
it('los tokens de modo oscuro cubren todos los tokens de modo claro', () => {
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([])
})
})
Paso 4: Generación de pruebas de accesibilidad
// tests/design/a11y.test.ts
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
const pages = [
{ name: 'Inicio', path: '/' },
{ name: 'Configuración', path: '/settings' },
// Lista de rutas detectada en el Paso 1
]
for (const page of pages) {
test.describe(`${page.name} Accesibilidad`, () => {
test('debe pasar 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('todos los elementos interactivos tienen nombres accesibles', 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('el contraste de color cumple con la relación 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('los objetivos táctiles tienen al menos 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)
}
}
})
})
}
// Accesibilidad en modo oscuro (cuando los tokens tienen modo oscuro)
test('el modo oscuro cumple con el contraste de color', async ({ page }) => {
await page.emulateMedia({ colorScheme: 'dark' })
await page.goto('/')
const results = await new AxeBuilder({ page })
.withRules(['color-contrast'])
.analyze()
expect(results.violations).toEqual([])
})
Paso 5: Generación de pruebas de regresión visual
// 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: 'Inicio', path: '/' },
// Páginas principales detectadas en el Paso 1
]
for (const vp of viewports) {
for (const pg of pages) {
test(`${pg.name} - ${vp.name} instantánea visual`, async ({ page }) => {
await page.setViewportSize({ width: vp.width, height: vp.height })
await page.goto(pg.path)
await page.waitForLoadState('networkidle')
// Gotcha 11 — Evitar que un render vacío coincida con la línea base y pase.
// Debe ejecutarse "antes" de la comparación de instantáneas para evitar que una pantalla vacía se fije como línea 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 }
)
})
}
}
// Cuando hay Storybook — pruebas visuales por componente
// Esta sección solo se genera si se detecta Storybook
Validación de cambio delimitado (scoped) — Al validar un cambio que solo modifica una propiedad específica de un elemento, separa el elemento objetivo del área circundante (Gotcha 12). Si la instantánea del área circundante falla, significa que un área no intencionada ha cambiado, por lo que se trata como una regresión.
// tests/design/scoped-change.test.ts
import { test, expect } from '@playwright/test'
test.describe('Cambio visual delimitado — el objetivo cambia, el entorno permanece inmutable', () => {
test('Instantánea del elemento objetivo (se permite el cambio)', async ({ page }) => {
await page.goto('/')
await expect(page.getByTestId('target-card'))
.toHaveScreenshot('target-card.png', { maxDiffPixelRatio: 0.01 })
})
// El área circundante "no debe cambiar", por lo que se usa un umbral más estricto que para el objetivo.
// Sin embargo, según Gotcha 4, no uses 0: fallará constantemente debido a diferencias de anti-aliasing y renderizado de fuentes.
test('Instantánea del área circundante (no se permite cambio — si falla, es regresión)', async ({ page }) => {
await page.goto('/')
await expect(page.getByTestId('surrounding-panel'))
.toHaveScreenshot('surrounding-panel.png', { maxDiffPixelRatio: 0.001 })
})
})
Paso 5-b: Generación de pruebas de propagación de decisiones (solo si existe .design/decisions.yaml)
Si no hay manifiesto, sáltate este paso, pero informa como "ausencia de manifiesto", no como "no aplicable"
(es la misma razón por la que el verificador emite NO_MANIFEST + exit 3: 0 objetivos es diferente de una prueba aprobada).
Si existe el manifiesto, por cada decision_id con status: approved, recorre required_surfaces[] y genera una prueba por superficie.
Las decisiones con status: superseded (reemplazadas) no generan pruebas: la superficie correspondiente es cubierta por la decisión
approved a la que apunta superseded_by. El esquema y las 4 reglas de cobertura están definidos en ../../references/visual-change-protocol.md §6,
y no se redefinen aquí. Las reglas de generación son tres:
- Usa
route_or_entry,state, yviewport_or_containerde la superficie directamente en la configuración de la prueba. - Ejecuta
assertions[]antes de la comparación con el golden: evita que un render vacío se fije como línea base. - Usa
selectors[]para la instantánea del localizador objetivo, y el exterior para la instantánea del área circundante (emparejado con Gotcha 12).
// tests/design/decision-DEC-20260813-001.test.ts — generado desde el manifiesto
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[] — antes de la comparación 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 — solo tiene sentido después de pasar las aserciones anteriores
await expect(main).toHaveScreenshot(
'DEC-20260813-001/dashboard.desktop.main.png', { maxDiffPixelRatio: 0.01 }
)
})
Después de la generación, ejecuta el verificador de cobertura (§6) y confirma que el código de salida sea 0. El código de salida 2 (SCHEMA_ERROR) significa que la forma de entrada es incorrecta y no se pudo evaluar; no se considera un pase aunque muestre violations=0.
El código de salida 3 (NO_SURFACE · NO_DECISION) se reporta como 0 objetivos, al igual que NO_MANIFEST. Si hay superficies que contienen golden pero sus assertions están vacías, es un FAIL, y el hecho de haber creado el archivo de prueba no reemplaza la cobertura.
Las superficies listadas en excluded_surfaces no generan pruebas, pero se enumeran en el informe junto con el motivo.
Paso 6: Generación de pruebas de diseño responsivo
// tests/design/responsive.test.ts
import { test, expect } from '@playwright/test'
// Usar los breakpoints del sistema de diseño del proyecto
const breakpoints = [
{ name: 'sm', width: 640 },
{ name: 'md', width: 768 },
{ name: 'lg', width: 1024 },
{ name: 'xl', width: 1280 },
]
test.describe('Diseño Responsivo', () => {
for (const bp of breakpoints) {
test(`sin desbordamiento horizontal en ${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)
})
}
})
Paso 7: Documentación de resultados
Genera un informe de prueba que incluya:
- Resultados de cada tipo de prueba (token, a11y, visual, responsive, decisión)
- Capturas de pantalla de fallos (si los hay)
- Resumen de violaciones de accesibilidad
- Cobertura de superficies de decisión (cuando aplica)
- Recomendaciones para la corrección de problemas encontrados