Files
Clarium/src/books/BookForm.test.tsx
mroberts 7f5d034a1f feat: auth by default, theme toggle, CoderPad startup, review fixes
- Require a session on every route; public routes opt out with @allow_anonymous
- Split password hashing and pepper loading into passwords.py
- Add a system/light/dark theme toggle backed by light-dark() colors
- Ignore stale 401s from an earlier session, PATCH only changed book fields,
  and block overlapping journal-entry saves
- Add bin/start and CoderPad Vite server settings for the pad's start/restart
- Rewrite README as a mise onboarding guide; expand .gitignore
- Include review-round fixes and tests
2026-10-02 16:49:14 -05:00

139 lines
4.9 KiB
TypeScript

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