diff --git a/src/components/card-delete.test.tsx b/src/components/card-delete.test.tsx new file mode 100644 index 0000000..3893b4c --- /dev/null +++ b/src/components/card-delete.test.tsx @@ -0,0 +1,59 @@ +import { render, screen, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { CardDeleteButton } from './card-delete' + +vi.mock('@/lib/api-client', () => { + class ApiClientErrorMock extends Error { + status: number + constructor(status: number, name: string, message: string) { + super(message) + this.name = name + this.status = status + } + } + return { + ApiClientError: ApiClientErrorMock, + cardsApi: { remove: vi.fn() }, + } +}) + +const routerMock = { push: vi.fn(), refresh: vi.fn() } +vi.mock('next/navigation', () => ({ + useRouter: () => routerMock, +})) + +import { cardsApi } from '@/lib/api-client' + +beforeEach(() => { + vi.mocked(cardsApi.remove).mockReset() + routerMock.push.mockReset() + routerMock.refresh.mockReset() +}) + +it('ouvre une confirmation avant de supprimer (F-07)', async () => { + render() + await userEvent.click(screen.getByRole('button', { name: 'Supprimer' })) + expect(screen.getByRole('dialog')).toBeInTheDocument() + expect(screen.getByText(/Cest quoi un commit \?/)).toBeInTheDocument() + expect(cardsApi.remove).not.toHaveBeenCalled() +}) + +it('annule sans supprimer ni naviguer', async () => { + render() + await userEvent.click(screen.getByRole('button', { name: 'Supprimer' })) + await userEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: 'Annuler' })) + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + expect(cardsApi.remove).not.toHaveBeenCalled() + expect(routerMock.push).not.toHaveBeenCalled() +}) + +it('supprime après confirmation puis retourne à la liste (F-07)', async () => { + vi.mocked(cardsApi.remove).mockResolvedValue(undefined as never) + render() + await userEvent.click(screen.getByRole('button', { name: 'Supprimer' })) + await userEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: 'Supprimer' })) + expect(cardsApi.remove).toHaveBeenCalledWith('c1') + expect(routerMock.push).toHaveBeenCalledWith('/cards') + expect(routerMock.refresh).toHaveBeenCalled() +}) diff --git a/src/components/card-delete.tsx b/src/components/card-delete.tsx new file mode 100644 index 0000000..093c855 --- /dev/null +++ b/src/components/card-delete.tsx @@ -0,0 +1,94 @@ +'use client' + +import { useRouter } from 'next/navigation' +import { useState } from 'react' +import { ApiClientError, cardsApi } from '@/lib/api-client' + +export function CardDeleteButton({ cardId, question }: { cardId: string; question: string }) { + const router = useRouter() + const [confirming, setConfirming] = useState(false) + const [error, setError] = useState(null) + const [deleting, setDeleting] = useState(false) + + function open() { + setConfirming(true) + setError(null) + } + function close() { + if (!deleting) { + setConfirming(false) + setError(null) + } + } + + async function confirmDelete() { + setDeleting(true) + setError(null) + try { + await cardsApi.remove(cardId) + router.push('/cards') + router.refresh() + } catch (e) { + if (e instanceof ApiClientError) setError(e.message) + else setError('Impossible de supprimer la carte') + setDeleting(false) + } + } + + return ( + <> + + + {confirming && ( +
+
e.stopPropagation()} + > +

+ Supprimer cette carte ? +

+

+ « {question} » sera définitivement supprimée. Cette action est irréversible. +

+ {error && ( +

+ {error} +

+ )} +
+ + +
+
+
+ )} + + ) +}