diff --git a/src/App.test.tsx b/src/App.test.tsx index 7e5eedb..8104c76 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,9 +1,106 @@ -import { render, screen } from '@testing-library/react' +import { fireEvent, render, screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' import App from './App' +const renderApp = () => render() + describe('App', () => { - it('renders without crashing', () => { - render() - expect(screen.getByRole('main')).toBeInTheDocument() + beforeEach(() => { + localStorage.clear() + vi.restoreAllMocks() + }) + + it('renders the empty state and the floating button', () => { + renderApp() + + expect(screen.getByRole('heading', { name: 'Notes' })).toBeInTheDocument() + expect(screen.getByText(/aucune note/i)).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Créer une note' })).toBeInTheDocument() + }) + + it('lists note titles from storage in alphabetical order', () => { + localStorage.setItem( + 'notes', + JSON.stringify([ + { id: 1, title: 'Zebre', content: 'x' }, + { id: 2, title: 'Alpha', content: 'y' }, + ]), + ) + + renderApp() + + const items = screen.getAllByRole('button').filter((b) => b.textContent !== '+') + expect(items.map((button) => button.textContent)).toEqual(['Alpha', 'Zebre']) + }) + + it('creates a note with the + button and opens the editor in edit mode', () => { + vi.spyOn(Date, 'now').mockReturnValue(1234) + renderApp() + + fireEvent.click(screen.getByRole('button', { name: 'Créer une note' })) + + expect(screen.getByLabelText('Titre')).toHaveValue('') + expect(screen.getByLabelText('Contenu')).toHaveValue('') + expect(screen.getByRole('button', { name: 'Enregistrer' })).toBeInTheDocument() + expect(JSON.parse(localStorage.getItem('notes')!)).toEqual([ + { id: 1234, title: '', content: '' }, + ]) + }) + + it('saves a new note and displays it in read mode', () => { + renderApp() + + fireEvent.click(screen.getByRole('button', { name: 'Créer une note' })) + fireEvent.change(screen.getByLabelText('Titre'), { target: { value: 'Courses' } }) + fireEvent.change(screen.getByLabelText('Contenu'), { target: { value: 'Lait' } }) + fireEvent.click(screen.getByRole('button', { name: 'Enregistrer' })) + + expect(screen.getByText('Courses')).toBeInTheDocument() + expect(screen.getByText('Lait')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Éditer' })).toBeInTheDocument() + }) + + it('opens a note from the list and navigates back', () => { + localStorage.setItem( + 'notes', + JSON.stringify([{ id: 1, title: 'Rendez-vous', content: 'Jeudi 10h' }]), + ) + renderApp() + + fireEvent.click(screen.getByText('Rendez-vous')) + + expect(screen.getByText('Jeudi 10h')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Éditer' })).toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: 'Retour' })) + + expect(screen.getByText('Rendez-vous')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Créer une note' })).toBeInTheDocument() + }) + + it('deletes a note from the editor with confirmation', () => { + localStorage.setItem('notes', JSON.stringify([{ id: 1, title: 'À supprimer', content: 'x' }])) + renderApp() + + fireEvent.click(screen.getByText('À supprimer')) + fireEvent.click(screen.getByRole('button', { name: 'Supprimer la note' })) + fireEvent.click(screen.getByRole('button', { name: 'Supprimer' })) + + expect(screen.getByText(/aucune note/i)).toBeInTheDocument() + expect(JSON.parse(localStorage.getItem('notes')!)).toEqual([]) + }) + + it('persists an edited note to storage', () => { + localStorage.setItem('notes', JSON.stringify([{ id: 1, title: 'Avant', content: 'x' }])) + renderApp() + + fireEvent.click(screen.getByText('Avant')) + fireEvent.click(screen.getByRole('button', { name: 'Éditer' })) + fireEvent.change(screen.getByLabelText('Titre'), { target: { value: 'Après' } }) + fireEvent.click(screen.getByRole('button', { name: 'Enregistrer' })) + + expect(JSON.parse(localStorage.getItem('notes')!)).toEqual([ + { id: 1, title: 'Après', content: 'x' }, + ]) }) }) diff --git a/src/App.tsx b/src/App.tsx index 35e11ab..18c533c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,68 @@ +import { useState } from 'react' +import { useNotes } from './hooks/useNotes' +import NoteList from './components/NoteList' +import NoteEditor from './components/NoteEditor' +import FloatingButton from './components/FloatingButton' + function App() { - return
+ const { notes, addNote, updateNote, deleteNote } = useNotes() + const [selectedId, setSelectedId] = useState(null) + const [createdId, setCreatedId] = useState(null) + + const selectedNote = notes.find((note) => note.id === selectedId) ?? null + + const createNote = () => { + const note = addNote() + setSelectedId(note.id) + setCreatedId(note.id) + } + + const handleUpdate = (id: number, patch: Parameters[1]) => { + updateNote(id, patch) + if (id === createdId) setCreatedId(null) + } + + const handleDelete = (id: number) => { + deleteNote(id) + setSelectedId(null) + } + + return ( +
+
+ {selectedNote ? ( +
+ +

Notes

+
+ ) : ( +

Notes

+ )} +
+ +
+ {selectedNote ? ( + + ) : ( + + )} +
+ + +
+ ) } export default App diff --git a/src/components/FloatingButton.test.tsx b/src/components/FloatingButton.test.tsx new file mode 100644 index 0000000..9fb95c8 --- /dev/null +++ b/src/components/FloatingButton.test.tsx @@ -0,0 +1,26 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import FloatingButton from './FloatingButton' + +describe('FloatingButton', () => { + it('renders a + button', () => { + render() + + expect(screen.getByRole('button', { name: 'Créer une note' })).toHaveTextContent('+') + }) + + it('calls onClick when clicked', () => { + const onClick = vi.fn() + render() + + fireEvent.click(screen.getByRole('button', { name: 'Créer une note' })) + + expect(onClick).toHaveBeenCalled() + }) + + it('uses a custom label when provided', () => { + render() + + expect(screen.getByRole('button', { name: 'Ajouter' })).toBeInTheDocument() + }) +}) diff --git a/src/components/FloatingButton.tsx b/src/components/FloatingButton.tsx new file mode 100644 index 0000000..eb7151b --- /dev/null +++ b/src/components/FloatingButton.tsx @@ -0,0 +1,19 @@ +interface FloatingButtonProps { + onClick: () => void + label?: string +} + +function FloatingButton({ onClick, label = 'Créer une note' }: FloatingButtonProps) { + return ( + + ) +} + +export default FloatingButton diff --git a/src/components/NoteEditor.tsx b/src/components/NoteEditor.tsx index cb73a25..3d33e44 100644 --- a/src/components/NoteEditor.tsx +++ b/src/components/NoteEditor.tsx @@ -6,10 +6,11 @@ interface NoteEditorProps { note: Note onUpdate: (id: number, patch: Partial>) => void onDelete: (id: number) => void + autoEdit?: boolean } -function NoteEditor({ note, onUpdate, onDelete }: NoteEditorProps) { - const [editing, setEditing] = useState(false) +function NoteEditor({ note, onUpdate, onDelete, autoEdit = false }: NoteEditorProps) { + const [editing, setEditing] = useState(autoEdit) const [confirmDelete, setConfirmDelete] = useState(false) const [title, setTitle] = useState(note.title) const [content, setContent] = useState(note.content)