feat: add book detail with progress updates and delete
Claude-Session: https://claude.ai/code/session_01M9MLit5Ko3X4s7rzC5Kv7X
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { Book } from '../api'
|
||||
import { dune, renderApp, signedIn } from '../test/helpers'
|
||||
|
||||
function withProgress(current_page: number): Book {
|
||||
const status = current_page === 0 ? 'not_started' : current_page === dune.total_pages ? 'finished' : 'reading'
|
||||
return { ...dune, current_page, status }
|
||||
}
|
||||
|
||||
describe('BookDetail', () => {
|
||||
it('updates progress and reflects it in the bar', async () => {
|
||||
const calls = signedIn({
|
||||
'GET /api/books/7': () => [200, dune],
|
||||
'PATCH /api/books/7': ({ body }) => [200, withProgress((body as { current_page: number }).current_page)],
|
||||
})
|
||||
const user = renderApp('/books/7')
|
||||
expect(await screen.findByText('142 / 380 (37%)')).toBeInTheDocument()
|
||||
|
||||
const input = screen.getByLabelText('Current page')
|
||||
await user.clear(input)
|
||||
await user.type(input, '200')
|
||||
await user.click(screen.getByRole('button', { name: 'Save progress' }))
|
||||
|
||||
expect(await screen.findByText('200 / 380 (53%)')).toBeInTheDocument()
|
||||
expect(calls).toContainEqual({ method: 'PATCH', path: '/api/books/7', body: { current_page: 200 } })
|
||||
})
|
||||
|
||||
it('marks a book finished', async () => {
|
||||
signedIn({
|
||||
'GET /api/books/7': () => [200, dune],
|
||||
'PATCH /api/books/7': () => [200, withProgress(380)],
|
||||
})
|
||||
const user = renderApp('/books/7')
|
||||
await user.click(await screen.findByRole('button', { name: 'Mark finished' }))
|
||||
expect(await screen.findByText('380 / 380 (100%)')).toBeInTheDocument()
|
||||
expect(screen.getByText('Finished')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows the server message when a progress update is rejected', async () => {
|
||||
signedIn({
|
||||
'GET /api/books/7': () => [200, dune],
|
||||
'PATCH /api/books/7': () => [400, { error: 'current_page must be between 0 and 100000', field: 'current_page' }],
|
||||
})
|
||||
const user = renderApp('/books/7')
|
||||
await user.click(await screen.findByRole('button', { name: 'Save progress' }))
|
||||
// findByText, not findByRole('alert'): once Task 11 mounts the journal, the page may show more than one alert.
|
||||
expect(await screen.findByText('current_page must be between 0 and 100000')).toHaveAttribute('role', 'alert')
|
||||
expect(screen.getByLabelText('Current page')).toHaveAttribute('aria-invalid', 'true')
|
||||
})
|
||||
|
||||
it('shows not found for a missing or foreign book', async () => {
|
||||
signedIn({ 'GET /api/books/999': () => [404, { error: 'Book not found' }] })
|
||||
renderApp('/books/999')
|
||||
expect(await screen.findByRole('heading', { name: 'Book not found' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /back to library/i })).toHaveAttribute('href', '/books')
|
||||
})
|
||||
|
||||
it('deletes after confirmation and returns to the list', async () => {
|
||||
const calls = signedIn({
|
||||
'GET /api/books/7': () => [200, dune],
|
||||
'DELETE /api/books/7': () => [204],
|
||||
'GET /api/books': () => [200, []],
|
||||
})
|
||||
const user = renderApp('/books/7')
|
||||
await user.click(await screen.findByRole('button', { name: 'Delete book' }))
|
||||
expect(screen.getByRole('dialog')).toHaveTextContent('Delete “Dune”?')
|
||||
await user.click(screen.getByRole('button', { name: 'Yes, delete' }))
|
||||
expect(await screen.findByText('No books yet. Add your first one.')).toBeInTheDocument()
|
||||
await waitFor(() => expect(calls.some((c) => c.method === 'DELETE' && c.path === '/api/books/7')).toBe(true))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user