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)) }) })