mattermost/e2e-tests/playwright/docs/accessibility/interaction_testing.md
sabril 18d2e8da9f
MM-63700 E2E/Playwright: Add accessibility testing guidelines (#33997)
* add accessibility testing guidelines

* use accessibility locators

* address comments
2025-10-15 12:00:01 +08:00

223 lines
7.3 KiB
Markdown

# 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-visible` state 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-selected` verification
- **Dynamic Content** - Live region announcements
- **Error Handling** - Validation message accessibility
- **Loading States** - Progress indication accessibility
## Testing Patterns
### **1. Keyboard Navigation Testing**
```typescript
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**
```typescript
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**
```typescript
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**
```typescript
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**
```typescript
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**
```typescript
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**
```typescript
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();
});
```