` | `getByRole('navigation')` |
### Test Pattern
```tsx
import { render, screen } from '@testing-library/react';
describe('UserCard semantic HTML', () => {
it('MUST use button for actions', () => {
render(
);
// Verify semantic elements
expect(screen.getByRole('button', { name: /edit/i })).toBeInTheDocument();
expect(screen.getByRole('img', { name: mockUser.name })).toBeInTheDocument();
expect(screen.getByRole('heading')).toHaveTextContent(mockUser.name);
});
});
```
### ARIA Attribute Validation
| Attribute | When Required | Test |
|-----------|---------------|------|
| `aria-label` | Icon-only buttons | `getByRole('button', { name: 'Close' })` |
| `aria-expanded` | Expandable sections | `expect(button).toHaveAttribute('aria-expanded', 'true')` |
| `aria-describedby` | Error messages | Input references error element |
| `aria-live` | Dynamic content | Toast, notifications |
| `role="alert"` | Error/success messages | `getByRole('alert')` |
| `aria-busy` | Loading states | `expect(region).toHaveAttribute('aria-busy', 'true')` |
---
## Keyboard Navigation (MANDATORY)
**HARD GATE:** All interactive elements MUST be keyboard accessible.
### Required Key Handlers
| Key | Expected Behavior | Where |
|-----|-------------------|-------|
| `Tab` | Move to next focusable element | All interactive elements |
| `Shift+Tab` | Move to previous focusable element | All interactive elements |
| `Enter` | Activate button/link | Buttons, links |
| `Space` | Toggle checkbox, activate button | Checkboxes, buttons |
| `Escape` | Close modal/dropdown | Modals, dropdowns, tooltips |
| `Arrow Up/Down` | Navigate list items | Dropdowns, menus, combobox |
| `Home/End` | Jump to first/last item | Lists, menus |
### Test Pattern
```tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('Dropdown keyboard navigation', () => {
it('MUST open with Enter and navigate with arrows', async () => {
const user = userEvent.setup();
render(
);
const trigger = screen.getByRole('combobox');
await user.tab(); // Focus trigger
expect(trigger).toHaveFocus();
await user.keyboard('{Enter}'); // Open dropdown
expect(screen.getByRole('listbox')).toBeVisible();
await user.keyboard('{ArrowDown}'); // Navigate
expect(screen.getByRole('option', { name: 'Option 1' })).toHaveFocus();
await user.keyboard('{ArrowDown}');
expect(screen.getByRole('option', { name: 'Option 2' })).toHaveFocus();
await user.keyboard('{Escape}'); // Close
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
expect(trigger).toHaveFocus(); // Focus returns
});
});
```
### Tab Order Verification
```tsx
describe('Form tab order', () => {
it('MUST follow logical order', async () => {
const user = userEvent.setup();
render(
);
await user.tab();
expect(screen.getByLabelText('Email')).toHaveFocus();
await user.tab();
expect(screen.getByLabelText('Password')).toHaveFocus();
await user.tab();
expect(screen.getByRole('button', { name: /sign in/i })).toHaveFocus();
});
});
```
### FORBIDDEN Patterns
```tsx
// FORBIDDEN: tabIndex > 0
// Breaks natural tab order
// FORBIDDEN: Removing outline without alternative
button:focus { outline: none; } // Focus not visible
// FORBIDDEN: Click-only handlers
// Not keyboard accessible
// CORRECT: Use button or add keyboard handler