Limit request bodies to 1 MiB (413 JSON), wire aria-describedby on the login form, add Retry and cancellation to the book form load, disable Add entry while a note POST is in flight, and poll pg_isready in README. Claude-Session: https://claude.ai/code/session_01M9MLit5Ko3X4s7rzC5Kv7X
99 lines
4.1 KiB
TypeScript
99 lines
4.1 KiB
TypeScript
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)
|
|
})
|
|
})
|