From b32dc9494b781ed47cf4f3d0db159349c871295f Mon Sep 17 00:00:00 2001 From: Erik Hjelm Fjeldheim Date: Sun, 6 Sep 2026 18:50:58 -0600 Subject: [PATCH] feat: add useBooks hook and integrate the book viewer (#12) - src/hooks/useBooks.ts: TanStack Query useQuery keyed ['books', subject] over searchBooksBySubject, exposing books/isLoading/isError - App: hardcoded fantasy subject (replaced by persisted choice in M3), loading (role=status), error (role=alert) and empty states, BookCard wired into a book viewer region - test infra: msw server + OpenLibrary handlers (request counter for cache assertions), QueryClient test providers, global onUnhandledRequest error so tests never touch the network - tests: hook caching (fetch count stays 1 across re-renders), App loading/error/empty/first-book, App snapshot updated deliberately (-u) Navigation and jump-list wiring lands in this PR as #13/#14 merge. Closes #12 --- src/App.css | 5 +++ src/App.test.tsx | 47 ++++++++++++++++++------- src/App.tsx | 18 +++++++++- src/__snapshots__/App.test.tsx.snap | 47 +++++++++++++++++++++++-- src/hooks/useBooks.test.ts | 32 +++++++++++++++++ src/hooks/useBooks.ts | 22 ++++++++++++ src/test/handlers.ts | 53 +++++++++++++++++++++++++++++ src/test/server.ts | 4 +++ src/test/setup.ts | 8 ++++- src/test/utils.tsx | 32 +++++++++++++++++ 10 files changed, 251 insertions(+), 17 deletions(-) create mode 100644 src/hooks/useBooks.test.ts create mode 100644 src/hooks/useBooks.ts create mode 100644 src/test/handlers.ts create mode 100644 src/test/server.ts create mode 100644 src/test/utils.tsx diff --git a/src/App.css b/src/App.css index 3f347db..7188ed3 100644 --- a/src/App.css +++ b/src/App.css @@ -29,6 +29,11 @@ padding: 1.5rem; } +.book-viewer { + max-width: 32rem; + margin: 0 auto; +} + .app-footer { padding: 0.75rem; text-align: center; diff --git a/src/App.test.tsx b/src/App.test.tsx index 90ee37b..b161d68 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,22 +1,45 @@ -import { describe, it, expect } from "vitest"; -import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { http, HttpResponse, delay } from "msw"; +import { screen } from "@testing-library/react"; import App from "./App"; +import { withProviders } from "./test/utils"; +import { server } from "./test/server"; +import { emptySearchHandler, failingSearchHandler } from "./test/handlers"; describe("App", () => { - it("renders the main heading", () => { - render(); - const heading = screen.getByRole("heading", { level: 1 }); - expect(heading).toHaveTextContent("Digital Library"); + it("shows a loading indicator while the request is in flight", async () => { + server.use( + http.get("https://openlibrary.org/search.json", async () => { + await delay(200); + return HttpResponse.json({ docs: [] }); + }), + ); + withProviders(); + expect(screen.getByRole("status")).toHaveTextContent("Loading books"); }); - it("renders a banner and main content region", () => { - render(); - expect(screen.getByRole("banner")).toBeInTheDocument(); - expect(screen.getByRole("main")).toBeInTheDocument(); + it("renders the first book once loaded", async () => { + withProviders(); + const heading = await screen.findByRole("heading", { name: "The Hobbit" }); + expect(heading).toBeInTheDocument(); + expect(screen.getByRole("region", { name: "Book viewer" })).toBeInTheDocument(); }); - it("matches the snapshot", () => { - const { container } = render(); + it("shows an error message when the request fails", async () => { + server.use(failingSearchHandler); + withProviders(); + expect(await screen.findByRole("alert")).toHaveTextContent("Could not load books"); + }); + + it("shows an empty message when no books match the subject", async () => { + server.use(emptySearchHandler); + withProviders(); + expect(await screen.findByText(/No books found/)).toBeInTheDocument(); + }); + + it("matches the snapshot", async () => { + const { container } = withProviders(); + await screen.findByRole("heading", { level: 2 }); expect(container).toMatchSnapshot(); }); }); diff --git a/src/App.tsx b/src/App.tsx index d5d163d..e8394b0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,13 @@ +import { BookCard } from "./components/BookCard"; +import { useBooks } from "./hooks/useBooks"; import "./App.css"; +const SUBJECT = "fantasy"; + function App() { + const { books, isLoading, isError } = useBooks(SUBJECT); + const current = books[0]; + return (
@@ -8,7 +15,16 @@ function App() {

Browse reading material from OpenLibrary, one book at a time.

-

Book browsing is coming soon.

+ {isLoading &&

Loading books…

} + {isError &&

Could not load books. Please try refreshing the page.

} + {!isLoading && !isError && books.length === 0 && ( +

No books found for the subject “{SUBJECT}”.

+ )} + {current && ( +
+ +
+ )}