diff --git a/DataSentinel/src/__tests__/accessibilite.test.jsx b/DataSentinel/src/__tests__/accessibilite.test.jsx new file mode 100644 index 0000000..3de26f7 --- /dev/null +++ b/DataSentinel/src/__tests__/accessibilite.test.jsx @@ -0,0 +1,89 @@ +/* Accessibilité des composants transverses. + + Le dossier affirme que les boîtes de dialogue piègent le focus et que les + messages dynamiques sont annoncés aux lecteurs d'écran. Ces tests rendent ces + affirmations vérifiables : elles étaient auparavant invalidables autrement + qu'en relisant le code. */ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import Modal from '../components/common/Modal'; +import ErrorMessage from '../components/common/ErrorMessage'; +import LoadingSpinner from '../components/common/LoadingSpinner'; + +function ouvrirModale(onClose = () => {}) { + return render( + + + + , + ); +} + +describe('Modal — accessibilité', () => { + it('est exposée comme boîte de dialogue modale', () => { + ouvrirModale(); + const dialogue = screen.getByRole('dialog'); + expect(dialogue).toHaveAttribute('aria-modal', 'true'); + }); + + it('est nommée par son titre', () => { + ouvrirModale(); + /* Le lecteur d'écran doit annoncer de quoi parle la modale. */ + expect(screen.getByRole('dialog', { name: 'Modifier le service' })).toBeInTheDocument(); + }); + + it('place le focus sur le premier champ à l’ouverture', () => { + ouvrirModale(); + expect(screen.getByLabelText('Nom')).toHaveFocus(); + }); + + it('referme la modale à la touche Échap', async () => { + const onClose = vi.fn(); + const utilisateur = userEvent.setup(); + ouvrirModale(onClose); + + await utilisateur.keyboard('{Escape}'); + expect(onClose).toHaveBeenCalled(); + }); + + it('retient le focus à l’intérieur de la modale', async () => { + const utilisateur = userEvent.setup(); + ouvrirModale(); + + /* Depuis le dernier élément, Tab doit revenir au premier et non filer + vers la page située derrière. */ + const fermer = screen.getByRole('button', { name: 'Fermer' }); + fermer.focus(); + await utilisateur.tab(); + + expect(document.activeElement).not.toBe(document.body); + expect(screen.getByRole('dialog').contains(document.activeElement)).toBe(true); + }); + + it('n’affiche rien quand elle est fermée', () => { + render( {}} title="Titre">contenu); + expect(screen.queryByRole('dialog')).toBeNull(); + }); +}); + +describe('Messages dynamiques — annonce aux lecteurs d’écran', () => { + it('annonce une erreur immédiatement', () => { + render(); + const alerte = screen.getByRole('alert'); + expect(alerte).toHaveAttribute('aria-live', 'assertive'); + expect(alerte).toHaveTextContent('service indisponible'); + }); + + it('annonce le chargement sans interrompre la lecture', () => { + render(); + const statut = screen.getByRole('status'); + expect(statut).toHaveAttribute('aria-live', 'polite'); + }); + + it('masque les éléments purement décoratifs', () => { + /* L'animation du spinner n'apporte rien à l'oral : elle doit être ignorée. */ + const { container } = render(); + expect(container.querySelector('[aria-hidden="true"]')).toBeInTheDocument(); + }); +}); diff --git a/DataSentinel/src/components/common/ErrorMessage.jsx b/DataSentinel/src/components/common/ErrorMessage.jsx index f14cbad..4d3dcdb 100644 --- a/DataSentinel/src/components/common/ErrorMessage.jsx +++ b/DataSentinel/src/components/common/ErrorMessage.jsx @@ -4,9 +4,12 @@ import { AlertCircle } from 'lucide-react'; import styles from './ErrorMessage.module.css'; export default function ErrorMessage({ message = 'Une erreur est survenue.', onRetry }) { + /* role="alert" annonce l'erreur immédiatement au lecteur d'écran : elle + apparaît après le chargement de la page, sans quoi elle passerait + inaperçue pour un utilisateur non voyant. */ return ( -
- +
+