with one click
accessibility-testing
accessibility-testing. Use when writing, running, or improving tests with accessibility testing.
Menu
accessibility-testing. Use when writing, running, or improving tests with accessibility testing.
Choose a 3D generation or reconstruction pipeline given input type, output format, and use case. Use when you need help with 3d pipeline.
|. Use when you need help with 8 bit orbit video template.
Produce the Agent Card and skills schema for an agent that should be callable over A2A. Use when you need help with a2a agent spec.
Design an A2A integration between two agents — Agent Card, task schemas, auth, streaming or polling. Use when you need help with a2a integrator.
Pre-deployment review of an automated-alignment-research pipeline, including sandbox isolation and log integrity. Use when you need help with aar deployment review.
Design an LLM A/B test — pick platform (Statsig or GrowthBook), primary metric, guardrails, sample size with LLM-noise buffer, CUPED, sequential stopping, and multiple-comparison correction. Use when you need help with ab plan.
| name | accessibility-testing |
| description | accessibility-testing. Use when writing, running, or improving tests with accessibility testing. |
| license | CC-BY-NC-SA-4.0 |
| metadata | {"risk":"unknown","source":"community","kind":"mode","category":"testing"} |
You are an expert accessibility (a11y) testing specialist with deep knowledge of WCAG guidelines, ARIA patterns, and inclusive design. You ensure digital products are usable by everyone, including people with disabilities, through comprehensive accessibility testing and remediation.
// utils/accessibility-checker.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
/**
* Comprehensive accessibility test helper
*/
export async function checkAccessibility(
page: any,
options?: {
include?: string[];
exclude?: string[];
tags?: string[];
rules?: any;
}
) {
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(options?.tags || ['wcag2a', 'wcag2aa', 'wcag21aa'])
.include(options?.include || [])
.exclude(options?.exclude || [])
.analyze();
const violations = accessibilityScanResults.violations;
if (violations.length > 0) {
console.log('\n=== Accessibility Violations ===\n');
violations.forEach((violation, index) => {
console.log(`${index + 1}. ${violation.id}: ${violation.description}`);
console.log(` Impact: ${violation.impact}`);
console.log(` WCAG: ${violation.tags.join(', ')}`);
console.log(` Affected nodes: ${violation.nodes.length}`);
violation.nodes.forEach((node, nodeIndex) => {
console.log(` ${nodeIndex + 1}. ${node.html}`);
console.log(` ${node.failureSummary}`);
});
console.log('');
});
}
return accessibilityScanResults;
}
// tests/accessibility/page-accessibility.spec.ts
import { test, expect } from '@playwright/test';
import { checkAccessibility } from '../utils/accessibility-checker';
test.describe('Page Accessibility', () => {
test('homepage should have no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await checkAccessibility(page);
expect(results.violations).toHaveLength(0);
});
test('should have proper heading hierarchy', async ({ page }) => {
await page.goto('/');
// Check for h1
const h1Count = await page.locator('h1').count();
expect(h1Count).toBe(1);
// Check heading order
const headings = await page.locator('h1, h2, h3, h4, h5, h6').all();
const levels = await Promise.all(
headings.map(async (h) => {
const tag = await h.evaluate((el) => el.tagName);
return parseInt(tag.substring(1));
})
);
// Verify no skipped levels
for (let i = 1; i < levels.length; i++) {
const diff = levels[i] - levels[i - 1];
expect(diff).toBeLessThanOrEqual(1);
}
});
test('all images should have alt text', async ({ page }) => {
await page.goto('/');
const images = await page.locator('img').all();
for (const img of images) {
const alt = await img.getAttribute('alt');
const ariaLabel = await img.getAttribute('aria-label');
const ariaLabelledBy = await img.getAttribute('aria-labelledby');
const role = await img.getAttribute('role');
// Image should have alt, aria-label, or be decorative
const hasLabel = alt !== null || ariaLabel || ariaLabelledBy;
const isDecorative = role === 'presentation' || role === 'none' || alt === '';
expect(hasLabel || isDecorative).toBe(true);
}
});
test('form inputs should have labels', async ({ page }) => {
await page.goto('/contact');
const inputs = await page.locator('input, select, textarea').all();
for (const input of inputs) {
const id = await input.getAttribute('id');
const ariaLabel = await input.getAttribute('aria-label');
const ariaLabelledBy = await input.getAttribute('aria-labelledby');
const type = await input.getAttribute('type');
// Skip hidden and submit inputs
if (type === 'hidden' || type === 'submit') continue;
// Check for associated label
let hasLabel = ariaLabel || ariaLabelledBy;
if (id) {
const label = await page.locator(`label[for="${id}"]`).count();
hasLabel = hasLabel || label > 0;
}
expect(hasLabel).toBe(true);
}
});
test('should have skip to main content link', async ({ page }) => {
await page.goto('/');
// Tab to first focusable element (should be skip link)
await page.keyboard.press('Tab');
const skipLink = page.locator('a:has-text("Skip to main content")').first();
await expect(skipLink).toBeFocused();
// Click skip link
await skipLink.click();
// Main content should be focused
const main = page.locator('main, [role="main"]');
await expect(main).toBeFocused();
});
test('should have proper color contrast', async ({ page }) => {
await page.goto('/');
const results = await checkAccessibility(page, {
tags: ['wcag2aa'],
rules: {
'color-contrast': { enabled: true },
},
});
const contrastViolations = results.violations.filter(
(v) => v.id === 'color-contrast'
);
expect(contrastViolations).toHaveLength(0);
});
});
// tests/accessibility/keyboard-navigation.spec.ts
test.describe('Keyboard Navigation', () => {
test('should navigate entire page with keyboard', async ({ page }) => {
await page.goto('/');
// Get all focusable elements
const focusableElements = await page.evaluate(() => {
const elements = Array.from(
document.querySelectorAll(
'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
);
return elements.length;
});
let tabCount = 0;
const maxTabs = focusableElements + 5; // Buffer for safety
// Tab through all elements
while (tabCount < maxTabs) {
await page.keyboard.press('Tab');
tabCount++;
// Check if we've cycled back to first element
const currentFocus = await page.evaluate(() => {
return document.activeElement?.tagName;
});
if (tabCount > focusableElements && currentFocus === 'BODY') {
break;
}
}
// Should have tabbed through all focusable elements
expect(tabCount).toBeGreaterThan(0);
});
test('modal should trap focus', async ({ page }) => {
await page.goto('/');
// Open modal
await page.click('[data-testid="open-modal"]');
// Wait for modal
const modal = page.locator('[role="dialog"]');
await expect(modal).toBeVisible();
// First focusable element should be focused
const firstFocusable = modal.locator(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
).first();
await expect(firstFocusable).toBeFocused();
// Tab through modal elements
for (let i = 0; i < 10; i++) {
await page.keyboard.press('Tab');
// Focus should stay within modal
const focusedElement = page.locator(':focus');
const isInsideModal = await focusedElement.evaluate(
(el, modalEl) => modalEl.contains(el),
await modal.elementHandle()
);
expect(isInsideModal).toBe(true);
}
// Close modal with Escape
await page.keyboard.press('Escape');
await expect(modal).not.toBeVisible();
});
test('dropdown menu should be keyboard accessible', async ({ page }) => {
await page.goto('/');
const menuButton = page.locator('[aria-haspopup="true"]').first();
// Focus menu button
await menuButton.focus();
await expect(menuButton).toBeFocused();
// Open menu with Enter
await page.keyboard.press('Enter');
const menu = page.locator('[role="menu"]');
await expect(menu).toBeVisible();
// First menu item should be focused
const firstMenuItem = menu.locator('[role="menuitem"]').first();
await expect(firstMenuItem).toBeFocused();
// Navigate with arrow keys
await page.keyboard.press('ArrowDown');
const secondMenuItem = menu.locator('[role="menuitem"]').nth(1);
await expect(secondMenuItem).toBeFocused();
// Close menu with Escape
await page.keyboard.press('Escape');
await expect(menu).not.toBeVisible();
await expect(menuButton).toBeFocused();
});
});
// tests/accessibility/screen-reader.spec.ts
test.describe('Screen Reader Accessibility', () => {
test('should have proper ARIA landmarks', async ({ page }) => {
await page.goto('/');
// Check for required landmarks
const landmarks = {
banner: page.locator('[role="banner"], header'),
navigation: page.locator('[role="navigation"], nav'),
main: page.locator('[role="main"], main'),
contentinfo: page.locator('[role="contentinfo"], footer'),
};
for (const [name, locator] of Object.entries(landmarks)) {
const count = await locator.count();
expect(count, `Missing ${name} landmark`).toBeGreaterThan(0);
}
});
test('should announce dynamic content changes', async ({ page }) => {
await page.goto('/');
// Check for live region
const liveRegion = page.locator('[aria-live="polite"]');
await expect(liveRegion).toBeAttached();
// Trigger notification
await page.click('[data-testid="show-notification"]');
// Live region should contain notification
await expect(liveRegion).toContainText('Success');
});
test('buttons should have accessible names', async ({ page }) => {
await page.goto('/');
const buttons = await page.locator('button').all();
for (const button of buttons) {
const accessibleName = await button.evaluate((el) => {
// Get accessible name (text content, aria-label, or title)
return (
el.getAttribute('aria-label') ||
el.textContent?.trim() ||
el.getAttribute('title') ||
''
);
});
expect(accessibleName.length).toBeGreaterThan(0);
}
});
test('links should have descriptive text', async ({ page }) => {
await page.goto('/');
const links = await page.locator('a').all();
for (const link of links) {
const accessibleName = await link.evaluate((el) => {
return (
el.getAttribute('aria-label') ||
el.textContent?.trim() ||
el.getAttribute('title') ||
''
);
});
// Avoid generic link text
const genericText = ['click here', 'read more', 'here', 'more'];
const isGeneric = genericText.some((text) =>
accessibleName.toLowerCase().includes(text)
);
expect(isGeneric).toBe(false);
expect(accessibleName.length).toBeGreaterThan(0);
}
});
});
// Accessible Component Example
// components/AccessibleButton.tsx
import { ButtonHTMLAttributes, forwardRef } from 'react';
interface AccessibleButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger';
loading?: boolean;
loadingText?: string;
}
export const AccessibleButton = forwardRef<HTMLButtonElement, AccessibleButtonProps>(
({ children, variant = 'primary', loading = false, loadingText, ...props }, ref) => {
return (
<button
ref={ref}
className={`btn btn-${variant}`}
disabled={loading || props.disabled}
aria-busy={loading}
aria-live="polite"
{...props}
>
{loading ? (
<>
<span className="spinner" aria-hidden="true" />
<span>{loadingText || 'Loading...'}</span>
</>
) : (
children
)}
</button>
);
}
);
AccessibleButton.displayName = 'AccessibleButton';
You ensure digital products are accessible to everyone, creating inclusive experiences that comply with standards while providing excellent usability for all users.