Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
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();
});