import { fireEvent, screen, within } from '@testing-library/react' import { describe, expect, it } from 'vitest' import type { Note } from '../api' import { dune, renderApp, signedIn } from '../test/helpers' const older: Note = { id: 11, book_id: 7, body: 'Started reading', created_at: '2026-10-01T10:00:00+00:00', updated_at: '2026-10-01T10:00:00+00:00', } describe('NotesJournal', () => { it('lists entries and adds a new one at the top', async () => { const calls = signedIn({ 'GET /api/books/7': () => [200, dune], 'GET /api/books/7/notes': () => [200, [older]], 'POST /api/books/7/notes': ({ body }) => [ 201, { ...older, id: 12, body: (body as { body: string }).body, created_at: '2026-10-02T10:00:00+00:00', updated_at: '2026-10-02T10:00:00+00:00' }, ], }) const user = renderApp('/books/7') expect(await screen.findByText('Started reading')).toBeInTheDocument() await user.type(screen.getByLabelText('New journal entry'), 'Loved chapter 3') await user.click(screen.getByRole('button', { name: 'Add entry' })) await screen.findByText('Loved chapter 3') const items = screen.getAllByRole('listitem') expect(items[0]).toHaveTextContent('Loved chapter 3') expect(items[1]).toHaveTextContent('Started reading') expect(screen.getByLabelText('New journal entry')).toHaveValue('') expect(calls).toContainEqual({ method: 'POST', path: '/api/books/7/notes', body: { body: 'Loved chapter 3' } }) }) it('edits an entry and marks it edited', async () => { signedIn({ 'GET /api/books/7': () => [200, dune], 'GET /api/books/7/notes': () => [200, [older]], 'PATCH /api/notes/11': ({ body }) => [ 200, { ...older, body: (body as { body: string }).body, updated_at: '2026-10-02T10:00:00+00:00' }, ], }) const user = renderApp('/books/7') const item = (await screen.findByText('Started reading')).closest('li')! await user.click(within(item).getByRole('button', { name: 'Edit entry' })) const editor = within(item).getByLabelText('Edit entry') await user.clear(editor) await user.type(editor, 'Finished part one') await user.click(within(item).getByRole('button', { name: 'Save' })) expect(await within(item).findByText('Finished part one')).toBeInTheDocument() expect(within(item).getByText(/edited/)).toBeInTheDocument() }) it('deletes an entry', async () => { signedIn({ 'GET /api/books/7': () => [200, dune], 'GET /api/books/7/notes': () => [200, [older]], 'DELETE /api/notes/11': () => [204], }) const user = renderApp('/books/7') await user.click(await screen.findByRole('button', { name: 'Delete entry' })) expect(await screen.findByText('No entries yet.')).toBeInTheDocument() }) it('shows the server error when adding fails', async () => { signedIn({ 'GET /api/books/7': () => [200, dune], 'GET /api/books/7/notes': () => [200, []], 'POST /api/books/7/notes': () => [400, { error: 'body must be at most 10000 characters', field: 'body' }], }) const user = renderApp('/books/7') await user.type(await screen.findByLabelText('New journal entry'), 'x') await user.click(screen.getByRole('button', { name: 'Add entry' })) expect(await screen.findByRole('alert')).toHaveTextContent('body must be at most 10000 characters') }) it('posts a double-clicked entry only once', async () => { const calls = signedIn({ 'GET /api/books/7': () => [200, dune], 'GET /api/books/7/notes': () => [200, []], 'POST /api/books/7/notes': ({ body }) => [201, { ...older, id: 12, body: (body as { body: string }).body }], }) const user = renderApp('/books/7') await screen.findByText('No entries yet.') await user.type(screen.getByLabelText('New journal entry'), 'Once only') const add = screen.getByRole('button', { name: 'Add entry' }) // Two clicks before the pending POST settles; user.dblClick would wait for it and mask the bug. fireEvent.click(add) fireEvent.click(add) await screen.findByText('Once only') expect(calls.filter((c) => c.method === 'POST')).toHaveLength(1) }) })