e2e-tests/playwright/docs/accessibility/interaction_testing.md
This guide covers automated accessibility testing through user interaction simulation using Playwright. These techniques test real user experiences with assistive technologies by automating keyboard navigation, focus management, and screen reader workflows.
:focus-visible state verificationaria-expanded, aria-selected verificationtest('keyboard navigation through menu', async ({page}) => {
// Open menu with keyboard
await page.getByRole('button', {name: 'Menu'}).focus();
await page.keyboard.press('Enter');
// Navigate through menu items
await expect(page.getByRole('menuitem').first()).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(page.getByRole('menuitem').nth(1)).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(page.getByRole('menuitem').nth(2)).toBeFocused();
// Test wrapping behavior
await page.keyboard.press('ArrowUp');
await expect(page.getByRole('menuitem').nth(1)).toBeFocused();
// Close menu with Escape
await page.keyboard.press('Escape');
await expect(page.getByRole('menuitem')).toHaveCount(0);
// Verify focus restoration
await expect(page.getByRole('button', {name: 'Menu'})).toBeFocused();
});
test('modal focus trapping', async ({page}) => {
// Open modal
await page.getByRole('button', {name: 'Open modal'}).click();
const modal = page.getByRole('dialog');
// Verify initial focus
await expect(modal).toBeFocused();
// Find all focusable elements in modal
const focusableElements = modal.locator('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
const count = await focusableElements.count();
// Tab through all elements
for (let i = 0; i < count; i++) {
await page.keyboard.press('Tab');
await expect(focusableElements.nth(i)).toBeFocused();
}
// Verify focus wraps back to first element
await page.keyboard.press('Tab');
await expect(modal).toBeFocused(); // Or first focusable element
// Test reverse tabbing
await page.keyboard.press('Shift+Tab');
await expect(focusableElements.last()).toBeFocused();
});
test('expandable section ARIA states', async ({page}) => {
const expandButton = page.getByRole('button', {name: 'Expand section', expanded: false});
const expandableContent = page.getByRole('region', {name: 'Expandable content'});
// Test initial collapsed state
await expect(expandButton).toHaveAttribute('aria-expanded', 'false');
await expect(expandableContent).toBeHidden();
// Expand with keyboard
await expandButton.focus();
await page.keyboard.press('Enter');
// Verify expanded state
await expect(expandButton).toHaveAttribute('aria-expanded', 'true');
await expect(expandableContent).toBeVisible();
// Verify focus moves to content
await expect(expandableContent).toBeFocused();
// Collapse with keyboard
await expandButton.focus();
await page.keyboard.press('Enter');
// Verify collapsed state
await expect(expandButton).toHaveAttribute('aria-expanded', 'false');
await expect(expandableContent).toBeHidden();
});
test('form validation accessibility', async ({page}) => {
const form = page.getByRole('form');
const requiredField = form.getByRole('textbox', {name: 'Email'});
const submitButton = form.getByRole('button', {name: 'Submit'});
// Submit empty form to trigger validation
await submitButton.focus();
await page.keyboard.press('Enter');
// Verify error state accessibility
await expect(requiredField).toHaveAttribute('aria-invalid', 'true');
await expect(page.getByRole('alert')).toBeVisible();
// Verify focus moves to invalid field
await expect(requiredField).toBeFocused();
// Test error message association
const errorId = await page.getByRole('alert').getAttribute('id');
await expect(requiredField).toHaveAttribute('aria-describedby', errorId);
// Fix validation error
await requiredField.fill('[email protected]');
await submitButton.focus();
await page.keyboard.press('Enter');
// Verify error cleared
await expect(requiredField).toHaveAttribute('aria-invalid', 'false');
await expect(page.getByRole('alert')).toBeHidden();
});
test('dynamic content announcements', async ({page}) => {
const liveRegion = page.getByRole('status');
const triggerButton = page.getByRole('button', {name: 'Load content'});
// Verify live region setup
await expect(liveRegion).toHaveAttribute('aria-live', 'polite');
// Trigger content update
await triggerButton.click();
// Wait for content to appear in live region
await expect(liveRegion).toContainText('Loading complete');
// Test urgent announcements
const urgentRegion = page.getByRole('alert');
await page.getByRole('button', {name: 'Urgent action'}).click();
await expect(urgentRegion).toContainText('Critical update');
});
test('focus indicators visibility', async ({page}) => {
const button = page.getByRole('button', {name: 'Save'});
// Tab to button (should show focus indicator)
await page.keyboard.press('Tab');
await toBeFocusedWithFocusVisible(button);
});
test('hierarchical menu navigation', async ({page}) => {
await page.getByRole('button', {name: 'Menu'}).focus();
await page.keyboard.press('Enter');
// Navigate to submenu trigger
const submenuTrigger = page.getByRole('menuitem', {name: /has submenu/});
await submenuTrigger.focus();
// Open submenu with right arrow
await page.keyboard.press('ArrowRight');
// Verify submenu opened and focused
const submenu = page.getByRole('menu').nth(1);
await expect(submenu).toBeVisible();
await expect(submenu.getByRole('menuitem').first()).toBeFocused();
// Navigate in submenu
await page.keyboard.press('ArrowDown');
await expect(submenu.getByRole('menuitem').nth(1)).toBeFocused();
// Return to parent menu with left arrow
await page.keyboard.press('ArrowLeft');
await expect(submenu).toBeHidden();
await expect(submenuTrigger).toBeFocused();
});