{"id":"accessibility-testing","name":"accessibility-testing","summary":"axe-core統合、WCAG 2.2 AAチェックリスト、キーボードナビゲーションテスト、スクリーンリーダーテスト、ARIAパターン検証などが含まれます。","body":"# Accessibility Testing\n\n## axe-core Setup\n\n### jest-axe (unit / component tests)\n```typescript\nimport { axe, toHaveNoViolations } from 'jest-axe'\nimport { render } from '@testing-library/react'\nexpect.extend(toHaveNoViolations)\n\ntest('LoginForm has no a11y violations', async () => {\n  const { container } = render(<LoginForm />)\n  const results = await axe(container)\n  expect(results).toHaveNoViolations()\n})\n```\n\n### playwright-axe (e2e)\n```typescript\nimport { test, expect } from '@playwright/test'\nimport AxeBuilder from '@axe-core/playwright'\n\ntest('homepage passes axe audit', async ({ page }) => {\n  await page.goto('/')\n  const results = await new AxeBuilder({ page })\n    .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])\n    .analyze()\n  expect(results.violations).toEqual([])\n})\n```\n\n### cypress-axe\n```javascript\n// cypress/support/e2e.js\nimport 'cypress-axe'\n\n// in test\ncy.visit('/')\ncy.injectAxe()\ncy.checkA11y(null, {\n  runOnly: { type: 'tag', values: ['wcag2aa'] },\n})\n```\n\n## WCAG 2.2 AA Checklist — Top 20 Violations\n\n| # | Criterion | Check |\n|---|-----------|-------|\n| 1 | Images have alt text | `<img alt=\"description\">` or `alt=\"\"` for decorative |\n| 2 | Form inputs have labels | `<label for>` or `aria-label` or `aria-labelledby` |\n| 3 | Color contrast ≥ 4.5:1 | Normal text; 3:1 for large text (18pt or 14pt bold) |\n| 4 | Heading hierarchy | h1 → h2 → h3, no skipping levels |\n| 5 | Keyboard focusable | All interactive elements reachable via Tab |\n| 6 | Focus visible | `:focus` outline never `outline: none` without replacement |\n| 7 | No keyboard trap | Tab can always exit modals, dropdowns, widgets |\n| 8 | Skip navigation link | First focusable element: \"Skip to main content\" |\n| 9 | Page has `<title>` | Unique, descriptive per page |\n| 10 | Language attribute | `<html lang=\"en\">` |\n| 11 | Error identification | Form errors are text, not color-only |\n| 12 | Error suggestions | Tell users how to fix the error |\n| 13 | Link purpose clear | No \"click here\" or \"read more\" without context |\n| 14 | Button text | No icon-only buttons without `aria-label` |\n| 15 | Table headers | `<th scope=\"col|row\">` on data tables |\n| 16 | No seizure content | No flashing > 3 times/sec |\n| 17 | Status messages | `role=\"status\"` or `aria-live` for dynamic updates |\n| 18 | Reflow at 400% zoom | Single column, no horizontal scroll |\n| 19 | Text spacing adjustable | No overflow when line-height/letter-spacing increased |\n| 20 | Timeout warning | Warn before session expires, allow extension |\n\n## Keyboard Navigation Test Patterns\n\n### Tab order verification\n```typescript\ntest('modal tab order is correct', async ({ page }) => {\n  await page.click('[data-testid=\"open-modal\"]')\n\n  // First focus should be the modal's close button or heading\n  await expect(page.locator('[aria-label=\"Close dialog\"]')).toBeFocused()\n\n  // Tab through: close → input → submit\n  await page.keyboard.press('Tab')\n  await expect(page.locator('#email-input')).toBeFocused()\n\n  await page.keyboard.press('Tab')\n  await expect(page.locator('[type=\"submit\"]')).toBeFocused()\n\n  // Wrap back to close button (focus trap)\n  await page.keyboard.press('Tab')\n  await expect(page.locator('[aria-label=\"Close dialog\"]')).toBeFocused()\n})\n```\n\n### Focus trap in modals\n```typescript\n// Escape should close\nawait page.keyboard.press('Escape')\nawait expect(page.locator('[role=\"dialog\"]')).not.toBeVisible()\n\n// Focus returns to trigger element after close\nawait expect(page.locator('[data-testid=\"open-modal\"]')).toBeFocused()\n```\n\n### Skip navigation link\n```typescript\ntest('skip link goes to main content', async ({ page }) => {\n  await page.goto('/')\n  await page.keyboard.press('Tab')   // first tab = skip link\n\n  const skipLink = page.locator('a:has-text(\"Skip to\")')\n  await expect(skipLink).toBeFocused()\n\n  await page.keyboard.press('Enter')\n  await expect(page.locator('main, #main-content')).toBeFocused()\n})\n```\n\n### Arrow key navigation (listbox/menu)\n```typescript\ntest('dropdown menu responds to arrow keys', async ({ page }) => {\n  await page.click('[aria-haspopup=\"listbox\"]')\n  await page.keyboard.press('ArrowDown')  // first option focused\n  await page.keyboard.press('ArrowDown')  // second option\n  await page.keyboard.press('Enter')      // select\n\n  // Verify selection\n  await expect(page.locator('[aria-selected=\"true\"]')).toContainText('Option 2')\n})\n```\n\n## ARIA Patterns\n\n### Live regions (dynamic announcements)\n```html\n<!-- For important real-time updates (errors, confirmations) -->\n<div role=\"alert\">Your payment failed. Please try again.</div>\n\n<!-- For polite updates (search results count) -->\n<div aria-live=\"polite\" aria-atomic=\"true\">\n  Showing 42 results for \"laptop\"\n</div>\n\n<!-- Screen reader only text (visually hidden) -->\n<style>\n  .sr-only {\n    position: absolute; width: 1px; height: 1px;\n    padding: 0; margin: -1px; overflow: hidden;\n    clip: rect(0,0,0,0); white-space: nowrap; border: 0;\n  }\n</style>\n```\n\n### Dialog / Modal\n```html\n<div role=\"dialog\"\n     aria-modal=\"true\"\n     aria-labelledby=\"dialog-title\"\n     aria-describedby=\"dialog-desc\">\n  <h2 id=\"dialog-title\">Confirm Delete</h2>\n  <p id=\"dialog-desc\">This action cannot be undone.</p>\n  <button>Cancel</button>\n  <button>Delete</button>\n</div>\n```\n\n### Tabs\n```html\n<div role=\"tablist\" aria-label=\"Account settings\">\n  <button role=\"tab\" aria-selected=\"true\"  aria-controls=\"panel-profile\" id=\"tab-profile\">Profile</button>\n  <button role=\"tab\" aria-selected=\"false\" aria-controls=\"panel-billing\" id=\"tab-billing\" tabindex=\"-1\">Billing</button>\n</div>\n<div role=\"tabpanel\" id=\"panel-profile\" aria-labelledby=\"tab-profile\">...</div>\n<div role=\"tabpanel\" id=\"panel-billing\" aria-labelledby=\"tab-billing\" hidden>...</div>\n```\n\n### Combobox / Autocomplete\n```html\n<label for=\"search\">Search users</label>\n<input type=\"text\"\n       id=\"search\"\n       role=\"combobox\"\n       aria-autocomplete=\"list\"\n       aria-expanded=\"true\"\n       aria-controls=\"search-listbox\"\n       aria-activedescendant=\"opt-2\" />\n<ul id=\"search-listbox\" role=\"listbox\">\n  <li role=\"option\" id=\"opt-1\">Alice</li>\n  <li role=\"option\" id=\"opt-2\" aria-selected=\"true\">Bob</li>\n</ul>\n```\n\n## Color Contrast Requirements\n\n| Text Size | Minimum Ratio | Enhanced (AAA) |\n|-----------|--------------|----------------|\n| Normal text (< 18pt / < 14pt bold) | 4.5:1 | 7:1 |\n| Large text (≥ 18pt or ≥ 14pt bold) | 3:1 | 4.5:1 |\n| UI components / icons | 3:1 | — |\n| Decorative / disabled | No requirement | — |\n\nTest tools: [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/), Figma A11y Plugin, Chrome DevTools CSS Overview.\n\n## VoiceOver Quick Reference (macOS)\n\n| Action | Shortcut |\n|--------|----------|\n| Start / Stop VoiceOver | Cmd + F5 |\n| Read next item | VO + Right Arrow |\n| Read page from top | VO + A |\n| Navigate headings | VO + Cmd + H |\n| Navigate links | VO + Cmd + L |\n| Navigate form controls | VO + Cmd + J |\n| Open Web Rotor | VO + U |\n\nVO = Control + Option\n\n## Common Violations and Fixes\n\n```html\n<!-- VIOLATION: Missing alt -->\n<img src=\"logo.png\" />\n<!-- FIX -->\n<img src=\"logo.png\" alt=\"Acme Corp logo\" />\n<!-- Decorative: -->\n<img src=\"divider.png\" alt=\"\" role=\"presentation\" />\n\n<!-- VIOLATION: Icon button with no label -->\n<button><svg>...</svg></button>\n<!-- FIX -->\n<button aria-label=\"Close dialog\"><svg aria-hidden=\"true\">...</svg></button>\n\n<!-- VIOLATION: Placeholder as label -->\n<input placeholder=\"Email address\" />\n<!-- FIX -->\n<label for=\"email\">Email address</label>\n<input id=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n\n<!-- VIOLATION: Color-only error -->\n<input style=\"border: 2px solid red\" />\n<!-- FIX -->\n<input aria-invalid=\"true\" aria-describedby=\"email-error\" />\n<span id=\"email-error\" role=\"alert\">Email is required</span>\n```\n\n## CI Integration (axe-core in GitHub Actions)\n\n```yaml\n# .github/workflows/a11y.yml\nname: Accessibility Tests\non: [pull_request]\n\njobs:\n  a11y:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with: { node-version: '20' }\n      - run: npm ci\n      - run: npx playwright install --with-deps chromium\n      - run: npm run test:a11y\n      - uses: actions/upload-artifact@v4\n        if: failure()\n        with:\n          name: a11y-report\n          path: playwright-report/\n```","author":"@vibeeval","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/vibeeval/vibecosystem/tree/main/skills/accessibility-testing","license":"MIT","category":"coding","lang":"en","tokens":2218,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":["webaim.org"]}}