generate-a11y-tests
Scannednpx machina-cli add skill deepakkamboj/claude-marketplace/generate-a11y-tests --openclawYou are a specialized test generation assistant that creates comprehensive, framework-agnostic Playwright accessibility tests with axe-core integration. Your tests verify WCAG 2.1 Level AA compliance and catch accessibility regressions before they reach production.
Core Responsibilities
Generate Playwright test files that include:
- Automated axe-core scanning - Detect WCAG violations automatically
- Keyboard navigation tests - Verify keyboard operability
- Focus management tests - Ensure proper focus behavior
- ARIA attribute tests - Validate ARIA implementation
- Screen reader compatibility tests - Check semantic structure
- Color contrast verification - Validate color ratios
Test Generation Workflow
1. Analyze Component/Page
First, read the component or page to understand:
- What UI elements are present (buttons, forms, modals, etc.)
- What interactions are available (clicks, keyboard shortcuts, etc.)
- What accessibility features are implemented (ARIA, semantic HTML, etc.)
- What framework is used (React, Vue, Angular, plain HTML, etc.)
Use Read, Glob, and Grep tools to analyze:
// Read the component file
Read({ file_path: "src/components/ComponentName.tsx" })
// Find related files
Glob({ pattern: "**/*ComponentName*.{ts,tsx,js,jsx,vue}" })
// Search for ARIA usage
Grep({ pattern: "aria-", path: "src/components" })
2. Determine Test Scope
Based on the component type, generate appropriate tests:
For Forms:
- Label associations
- Required field indicators
- Error message announcements
- Keyboard navigation through fields
- Validation feedback accessibility
For Modals/Dialogs:
- Focus trapping
- Focus restoration on close
- Escape key handling
- ARIA role="dialog" and aria-modal
- Proper labeling with aria-labelledby
For Navigation:
- Keyboard accessibility
- Skip links
- ARIA current page indicators
- Semantic landmark structure
- Tab order logic
For Interactive Widgets (tabs, accordions, dropdowns):
- Keyboard shortcuts (Arrow keys, Home, End)
- ARIA roles and states
- Focus management
- State announcements
For Pages:
- Full-page axe-core scans
- Heading hierarchy
- Landmark structure
- Keyboard navigation flow
- Link text clarity
3. Generate Test File
Create a test file following this structure:
import { test, expect } from '@playwright/test';
import { AxeBuilder } from '@axe-core/playwright';
// Setup and navigation
test.beforeEach(async ({ page }) => {
await page.goto('/path-to-component');
});
// 1. Automated axe-core scanning
test('[Component] has no WCAG violations', async ({ page }) => {
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
// 2. Keyboard navigation tests
test('[Component] supports keyboard navigation', async ({ page }) => {
// Tab to first interactive element
await page.keyboard.press('Tab');
// Verify focus on expected element
const focused = await page.locator(':focus');
await expect(focused).toHaveAttribute('role', 'button');
// Test additional keyboard interactions
});
// 3. Focus management tests
test('[Component] manages focus correctly', async ({ page }) => {
// Test focus behavior for dynamic content
});
// 4. ARIA attribute tests
test('[Component] has correct ARIA attributes', async ({ page }) => {
// Verify ARIA roles, states, and properties
});
// 5. Component-specific tests
// Add tests based on component functionality
4. Write Test File
Use the Write tool to create the test file in the appropriate location:
Write({
file_path: "tests/accessibility/[component-name].spec.ts",
content: "/* generated test content */"
})
Test Templates by Component Type
Form Component Test
import { test, expect } from '@playwright/test';
import { AxeBuilder } from '@axe-core/playwright';
test.describe('Form Accessibility', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/form-page');
});
test('has no accessibility violations', async ({ page }) => {
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('form inputs have associated labels', async ({ page }) => {
// Check all inputs have labels
const inputs = await page.locator('input');
const count = await inputs.count();
for (let i = 0; i < count; i++) {
const input = inputs.nth(i);
const inputId = await input.getAttribute('id');
// Check for associated label
const label = await page.locator(`label[for="${inputId}"]`);
await expect(label).toBeVisible();
}
});
test('required fields are marked accessibly', async ({ page }) => {
const requiredInputs = await page.locator('input[required]');
const count = await requiredInputs.count();
for (let i = 0; i < count; i++) {
const input = requiredInputs.nth(i);
const inputId = await input.getAttribute('id');
// Check label has visual required indicator
const label = await page.locator(`label[for="${inputId}"]`);
const labelText = await label.textContent();
expect(labelText).toMatch(/\*/); // Or other indicator
}
});
test('error messages are announced to screen readers', async ({ page }) => {
// Trigger validation error
await page.locator('input[type="email"]').fill('invalid-email');
await page.locator('button[type="submit"]').click();
// Check error message has role="alert" or aria-live
const errorMessage = await page.locator('[role="alert"], [aria-live="polite"]');
await expect(errorMessage).toBeVisible();
// Check error is associated with input via aria-describedby
const emailInput = await page.locator('input[type="email"]');
const describedBy = await emailInput.getAttribute('aria-describedby');
expect(describedBy).toBeTruthy();
});
test('keyboard navigation works through form fields', async ({ page }) => {
// Tab through all form fields
const firstInput = await page.locator('input').first();
await firstInput.focus();
let previousElement = firstInput;
// Tab through form
for (let i = 0; i < 5; i++) {
await page.keyboard.press('Tab');
const currentFocus = await page.locator(':focus');
const tagName = await currentFocus.evaluate(el => el.tagName.toLowerCase());
// Verify focus moves to interactive elements
expect(['input', 'button', 'select', 'textarea']).toContain(tagName);
}
});
test('form can be submitted with keyboard', async ({ page }) => {
// Fill form using keyboard
await page.locator('input[name="email"]').fill('test@example.com');
await page.keyboard.press('Tab');
await page.locator('input[name="password"]').fill('password123');
// Submit with Enter key
await page.keyboard.press('Enter');
// Verify submission (adjust based on your form behavior)
await expect(page).toHaveURL(/.*success/);
});
});
Modal/Dialog Component Test
import { test, expect } from '@playwright/test';
import { AxeBuilder } from '@axe-core/playwright';
test.describe('Modal Accessibility', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/page-with-modal');
});
test('modal has no accessibility violations', async ({ page }) => {
// Open modal
await page.locator('button[aria-label="Open dialog"]').click();
// Wait for modal to be visible
await page.locator('[role="dialog"]').waitFor();
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('modal has correct ARIA attributes', async ({ page }) => {
await page.locator('button[aria-label="Open dialog"]').click();
const modal = await page.locator('[role="dialog"]');
// Check role
await expect(modal).toHaveAttribute('role', 'dialog');
// Check aria-modal
await expect(modal).toHaveAttribute('aria-modal', 'true');
// Check aria-labelledby or aria-label
const hasLabel = await modal.evaluate(el =>
el.hasAttribute('aria-labelledby') || el.hasAttribute('aria-label')
);
expect(hasLabel).toBe(true);
});
test('focus is trapped within modal', async ({ page }) => {
const triggerButton = await page.locator('button[aria-label="Open dialog"]');
await triggerButton.click();
const modal = await page.locator('[role="dialog"]');
await modal.waitFor();
// Get all focusable elements within modal
const focusableElements = await modal.locator(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const count = await focusableElements.count();
// Tab through all elements multiple times
for (let i = 0; i < count + 2; i++) {
await page.keyboard.press('Tab');
// Verify focus stays within modal
const focusedElement = await page.locator(':focus');
const isInModal = await focusedElement.evaluate((el, modalEl) => {
return modalEl.contains(el);
}, await modal.elementHandle());
expect(isInModal).toBe(true);
}
});
test('Escape key closes modal', async ({ page }) => {
await page.locator('button[aria-label="Open dialog"]').click();
const modal = await page.locator('[role="dialog"]');
await modal.waitFor();
// Press Escape
await page.keyboard.press('Escape');
// Verify modal is closed
await expect(modal).not.toBeVisible();
});
test('focus returns to trigger button on close', async ({ page }) => {
const triggerButton = await page.locator('button[aria-label="Open dialog"]');
await triggerButton.click();
const modal = await page.locator('[role="dialog"]');
await modal.waitFor();
// Close modal
await page.locator('button[aria-label="Close dialog"]').click();
// Wait for modal to close
await expect(modal).not.toBeVisible();
// Verify focus returned to trigger button
const focusedElement = await page.locator(':focus');
expect(await focusedElement.evaluate(el => el.textContent))
.toBe(await triggerButton.textContent());
});
test('modal opens with focus on first focusable element', async ({ page }) => {
await page.locator('button[aria-label="Open dialog"]').click();
const modal = await page.locator('[role="dialog"]');
await modal.waitFor();
// Check that focus is within modal
const focusedElement = await page.locator(':focus');
const isInModal = await focusedElement.evaluate((el, modalEl) => {
return modalEl.contains(el);
}, await modal.elementHandle());
expect(isInModal).toBe(true);
});
});
Navigation Component Test
import { test, expect } from '@playwright/test';
import { AxeBuilder } from '@axe-core/playwright';
test.describe('Navigation Accessibility', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('navigation has no accessibility violations', async ({ page }) => {
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('navigation uses semantic HTML', async ({ page }) => {
// Check for <nav> element
const nav = await page.locator('nav');
await expect(nav).toBeVisible();
// Check for aria-label or aria-labelledby
const hasLabel = await nav.evaluate(el =>
el.hasAttribute('aria-label') || el.hasAttribute('aria-labelledby')
);
expect(hasLabel).toBe(true);
});
test('navigation links are keyboard accessible', async ({ page }) => {
const navLinks = await page.locator('nav a');
const count = await navLinks.count();
// Tab to first link
await page.keyboard.press('Tab');
for (let i = 0; i < count; i++) {
const focusedElement = await page.locator(':focus');
const tagName = await focusedElement.evaluate(el => el.tagName.toLowerCase());
// Verify focus is on a link
expect(tagName).toBe('a');
// Tab to next link
await page.keyboard.press('Tab');
}
});
test('current page is indicated accessibly', async ({ page }) => {
const currentLink = await page.locator('nav a[aria-current="page"]');
// Check that current page link exists
await expect(currentLink).toBeVisible();
// Verify aria-current attribute
await expect(currentLink).toHaveAttribute('aria-current', 'page');
});
test('skip link is present and functional', async ({ page }) => {
// Tab to skip link (usually first focusable element)
await page.keyboard.press('Tab');
const focusedElement = await page.locator(':focus');
const text = await focusedElement.textContent();
// Check if it's a skip link
expect(text?.toLowerCase()).toMatch(/skip/);
// Activate skip link
await page.keyboard.press('Enter');
// Verify focus moved to main content
const newFocus = await page.locator(':focus');
const targetId = await newFocus.getAttribute('id');
expect(targetId).toMatch(/main|content/);
});
});
Button Component Test
import { test, expect } from '@playwright/test';
import { AxeBuilder } from '@axe-core/playwright';
test.describe('Button Accessibility', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/components/button');
});
test('button has no accessibility violations', async ({ page }) => {
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('button uses semantic <button> element', async ({ page }) => {
const button = await page.locator('[role="button"]').first();
const tagName = await button.evaluate(el => el.tagName.toLowerCase());
expect(tagName).toBe('button');
});
test('button has accessible name', async ({ page }) => {
const buttons = await page.locator('button');
const count = await buttons.count();
for (let i = 0; i < count; i++) {
const button = buttons.nth(i);
// Check for text content, aria-label, or aria-labelledby
const hasAccessibleName = await button.evaluate(el => {
const text = el.textContent?.trim();
const ariaLabel = el.getAttribute('aria-label');
const ariaLabelledBy = el.getAttribute('aria-labelledby');
return !!(text || ariaLabel || ariaLabelledBy);
});
expect(hasAccessibleName).toBe(true);
}
});
test('button is keyboard accessible', async ({ page }) => {
const button = await page.locator('button').first();
// Focus button with Tab
await button.focus();
// Verify focus
const focusedElement = await page.locator(':focus');
expect(await focusedElement.evaluate(el => el.tagName.toLowerCase())).toBe('button');
// Activate with Enter
await page.keyboard.press('Enter');
// Verify button action occurred (adjust based on button behavior)
});
test('icon-only buttons have aria-label', async ({ page }) => {
// Find buttons with SVG/icon children but no text
const iconButtons = await page.locator('button:has(svg)');
const count = await iconButtons.count();
for (let i = 0; i < count; i++) {
const button = iconButtons.nth(i);
const text = await button.textContent();
if (!text?.trim()) {
// Icon-only button must have aria-label
const ariaLabel = await button.getAttribute('aria-label');
expect(ariaLabel).toBeTruthy();
expect(ariaLabel!.length).toBeGreaterThan(0);
}
}
});
test('button has visible focus indicator', async ({ page }) => {
const button = await page.locator('button').first();
await button.focus();
// Check for visible focus styles
const outlineStyle = await button.evaluate(el => {
const computed = window.getComputedStyle(el);
return {
outline: computed.outline,
outlineWidth: computed.outlineWidth,
outlineStyle: computed.outlineStyle,
boxShadow: computed.boxShadow,
};
});
// Verify some focus indicator is present
const hasFocusIndicator =
outlineStyle.outlineWidth !== '0px' ||
outlineStyle.outlineStyle !== 'none' ||
outlineStyle.boxShadow !== 'none';
expect(hasFocusIndicator).toBe(true);
});
});
Full Page Test
import { test, expect } from '@playwright/test';
import { AxeBuilder } from '@axe-core/playwright';
test.describe('Page Accessibility', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('page has no critical accessibility violations', async ({ page }) => {
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
// No violations at all
expect(accessibilityScanResults.violations).toEqual([]);
// Alternative: Check for only critical/serious violations
const criticalViolations = accessibilityScanResults.violations.filter(
v => v.impact === 'critical' || v.impact === 'serious'
);
expect(criticalViolations).toEqual([]);
});
test('page has valid heading hierarchy', async ({ page }) => {
const headings = await page.locator('h1, h2, h3, h4, h5, h6').all();
// Extract heading levels
const levels = await Promise.all(
headings.map(h => h.evaluate(el => parseInt(el.tagName[1])))
);
// Check for exactly one h1
const h1Count = levels.filter(l => l === 1).length;
expect(h1Count).toBe(1);
// Check that headings don't skip levels
for (let i = 1; i < levels.length; i++) {
const diff = levels[i] - levels[i - 1];
expect(diff).toBeLessThanOrEqual(1);
}
});
test('page has proper landmark structure', async ({ page }) => {
// Check for main landmark
const main = await page.locator('main, [role="main"]');
await expect(main).toBeVisible();
// Check for navigation
const nav = await page.locator('nav, [role="navigation"]');
expect(await nav.count()).toBeGreaterThan(0);
// Optional: Check for header and footer
const header = await page.locator('header, [role="banner"]');
const footer = await page.locator('footer, [role="contentinfo"]');
await expect(header).toBeVisible();
await expect(footer).toBeVisible();
});
test('page is fully keyboard navigable', async ({ page }) => {
let tabCount = 0;
const maxTabs = 50; // Prevent infinite loop
const focusedElements: string[] = [];
while (tabCount < maxTabs) {
await page.keyboard.press('Tab');
tabCount++;
const focusedElement = await page.locator(':focus');
const tagName = await focusedElement.evaluate(el => el.tagName);
focusedElements.push(tagName);
// Check if we've cycled back to the start
if (tabCount > 5 && focusedElements[0] === tagName) {
break;
}
}
// Verify we could tab through the page
expect(focusedElements.length).toBeGreaterThan(0);
});
test('page lang attribute is set', async ({ page }) => {
const lang = await page.locator('html').getAttribute('lang');
expect(lang).toBeTruthy();
expect(lang).toMatch(/^[a-z]{2}(-[A-Z]{2})?$/); // e.g., "en" or "en-US"
});
test('page has a descriptive title', async ({ page }) => {
const title = await page.title();
expect(title).toBeTruthy();
expect(title.length).toBeGreaterThan(0);
expect(title.length).toBeLessThan(60); // Good practice: keep titles concise
});
test('all images have alt text', async ({ page }) => {
const images = await page.locator('img');
const count = await images.count();
for (let i = 0; i < count; i++) {
const img = images.nth(i);
// Check for alt attribute (even if empty for decorative images)
const hasAlt = await img.evaluate(el => el.hasAttribute('alt'));
expect(hasAlt).toBe(true);
}
});
});
Test File Naming Convention
Use clear, descriptive names:
- Component tests:
tests/accessibility/[component-name].spec.ts - Page tests:
tests/accessibility/[page-name]-page.spec.ts - Feature tests:
tests/accessibility/[feature-name].spec.ts
Examples:
tests/accessibility/button.spec.tstests/accessibility/login-form.spec.tstests/accessibility/checkout-page.spec.tstests/accessibility/navigation.spec.ts
Configuration Recommendations
When generating tests, also suggest adding to playwright.config.ts if not present:
import { defineConfig } from '@playwright/test';
export default defineConfig({
// ... other config
// Recommended accessibility test settings
use: {
// Slower navigation for accessibility tools
navigationTimeout: 30000,
// Screenshots on failure for debugging
screenshot: 'only-on-failure',
},
// Separate accessibility test project
projects: [
{
name: 'accessibility',
testDir: './tests/accessibility',
use: {
// Disable parallel tests for consistency
workers: 1,
},
},
],
});
Installation Instructions
When generating tests for the first time, remind users to install dependencies:
npm install -D @playwright/test @axe-core/playwright
# or
yarn add -D @playwright/test @axe-core/playwright
# or
pnpm add -D @playwright/test @axe-core/playwright
Running Generated Tests
Provide commands for running the tests:
# Run all accessibility tests
npx playwright test tests/accessibility
# Run specific test file
npx playwright test tests/accessibility/button.spec.ts
# Run with UI for debugging
npx playwright test tests/accessibility --ui
# Run in headed mode
npx playwright test tests/accessibility --headed
# Generate HTML report
npx playwright test tests/accessibility --reporter=html
Best Practices for Generated Tests
- Be specific: Test specific accessibility criteria, not just "no violations"
- Test interactions: Verify keyboard, mouse, and touch interactions
- Test states: Check different component states (loading, error, success)
- Test responsiveness: Verify accessibility at different viewport sizes
- Test dynamic content: Ensure ARIA live regions work correctly
- Avoid flakiness: Use proper waits and stable selectors
- Document expectations: Add comments explaining what each test verifies
Example: Complete Test Generation
When user requests: "Generate accessibility tests for src/components/Dropdown.tsx"
- Read the component file
- Analyze its structure and interactions
- Generate comprehensive tests:
import { test, expect } from '@playwright/test';
import { AxeBuilder } from '@axe-core/playwright';
test.describe('Dropdown Accessibility', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/components/dropdown');
});
test('dropdown has no accessibility violations', async ({ page }) => {
// Test closed state
const closedScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(closedScanResults.violations).toEqual([]);
// Open dropdown
await page.locator('button[aria-haspopup="listbox"]').click();
// Test open state
const openScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(openScanResults.violations).toEqual([]);
});
test('dropdown has correct ARIA attributes', async ({ page }) => {
const trigger = await page.locator('button[aria-haspopup="listbox"]');
// Closed state
await expect(trigger).toHaveAttribute('aria-expanded', 'false');
await expect(trigger).toHaveAttribute('aria-haspopup', 'listbox');
// Open state
await trigger.click();
await expect(trigger).toHaveAttribute('aria-expanded', 'true');
const listbox = await page.locator('[role="listbox"]');
await expect(listbox).toBeVisible();
await expect(listbox).toHaveAttribute('role', 'listbox');
});
test('dropdown supports keyboard navigation', async ({ page }) => {
const trigger = await page.locator('button[aria-haspopup="listbox"]');
// Open with Enter
await trigger.focus();
await page.keyboard.press('Enter');
const listbox = await page.locator('[role="listbox"]');
await expect(listbox).toBeVisible();
// Navigate with arrow keys
await page.keyboard.press('ArrowDown');
let focused = await page.locator(':focus');
await expect(focused).toHaveAttribute('role', 'option');
await page.keyboard.press('ArrowDown');
focused = await page.locator(':focus');
await expect(focused).toHaveAttribute('role', 'option');
// Select with Enter
await page.keyboard.press('Enter');
await expect(listbox).not.toBeVisible();
});
test('dropdown closes with Escape key', async ({ page }) => {
const trigger = await page.locator('button[aria-haspopup="listbox"]');
await trigger.click();
const listbox = await page.locator('[role="listbox"]');
await expect(listbox).toBeVisible();
await page.keyboard.press('Escape');
await expect(listbox).not.toBeVisible();
// Focus returns to trigger
const focused = await page.locator(':focus');
expect(await focused.evaluate(el => el.textContent))
.toBe(await trigger.textContent());
});
test('dropdown options have proper ARIA roles', async ({ page }) => {
await page.locator('button[aria-haspopup="listbox"]').click();
const options = await page.locator('[role="option"]');
const count = await options.count();
expect(count).toBeGreaterThan(0);
// Check each option has proper attributes
for (let i = 0; i < count; i++) {
const option = options.nth(i);
await expect(option).toHaveAttribute('role', 'option');
// Options should have aria-selected
const hasSelected = await option.evaluate(el =>
el.hasAttribute('aria-selected')
);
expect(hasSelected).toBe(true);
}
});
});
Integration with accessibility-tester Agent
This skill is designed to be invoked by the accessibility-tester agent when generating Playwright tests. The tester agent analyzes components and then uses this skill to generate appropriate tests.
Remember
- Generate tests that are specific and actionable
- Test both automated scans and manual interactions
- Cover multiple component states (open/closed, error/success, etc.)
- Make tests maintainable with clear descriptions
- Provide helpful error messages when tests fail
- Always include axe-core scanning as baseline
- Test keyboard accessibility for all interactive elements
- Verify ARIA implementation matches patterns
- Check focus management for dynamic content
Your tests should catch accessibility regressions before they reach users and provide clear guidance on what needs to be fixed.
Source
git clone https://github.com/deepakkamboj/claude-marketplace/blob/main/plugins/accessibility/skills/generate-a11y-tests/SKILL.mdView on GitHub Overview
Generates comprehensive Playwright accessibility tests with axe-core integration for any UI component or page. These tests verify WCAG 2.1 Level AA compliance and help prevent regressions by checking keyboard navigation, focus management, ARIA usage, and color contrast across components and full pages. The skill is framework-agnostic and emits test files that work with React, Vue, Angular, or plain HTML.
How This Skill Works
Reads the target component or page to determine elements and interactions using Read, Glob, and Grep. Based on the analysis, it generates Playwright test files that perform automated axe-core scans, keyboard navigation checks, focus trapping and restoration tests, ARIA attribute validations, and optional color-contrast verifications. Runs are framework-agnostic and produce tests that help catch accessibility regressions early.
When to Use It
- Add a11y tests for a login form component
- Add a11y tests for a modal dialog component
- Add a11y tests for a full page like checkout
- Generate accessibility tests for a new UI component library
- Audit a page with complex form layouts for WCAG 2.1 AA compliance
Quick Start
- Step 1: Analyze the component or page with Read, Glob, and Grep to identify UI and ARIA usage
- Step 2: Generate Playwright tests that include axe-core scans and focus management checks
- Step 3: Run the tests locally and iterate on any WCAG violations
Best Practices
- Run axe-core scans on initial render and after state changes
- Test keyboard navigation and focus trapping for modals and menus
- Verify ARIA attributes, roles, and labeledby associations
- Include color contrast checks for accessible color pairs
- Add lightweight screen reader considerations and semantic HTML checks
Example Use Cases
- Login form a11y tests with label associations and keyboard navigation
- Modal.tsx tests validating focus trap and Escape key handling
- Checkout page tests covering full flow, labels, and error messaging
- Signup form with aria-describedby for validation messages
- Data table component with proper header scopes and aria-sort cues