trainlog/web/src/dashboard/DashboardGrid.test.tsx
2026-09-16 14:19:23 +02:00

107 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(<DashboardGrid />)
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(<DashboardGrid />)
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(<DashboardGrid />)
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(<DashboardGrid />)
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(<DashboardGrid />)
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(<DashboardGrid />)
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(<DashboardGrid />)
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
})
})