From 0af0cdc40568e5b39e88845df5599482e4abe6d1 Mon Sep 17 00:00:00 2001 From: gilles Date: Sun, 13 Sep 2026 15:32:33 +0200 Subject: [PATCH] feat(notes): add note deletion with confirmation --- src/components/DeleteModal.test.tsx | 42 +++++++++++++++++++++++++++ src/components/DeleteModal.tsx | 44 +++++++++++++++++++++++++++++ src/components/NoteEditor.test.tsx | 34 ++++++++++++++++++++-- src/components/NoteEditor.tsx | 26 +++++++++++++++-- 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 src/components/DeleteModal.test.tsx create mode 100644 src/components/DeleteModal.tsx diff --git a/src/components/DeleteModal.test.tsx b/src/components/DeleteModal.test.tsx new file mode 100644 index 0000000..24fda81 --- /dev/null +++ b/src/components/DeleteModal.test.tsx @@ -0,0 +1,42 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import DeleteModal from './DeleteModal' + +describe('DeleteModal', () => { + it('renders nothing when closed', () => { + render() + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + }) + + it('renders the dialog with the note title when open', () => { + render() + + expect(screen.getByRole('dialog', { name: 'Supprimer la note ?' })).toBeInTheDocument() + expect(screen.getByText(/Courses/)).toBeInTheDocument() + }) + + it('shows a placeholder when the note has no title', () => { + render() + + expect(screen.getByText(/Sans titre/)).toBeInTheDocument() + }) + + it('calls onConfirm when the confirm button is clicked', () => { + const onConfirm = vi.fn() + render() + + fireEvent.click(screen.getByRole('button', { name: 'Supprimer' })) + + expect(onConfirm).toHaveBeenCalled() + }) + + it('calls onCancel when the cancel button is clicked', () => { + const onCancel = vi.fn() + render() + + fireEvent.click(screen.getByRole('button', { name: 'Annuler' })) + + expect(onCancel).toHaveBeenCalled() + }) +}) diff --git a/src/components/DeleteModal.tsx b/src/components/DeleteModal.tsx new file mode 100644 index 0000000..d44c702 --- /dev/null +++ b/src/components/DeleteModal.tsx @@ -0,0 +1,44 @@ +interface DeleteModalProps { + open: boolean + title: string + onConfirm: () => void + onCancel: () => void +} + +function DeleteModal({ open, title, onConfirm, onCancel }: DeleteModalProps) { + if (!open) return null + + return ( +
+
+

+ Supprimer la note ? +

+

{title || 'Sans titre'} sera définitivement supprimée.

+
+ + +
+
+
+ ) +} + +export default DeleteModal diff --git a/src/components/NoteEditor.test.tsx b/src/components/NoteEditor.test.tsx index 58b44e9..22edaca 100644 --- a/src/components/NoteEditor.test.tsx +++ b/src/components/NoteEditor.test.tsx @@ -7,8 +7,9 @@ const note: Note = { id: 42, title: 'Courses', content: 'Lait\nOeufs' } const renderEditor = (overrides: Partial = {}) => { const onUpdate = vi.fn() - render() - return { onUpdate } + const onDelete = vi.fn() + render() + return { onUpdate, onDelete } } describe('NoteEditor', () => { @@ -70,4 +71,33 @@ describe('NoteEditor', () => { expect(onUpdate).not.toHaveBeenCalled() expect(screen.getByText('Courses')).toBeInTheDocument() }) + + it('opens the delete confirmation when the × button is clicked', () => { + renderEditor() + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: 'Supprimer la note' })) + + expect(screen.getByRole('dialog', { name: 'Supprimer la note ?' })).toBeInTheDocument() + }) + + it('deletes the note after confirmation', () => { + const { onDelete } = renderEditor() + + fireEvent.click(screen.getByRole('button', { name: 'Supprimer la note' })) + fireEvent.click(screen.getByRole('button', { name: 'Supprimer' })) + + expect(onDelete).toHaveBeenCalledWith(42) + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + }) + + it('does not delete when the confirmation is cancelled', () => { + const { onDelete } = renderEditor() + + fireEvent.click(screen.getByRole('button', { name: 'Supprimer la note' })) + fireEvent.click(screen.getByRole('button', { name: 'Annuler' })) + + expect(onDelete).not.toHaveBeenCalled() + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + }) }) diff --git a/src/components/NoteEditor.tsx b/src/components/NoteEditor.tsx index 9769b50..cb73a25 100644 --- a/src/components/NoteEditor.tsx +++ b/src/components/NoteEditor.tsx @@ -1,13 +1,16 @@ import { useState } from 'react' import type { Note } from '../types' +import DeleteModal from './DeleteModal' interface NoteEditorProps { note: Note onUpdate: (id: number, patch: Partial>) => void + onDelete: (id: number) => void } -function NoteEditor({ note, onUpdate }: NoteEditorProps) { +function NoteEditor({ note, onUpdate, onDelete }: NoteEditorProps) { const [editing, setEditing] = useState(false) + const [confirmDelete, setConfirmDelete] = useState(false) const [title, setTitle] = useState(note.title) const [content, setContent] = useState(note.content) @@ -71,7 +74,17 @@ function NoteEditor({ note, onUpdate }: NoteEditorProps) { return (
-

{note.title || 'Sans titre'}

+
+

{note.title || 'Sans titre'}

+ +

{note.content || Note vide}

@@ -84,6 +97,15 @@ function NoteEditor({ note, onUpdate }: NoteEditorProps) { Éditer + { + setConfirmDelete(false) + onDelete(note.id) + }} + onCancel={() => setConfirmDelete(false)} + />
) }