/** * Accessibility Tests * FE-TEST-013: Test keyboard navigation, screen reader support * * This test suite covers: * - Keyboard navigation (Tab, Shift+Tab, Enter, Space, Arrow keys) * - Screen reader support (ARIA labels, roles, states) * - Focus management * - Skip links */ import { describe, it, expect, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; // Simple test components const LoginForm = () => (
); const RegisterForm = () => (
); const SimpleDialog = ({ open, onClose, children, }: { open: boolean; onClose: () => void; children: React.ReactNode; }) => { if (!open) return null; return (

Test Dialog

{children}
); }; describe('Accessibility Tests', () => { describe('Keyboard Navigation', () => { it('should navigate through form fields with Tab key', async () => { const user = userEvent.setup(); render(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/mot de passe/i); const submitButton = screen.getByRole('button', { name: /se connecter/i, }); // Focus should start on email input emailInput.focus(); expect(emailInput).toHaveFocus(); // Tab to password input await user.tab(); expect(passwordInput).toHaveFocus(); // Tab to submit button await user.tab(); expect(submitButton).toHaveFocus(); }); it('should navigate backwards with Shift+Tab', async () => { const user = userEvent.setup(); render(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/mot de passe/i); // Start on password input passwordInput.focus(); expect(passwordInput).toHaveFocus(); // Shift+Tab to email input await user.tab({ shift: true }); expect(emailInput).toHaveFocus(); }); it('should activate buttons with Enter key', async () => { const user = userEvent.setup(); const handleClick = vi.fn(); render(); const button = screen.getByRole('button', { name: /test button/i }); button.focus(); expect(button).toHaveFocus(); // Press Enter await user.keyboard('{Enter}'); expect(handleClick).toHaveBeenCalledTimes(1); }); it('should activate buttons with Space key', async () => { const user = userEvent.setup(); const handleClick = vi.fn(); render(); const button = screen.getByRole('button', { name: /test button/i }); button.focus(); expect(button).toHaveFocus(); // Press Space await user.keyboard(' '); expect(handleClick).toHaveBeenCalledTimes(1); }); it('should submit form with Enter key on submit button', async () => { const user = userEvent.setup(); const handleSubmit = vi.fn((e) => e.preventDefault()); render(
, ); const submitButton = screen.getByRole('button', { name: /submit/i }); submitButton.focus(); await user.keyboard('{Enter}'); expect(handleSubmit).toHaveBeenCalled(); }); it('should close dialog with Escape key', async () => { const user = userEvent.setup(); const handleClose = vi.fn(); const TestDialog = () => { const [open, setOpen] = React.useState(true); React.useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape') { setOpen(false); handleClose(); } }; document.addEventListener('keydown', handleEscape); return () => document.removeEventListener('keydown', handleEscape); }, []); if (!open) return null; return (

Test Dialog

Dialog content

); }; render(); const dialog = screen.getByRole('dialog'); expect(dialog).toBeInTheDocument(); // Press Escape await user.keyboard('{Escape}'); expect(handleClose).toHaveBeenCalled(); }); it('should have focusable elements within modal', async () => { render( , ); const dialog = screen.getByRole('dialog'); const firstButton = screen.getByRole('button', { name: /first button/i }); const secondButton = screen.getByRole('button', { name: /second button/i, }); const closeButton = screen.getByRole('button', { name: /close/i }); // Verify dialog and buttons are present expect(dialog).toBeInTheDocument(); expect(firstButton).toBeInTheDocument(); expect(secondButton).toBeInTheDocument(); expect(closeButton).toBeInTheDocument(); // Verify buttons are within dialog expect(dialog.contains(firstButton)).toBe(true); expect(dialog.contains(secondButton)).toBe(true); expect(dialog.contains(closeButton)).toBe(true); }); }); describe('Screen Reader Support', () => { it('should have proper ARIA labels on form inputs', () => { render(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/mot de passe/i); expect(emailInput).toHaveAttribute('type', 'email'); expect(passwordInput).toHaveAttribute('type', 'password'); }); it('should have proper ARIA labels on buttons', () => { render( , ); const button = screen.getByRole('button', { name: /play track/i }); expect(button).toBeInTheDocument(); expect(button).toHaveAttribute('aria-label', 'Play track'); }); it('should have proper ARIA roles on interactive elements', () => { render(
Link
, ); expect(screen.getByRole('button')).toBeInTheDocument(); expect(screen.getByRole('link')).toBeInTheDocument(); expect(screen.getByRole('textbox')).toBeInTheDocument(); expect(screen.getByRole('combobox')).toBeInTheDocument(); }); it('should have proper ARIA states on form fields', () => { render(
This field is required
, ); const requiredInput = screen.getByLabelText(/required field/i); const invalidInput = screen.getByLabelText(/invalid field/i); expect(requiredInput).toHaveAttribute('aria-required', 'true'); expect(invalidInput).toHaveAttribute('aria-invalid', 'true'); expect(invalidInput).toHaveAttribute('aria-describedby', 'error-message'); }); it('should have proper ARIA labels on dialogs', () => { render(

Dialog content

, ); const dialog = screen.getByRole('dialog'); expect(dialog).toHaveAttribute('aria-modal', 'true'); const title = screen.getByText(/test dialog/i); expect(title).toBeInTheDocument(); }); it('should have proper ARIA labels on navigation', () => { render( , ); const nav = screen.getByRole('navigation', { name: /main navigation/i }); expect(nav).toBeInTheDocument(); expect(nav).toHaveAttribute('aria-label', 'Main navigation'); }); it('should have proper ARIA labels on lists', () => { render(
  • Track 1
  • Track 2
  • Track 3
, ); const list = screen.getByRole('list', { name: /track list/i }); expect(list).toBeInTheDocument(); expect(list).toHaveAttribute('aria-label', 'Track list'); }); it('should hide decorative icons from screen readers', () => { render( , ); const icon = screen.getByText('▶'); expect(icon).toHaveAttribute('aria-hidden', 'true'); }); }); describe('Focus Management', () => { it('should show visible focus indicators', () => { render(); const button = screen.getByRole('button'); button.focus(); // Check that button has focus expect(button).toHaveFocus(); }); it('should move focus to first focusable element in modal', async () => { render( , ); // Wait for dialog to be fully rendered await new Promise((resolve) => setTimeout(resolve, 100)); // First focusable element should be focused const firstButton = screen.getByRole('button', { name: /first button/i }); // Note: Actual focus behavior depends on Dialog implementation expect(firstButton).toBeInTheDocument(); }); it('should restore focus after closing modal', async () => { const user = userEvent.setup(); const TestComponent = () => { const [open, setOpen] = React.useState(false); return (
{open && ( setOpen(false)}>

Dialog content

)}
); }; render(); const openButton = screen.getByRole('button', { name: /open dialog/i }); openButton.focus(); await user.click(openButton); const closeButton = screen.getByRole('button', { name: /close/i }); await user.click(closeButton); // Focus should be restored to open button // Note: Actual behavior depends on Dialog implementation expect(openButton).toBeInTheDocument(); }); }); describe('Skip Links', () => { it('should have skip to main content link', () => { render( , ); const skipLink = screen.getByRole('link', { name: /skip to main content/i, }); expect(skipLink).toBeInTheDocument(); expect(skipLink).toHaveAttribute('href', '#main-content'); }); }); describe('Form Accessibility', () => { it('should associate labels with form inputs', () => { render(
, ); const input = screen.getByLabelText(/email/i); expect(input).toHaveAttribute('id', 'email-input'); }); it('should show error messages with proper ARIA attributes', () => { render(
Email is required
, ); const input = screen.getByLabelText(/email/i); const errorMessage = screen.getByRole('alert'); expect(input).toHaveAttribute('aria-invalid', 'true'); expect(input).toHaveAttribute('aria-describedby', 'email-error'); expect(errorMessage).toHaveTextContent('Email is required'); }); it('should have proper form structure', () => { render(); // Check that form fields have labels const emailInput = screen.getByLabelText(/email/i); const usernameInput = screen.getByLabelText(/nom d'utilisateur/i); const passwordInput = screen.getByLabelText(/mot de passe/i); expect(emailInput).toBeInTheDocument(); expect(usernameInput).toBeInTheDocument(); expect(passwordInput).toBeInTheDocument(); }); }); });