mirror of
https://github.com/mattermost/mattermost.git
synced 2026-04-15 05:57:37 -04:00
7.3 KiB
7.3 KiB
Accessibility Interaction Testing Best Practices
Overview
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.
Core Capabilities
Keyboard Navigation Testing
- Arrow Key Navigation - Menu and list traversal
- Tab Navigation - Focus order validation
- Enter/Space Activation - Button and link interaction
- Escape Key Handling - Modal and menu dismissal
- Letter Key Shortcuts - Quick navigation patterns
Focus Management Validation
- Focus Visibility -
:focus-visiblestate verification - Focus Trapping - Modal and dialog containment
- Focus Restoration - Return focus after interactions
- Focus Order - Logical tab sequence validation
State and Behavior Testing
- ARIA State Changes -
aria-expanded,aria-selectedverification - Dynamic Content - Live region announcements
- Error Handling - Validation message accessibility
- Loading States - Progress indication accessibility
Testing Patterns
1. Keyboard Navigation Testing
test('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();
});
2. Focus Trapping in Modals
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();
});
3. ARIA State Management Testing
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();
});
4. Form Validation Accessibility
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('valid@example.com');
await submitButton.focus();
await page.keyboard.press('Enter');
// Verify error cleared
await expect(requiredField).toHaveAttribute('aria-invalid', 'false');
await expect(page.getByRole('alert')).toBeHidden();
});
5. Live Region Announcements
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');
});
6. Focus Visibility Testing
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);
});
7. Complex Navigation Patterns
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();
});