From 208fe98f1b1aac002e7671edbb7bdf579d3a766d Mon Sep 17 00:00:00 2001 From: gilles Date: Mon, 21 Sep 2026 19:13:27 +0200 Subject: [PATCH] feat(ui): suppression d'une carte avec confirmation (P3-6, F-07) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CardDeleteButton ouvre un dialog modal (aria-modal, aria-labelledby) qui résume la carte à supprimer. Confirmation : cardsApi.remove puis retour à la liste /cards. Annuler referme sans REMOVE. Test F-07 couvre l'ouverture, l'annulation (remove non appelé) et la confirmation (remove + navigation). --- src/components/card-delete.test.tsx | 59 ++++++++++++++++++ src/components/card-delete.tsx | 94 +++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 src/components/card-delete.test.tsx create mode 100644 src/components/card-delete.tsx 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} +

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