feat(notes): add note deletion with confirmation
This commit is contained in:
1 parent
0976c51cc1
commit
0af0cdc405
4 files changed
+141
-3
No files matched your search
@@ -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(<DeleteModal open={false} title="Courses" onConfirm={vi.fn()} onCancel={vi.fn()} />)
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the dialog with the note title when open', () => {
|
||||
render(<DeleteModal open title="Courses" onConfirm={vi.fn()} onCancel={vi.fn()} />)
|
||||
|
||||
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(<DeleteModal open title="" onConfirm={vi.fn()} onCancel={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText(/Sans titre/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onConfirm when the confirm button is clicked', () => {
|
||||
const onConfirm = vi.fn()
|
||||
render(<DeleteModal open title="Courses" onConfirm={onConfirm} onCancel={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Supprimer' }))
|
||||
|
||||
expect(onConfirm).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('calls onCancel when the cancel button is clicked', () => {
|
||||
const onCancel = vi.fn()
|
||||
render(<DeleteModal open title="Courses" onConfirm={vi.fn()} onCancel={onCancel} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Annuler' }))
|
||||
|
||||
expect(onCancel).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -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 (
|
||||
<div className="fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="delete-confirm-title"
|
||||
className="w-full max-w-sm bg-white rounded-lg p-5 flex flex-col gap-4"
|
||||
>
|
||||
<h2 id="delete-confirm-title" className="text-lg font-semibold">
|
||||
Supprimer la note ?
|
||||
</h2>
|
||||
<p className="text-gray-600">{title || 'Sans titre'} sera définitivement supprimée.</p>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 rounded border border-gray-300 text-gray-600"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
className="px-4 py-2 rounded bg-red-600 text-white"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeleteModal
|
||||
@@ -7,8 +7,9 @@ const note: Note = { id: 42, title: 'Courses', content: 'Lait\nOeufs' }
|
||||
|
||||
const renderEditor = (overrides: Partial<Note> = {}) => {
|
||||
const onUpdate = vi.fn()
|
||||
render(<NoteEditor note={{ ...note, ...overrides }} onUpdate={onUpdate} />)
|
||||
return { onUpdate }
|
||||
const onDelete = vi.fn()
|
||||
render(<NoteEditor note={{ ...note, ...overrides }} onUpdate={onUpdate} onDelete={onDelete} />)
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -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<Pick<Note, 'title' | 'content'>>) => 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 (
|
||||
<article className="flex flex-col gap-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<h2 className="text-2xl font-semibold">{note.title || 'Sans titre'}</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConfirmDelete(true)}
|
||||
aria-label="Supprimer la note"
|
||||
className="px-2 py-1 rounded text-gray-600 hover:text-red-600"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap">
|
||||
{note.content || <span className="text-gray-400 italic">Note vide</span>}
|
||||
</p>
|
||||
@@ -84,6 +97,15 @@ function NoteEditor({ note, onUpdate }: NoteEditorProps) {
|
||||
Éditer
|
||||
</button>
|
||||
</div>
|
||||
<DeleteModal
|
||||
open={confirmDelete}
|
||||
title={note.title}
|
||||
onConfirm={() => {
|
||||
setConfirmDelete(false)
|
||||
onDelete(note.id)
|
||||
}}
|
||||
onCancel={() => setConfirmDelete(false)}
|
||||
/>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user