73 lines
3.4 KiB
TypeScript
73 lines
3.4 KiB
TypeScript
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))
|
|
})
|
|
})
|