import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { DashboardGrid } from './DashboardGrid' import { DEFAULT_DASHBOARD_LAYOUT } from './dashboardLayout' beforeEach(() => { vi.stubGlobal('fetch', vi.fn((_input: RequestInfo | URL, init?: RequestInit) => { const submitted = init?.method === 'PUT' ? JSON.parse(String(init.body)) : null return Promise.resolve({ ok: true, status: 200, headers: new Headers({ ETag: submitted ? '"1"' : '"0"', 'X-Trainlog-CSRF-Token': 'a'.repeat(64) }), json: () => Promise.resolve({ format: 'trainlog-dashboard-layout', version: 1, revision: submitted ? 1 : 0, columns: 12, source: submitted ? 'persisted' : 'default', tiles: submitted?.tiles ?? DEFAULT_DASHBOARD_LAYOUT }), }) })) }) describe('grille interactive du Dashboard', () => { it('est verrouillée par défaut sans poignées de redimensionnement', async () => { const { container } = render() expect(await screen.findByRole('button', { name: 'Modifier l’agencement' })).toBeInTheDocument() expect(container.querySelector('.dashboard-layout')).not.toHaveClass('is-editing') expect(container.querySelectorAll('.react-resizable-handle')).toHaveLength(0) expect(screen.queryByRole('article', { name: /tuile modifiable/i })).not.toBeInTheDocument() }) it('active drag, resize et les commandes transactionnelles en mode édition', async () => { const { container } = render() fireEvent.click(await screen.findByRole('button', { name: 'Modifier l’agencement' })) expect(container.querySelector('.dashboard-layout')).toHaveClass('is-editing') expect(screen.getByRole('button', { name: 'Annuler' })).toBeInTheDocument() expect(screen.getByRole('button', { name: 'Réinitialiser' })).toBeInTheDocument() expect(screen.getByRole('button', { name: 'Enregistrer' })).toBeInTheDocument() expect(container.querySelectorAll('.react-resizable-handle').length).toBeGreaterThan(0) expect(screen.getByRole('article', { name: 'Progression, tuile modifiable' })).toHaveAttribute('tabindex', '0') }) it('déplace et redimensionne une tuile au clavier avec annonce accessible', async () => { render() fireEvent.click(await screen.findByRole('button', { name: 'Modifier l’agencement' })) const tile = screen.getByRole('article', { name: 'Progression, tuile modifiable' }) tile.focus() expect(tile).toHaveFocus() fireEvent.keyDown(tile, { key: 'ArrowDown' }) expect(screen.getByText(/Progression : colonne \d+, ligne \d+, largeur \d+, hauteur \d+\./)).toBeInTheDocument() fireEvent.keyDown(tile, { key: 'ArrowRight', shiftKey: true }) expect(screen.getByText(/Progression :.*largeur 9/)).toBeInTheDocument() }) it('Annuler restaure le layout d’entrée et Enregistrer persiste l’état React', async () => { const { container } = render() const progression = () => container.querySelector('[data-testid="grid-item-progression"]') as HTMLElement fireEvent.click(await screen.findByRole('button', { name: 'Modifier l’agencement' })) const initial = progression().style.width fireEvent.keyDown(screen.getByRole('article', { name: 'Progression, tuile modifiable' }), { key: 'ArrowRight', shiftKey: true }) expect(progression().style.width).not.toBe(initial) fireEvent.click(screen.getByRole('button', { name: 'Annuler' })) expect(progression().style.width).toBe(initial) fireEvent.click(screen.getByRole('button', { name: 'Modifier l’agencement' })) fireEvent.keyDown(screen.getByRole('article', { name: 'Progression, tuile modifiable' }), { key: 'ArrowRight', shiftKey: true }) const edited = progression().style.width fireEvent.click(screen.getByRole('button', { name: 'Enregistrer' })) await waitFor(() => expect(screen.getByRole('button', { name: 'Modifier l’agencement' })).toBeInTheDocument()) expect(progression().style.width).toBe(edited) }) it('Réinitialiser restaure le défaut dans le brouillon', async () => { const { container } = render() fireEvent.click(await screen.findByRole('button', { name: 'Modifier l’agencement' })) const tile = screen.getByRole('article', { name: 'Progression, tuile modifiable' }) fireEvent.keyDown(tile, { key: 'ArrowRight', shiftKey: true }) const modified = (container.querySelector('[data-testid="grid-item-progression"]') as HTMLElement).style.width fireEvent.click(screen.getByRole('button', { name: 'Réinitialiser' })) expect((container.querySelector('[data-testid="grid-item-progression"]') as HTMLElement).style.width).not.toBe(modified) }) it('conserve le brouillon et propose un reload lors d’un conflit 412', async () => { const fetchMock = vi.fn((_input: RequestInfo | URL, init?: RequestInit) => { if (init?.method === 'PUT') return Promise.resolve({ ok: false, status: 412, headers: new Headers(), json: () => Promise.resolve({ error: 'revision_conflict' }) }) return Promise.resolve({ ok: true, status: 200, headers: new Headers({ ETag: '"0"', 'X-Trainlog-CSRF-Token': 'a'.repeat(64) }), json: () => Promise.resolve({ format: 'trainlog-dashboard-layout', version: 1, revision: 0, columns: 12, source: 'default', tiles: DEFAULT_DASHBOARD_LAYOUT }) }) }) vi.stubGlobal('fetch', fetchMock) render() fireEvent.click(await screen.findByRole('button', { name: 'Modifier l’agencement' })) fireEvent.click(screen.getByRole('button', { name: 'Enregistrer' })) expect(await screen.findByRole('alert')).toHaveTextContent('modifié dans un autre onglet') expect(screen.getByRole('button', { name: 'Recharger l’agencement' })).toBeInTheDocument() expect(screen.getByRole('button', { name: 'Annuler' })).toBeInTheDocument() }) it('ne sauvegarde jamais depuis la projection téléphone', async () => { window.innerWidth = 390 const fetchMock = vi.mocked(fetch) render() fireEvent.click(await screen.findByRole('button', { name: 'Modifier l’agencement' })) expect(screen.getByRole('button', { name: 'Enregistrer' })).toBeDisabled() expect(screen.getByText(/écran large/)).toBeInTheDocument() expect(fetchMock).toHaveBeenCalledTimes(1) window.innerWidth = 1440 }) })