import { screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { dune, renderApp, signedIn } from "../test/helpers"; const fresh = { ...dune, current_page: 0, status: "not_started" as const }; describe("BookForm", () => { it("adds a book and opens it", async () => { const calls = signedIn({ "POST /api/books": () => [201, fresh], "GET /api/books/7": () => [200, fresh], }); const user = renderApp("/books/new"); await screen.findByRole("option", { name: "Science Fiction" }); await user.type(screen.getByLabelText("Title"), "Dune"); await user.type(screen.getByLabelText("Author"), "Frank Herbert"); await user.selectOptions(screen.getByLabelText("Genre"), "Science Fiction"); await user.type(screen.getByLabelText("Total pages"), "380"); await user.click(screen.getByRole("button", { name: "Add book" })); expect( await screen.findByRole("heading", { name: "Dune", level: 1 }), ).toBeInTheDocument(); expect(calls).toContainEqual({ method: "POST", path: "/api/books", body: { title: "Dune", author: "Frank Herbert", genre_id: 5, total_pages: 380, current_page: 0, }, }); }); it("shows a server validation error against the field", async () => { signedIn({ "POST /api/books": () => [ 400, { error: "Unknown genre_id; see GET /api/genres", field: "genre_id" }, ], }); const user = renderApp("/books/new"); await screen.findByRole("option", { name: "Science Fiction" }); await user.type(screen.getByLabelText("Title"), "Dune"); await user.type(screen.getByLabelText("Author"), "Frank Herbert"); await user.selectOptions(screen.getByLabelText("Genre"), "Science Fiction"); await user.type(screen.getByLabelText("Total pages"), "380"); await user.click(screen.getByRole("button", { name: "Add book" })); expect(await screen.findByRole("alert")).toHaveTextContent( "Unknown genre_id", ); expect(screen.getByLabelText("Genre")).toHaveAttribute( "aria-invalid", "true", ); }); it("edits an existing book", async () => { let saved = dune; const calls = signedIn({ "GET /api/books/7": () => [200, saved], "PATCH /api/books/7": ({ body }) => { saved = { ...saved, ...(body as object) }; return [200, saved]; }, }); const user = renderApp("/books/7/edit"); const title = await screen.findByDisplayValue("Dune"); await user.clear(title); await user.type(title, "Dune Messiah"); await user.click(screen.getByRole("button", { name: "Save changes" })); expect( await screen.findByRole("heading", { name: "Dune Messiah", level: 1 }), ).toBeInTheDocument(); expect(calls).toContainEqual({ method: "PATCH", path: "/api/books/7", body: { title: "Dune Messiah" }, }); }); it("retries a failed load and shows the prefilled form", async () => { let attempts = 0; signedIn({ "GET /api/books/7": () => ++attempts === 1 ? [500, { error: "Database unavailable" }] : [200, dune], }); const user = renderApp("/books/7/edit"); expect(await screen.findByRole("alert")).toHaveTextContent( "Database unavailable", ); await user.click(screen.getByRole("button", { name: "Retry" })); expect(await screen.findByLabelText("Title")).toHaveValue("Dune"); }); it("saving without changes returns to the book without a request", async () => { const calls = signedIn({ "GET /api/books/7": () => [200, dune], "GET /api/books/7/notes": () => [200, []], }); const user = renderApp("/books/7/edit"); await screen.findByDisplayValue("Dune"); await user.click(screen.getByRole("button", { name: "Save changes" })); await screen.findByRole("heading", { name: "Dune", level: 1 }); expect(calls.some((call) => call.method === "PATCH")).toBe(false); }); it("preserves progress saved in another session when only the title changes", async () => { let saved = { ...dune }; signedIn({ "GET /api/books/7": () => [200, { ...saved }], "GET /api/books/7/notes": () => [200, []], "PATCH /api/books/7": ({ body }) => { saved = { ...saved, ...(body as object) }; return [200, saved]; }, }); const user = renderApp("/books/7/edit"); const title = await screen.findByDisplayValue("Dune"); await screen.findByRole("option", { name: "Science Fiction" }); // Another session saves progress after this form loads its snapshot. saved = { ...saved, current_page: 300 }; await user.clear(title); await user.type(title, "Dune Messiah"); await user.click(screen.getByRole("button", { name: "Save changes" })); await screen.findByRole("heading", { name: "Dune Messiah", level: 1 }); expect(screen.getByRole("progressbar")).toHaveAttribute("value", "300"); }); });