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 (
-
-
+
+
{message}
{onRetry && (