Skip to content

Latest commit

Β 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Vitest A11y Testing Skill

Senior developer expert skill for accessibility testing with Vitest and W3C ARIA APG patterns

License: MIT Vitest WCAG 2.2

🎯 Overview

This skill provides comprehensive guidance for writing precise, non-flaky accessibility tests using Vitest. It covers both jsdom mode and Browser Mode, with deep expertise in:

  • βœ… W3C ARIA Authoring Practices Guide (APG) patterns
  • βœ… WCAG 2.2 AA/AAA compliance testing
  • βœ… Real browser testing with Vitest Browser Mode
  • βœ… Automated accessibility scans with vitest-axe
  • βœ… Focus management and keyboard navigation
  • βœ… Screen reader compatibility verification

πŸš€ Quick Start

Installation

# For jsdom mode (structural tests + axe scans)
npm install --save-dev vitest vitest-axe @testing-library/react jsdom

# For Browser Mode (interactive tests + keyboard navigation)
npm install --save-dev @vitest/browser playwright vitest-browser-react
npx playwright install chromium

# For Angular
npm install --save-dev vitest vitest-axe @testing-library/angular
npm install --save-dev @vitest/browser playwright vitest-browser-angular

Basic Configuration

vitest.config.ts (Browser Mode - Recommended for a11y)

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: 'playwright',
      instances: [{ browser: 'chromium' }],
      headless: true,
    },
  },
});

Your First Accessibility Test

import { page, userEvent } from 'vitest/browser';
import { render } from 'vitest-browser-react';
import { test, expect } from 'vitest';

test('Modal is accessible', async () => {
  // WCAG 4.1.2: Name, Role, Value
  // APG: Dialog Pattern
  
  const { getByRole } = render(<Modal isOpen title="Settings" />);
  
  // Given: Modal is rendered
  const modal = getByRole('dialog');
  
  // Then: Has correct ARIA attributes
  await expect.element(modal).toHaveAttribute('aria-modal', 'true');
  await expect.element(modal).toHaveAccessibleName('Settings');
  
  // When: User presses Escape
  await userEvent.keyboard('{Escape}');
  
  // Then: Modal closes (WCAG 2.1.2: No Keyboard Trap)
  await expect.element(modal).not.toBeInTheDocument();
});

πŸ“š Documentation

Core Guides

Examples

πŸŽ“ Key Concepts

Browser Mode vs jsdom

Feature Browser Mode jsdom Mode
Focus Testing βœ… Real focus behavior ❌ Simulated
Keyboard Nav βœ… Real browser events ⚠️ Limited
Visual Testing βœ… Computed styles ❌ Not available
Speed Slower (real browser) Faster (simulated)
Use Case Interactive a11y tests Structural a11y tests

Rule of Thumb:

  • Browser Mode: For APG patterns (Dialog, Tabs, Combobox, etc.)
  • jsdom Mode: For axe scans and semantic HTML verification

Test Format: Gherkin (Given-When-Then)

All tests follow Gherkin format for clarity:

test('description of what is being tested', async () => {
  // WCAG X.X.X: Criterion name
  // APG: "Specific rule from APG" (if applicable)
  
  // Given: Setup - render component with specific state
  const { container } = await render(Component, { props });
  
  // When: Action - user interaction (if applicable)
  await userEvent.keyboard('{Tab}');
  
  // Then: Assertion - verify accessibility contract
  await expect.element(element).toHaveFocus();
});

Supported APG Patterns

Pattern Status Documentation
Dialog (Modal) βœ… APG Patterns
Tabs βœ… APG Patterns
Combobox βœ… APG Patterns
Menu Button βœ… APG Patterns
Accordion βœ… APG Patterns
Listbox βœ… APG Patterns
Slider βœ… APG Patterns
Tooltip βœ… APG Patterns
Tree View βœ… APG Patterns
Toolbar βœ… APG Patterns
Breadcrumb βœ… APG Patterns
Alert βœ… APG Patterns
Alert Dialog βœ… APG Patterns

πŸ” WCAG 2.2 Coverage

This skill covers all WCAG 2.2 Level AA criteria, including the 9 new success criteria:

  • 2.4.11 Focus Not Obscured (Minimum) - AA
  • 2.4.12 Focus Not Obscured (Enhanced) - AAA
  • 2.5.7 Dragging Movements - AA
  • 2.5.8 Target Size (Minimum) - AA (24x24 CSS pixels)
  • 3.2.6 Consistent Help - A
  • 3.3.7 Redundant Entry - A
  • 3.3.8 Accessible Authentication (Minimum) - AA
  • 3.3.9 Accessible Authentication (Enhanced) - AAA

See WCAG 2.2 Reference for complete details.

πŸ› οΈ Features

Mandatory Autofix Behavior

The skill automatically detects and fixes common issues:

  1. βœ… Detects failures (a11y assertions, test environment, imports, DI, aliases)
  2. βœ… Applies minimal safe edits to code/config/spec files
  3. βœ… Re-runs the same failing test scope
  4. βœ… Repeats until green or blocked by missing product decision
  5. βœ… Reports exactly what was changed and why

Angular v20+ Support

  • βœ… Automatic detection of Angular projects
  • βœ… Uses ng test (required for Angular v20+)
  • βœ… Handles templateUrl, styleUrls, and path aliases
  • βœ… Automatic DI provider patching

Framework Support

  • βœ… React - Full support with vitest-browser-react
  • βœ… Angular - Full support with @testing-library/angular
  • βœ… Vue - Full support with vitest-browser-vue
  • βœ… Svelte - Full support with vitest-browser-svelte

πŸ“– Usage Examples

Testing Focus Management

test('Modal receives focus when opened', async () => {
  // WCAG 2.4.3: Focus Order
  // APG: "Focus moves to element inside dialog"
  
  const { getByRole } = render(<App />);
  const trigger = getByRole('button', { name: /open/i });
  
  // When: User opens modal
  await trigger.click();
  
  // Then: Focus moves inside modal
  const modal = getByRole('dialog');
  await expect.element(modal).toHaveFocus();
});

Testing Keyboard Navigation

test('Tab key navigates through menu items', async () => {
  // APG: "Tab β€” Moves focus to next focusable element"
  // WCAG 2.1.1: Keyboard accessible
  
  const { getByRole } = render(<Menu />);
  const button = getByRole('button', { name: /menu/i });
  
  // Given: Menu is open
  await button.click();
  
  // When: User presses Tab
  await userEvent.keyboard('{Tab}');
  
  // Then: Focus moves to first menu item
  const firstItem = getByRole('menuitem', { name: /save/i });
  await expect.element(firstItem).toHaveFocus();
});

Testing with axe-core

import { axe } from 'vitest-axe';

test('Component has no WCAG AA violations', async () => {
  const { container } = render(<MyComponent />);
  
  const results = await axe(container, {
    runOnly: { 
      type: 'tag', 
      values: ['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'] 
    },
  });
  
  expect(results).toHaveNoViolations();
});

🀝 Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines.

πŸ“ License

MIT License - see LICENSE file for details.

πŸ”— Resources

πŸ’‘ Tips

  1. Always read the component first - Never generate tests from a description alone
  2. Use Browser Mode for interactive tests - Focus, keyboard, and visual testing require a real browser
  3. Cite WCAG and APG rules - Every test should reference the specific criterion it verifies
  4. Follow Gherkin format - Given-When-Then makes tests readable and maintainable
  5. Test accessibility contracts, not implementation - Use semantic queries (getByRole) over test IDs

πŸ†˜ Support

  • Issues: Report bugs or request features via GitHub Issues
  • Discussions: Ask questions in GitHub Discussions
  • Documentation: Check SKILL.md for complete reference

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors