diff --git a/README.md b/README.md
index c2258e1..29633cd 100644
--- a/README.md
+++ b/README.md
@@ -41,10 +41,7 @@ Shared helpers and fixtures live in `app/test/`:
- `setup.ts` adds the [jest-dom](https://github.com/testing-library/jest-dom)
matchers (e.g. `toBeInTheDocument`) and clears the DOM and `localStorage`
after every test.
-- `renderWithQueryClient.tsx` renders components inside a
- `QueryClientProvider`, which components that use `useBooks` need.
-- `fixtures/` holds saved Open Library responses (a search and a work) and a
- sample book.
+- `fixtures/` holds a sample book.
### What is tested
@@ -52,31 +49,16 @@ We test where there is logic, state or several parts working together. Static
markup (like `Header` and `Footer`) is not tested: such tests break on every
intended change without catching real bugs.
-- **Unit tests** for the API layer (`app/api/openLibrary.test.ts`): `toBook`
- and `toDescription` map Open Library data to our types (with fallbacks for
- missing fields), and `searchBooks` and `fetchWork` build the right request
- and throw on HTTP errors.
-- **Component tests** for `BookCard`, `BookDetails`, `FavoriteButton`,
- `Pagination`, `SearchBar` and `BookGrid`: rendering based on props, user
- interaction (clicks, typing) and state (favorites in `localStorage`; loading,
- error and empty states; paging; loading the description).
+- **Component tests** for `BookCard`, `FavoriteButton`, `Pagination` and
+ `SearchBar`: rendering based on props, user interaction (clicks, typing)
+ and state (favorites in `localStorage`, paging).
- **Snapshot test** for `BookCard`. The rendered HTML is stored in
`__snapshots__/` next to the test and compared on every run. If a markup
change is intended, update the snapshot with `pnpm test -u`.
-- **Integration test** for `App`: selecting a book shows its details and
- description, and going back shows the grid again without a new API request.
### Mocking
-No test sends requests to openlibrary.org:
-
-- `openLibrary.test.ts` replaces the global `fetch` with a mock (`vi.spyOn`)
- that answers with the saved responses in `app/test/fixtures/`.
-- The `App` and `BookDetails` tests replace `searchBooks` and `fetchWork`
- with mocks (`vi.mock`), so the components receive the fixture data
- (`searchResponse.json`, `workResponse.json`) right away.
-- `BookGrid` does not fetch anything itself; its tests pass a fake query
- result in as a prop.
+The page tests (home and selectedBook) were removed when the frontend moved from Open Library to our GraphQL API. They will be rewritten with Apollo's MockedProvider in #33. The remaining component tests get their data through props, so they do not need any mocking.
### Browsers and devices
@@ -145,5 +127,5 @@ short, it was used for:
the `description` field of a work.
Where AI contributed directly to a piece of code, there is a comment saying
-so, for example in [app/api/types.ts](app/api/types.ts) and
-[app/root.tsx](app/root.tsx).
+so, for example in [frontend/app/api/types/index.ts](frontend/app/api/types/index.ts) and
+[frontend/app/root.tsx](frontend/app/root.tsx).
diff --git a/frontend/app/routes/book/selectedBook.test.tsx b/frontend/app/routes/book/selectedBook.test.tsx
deleted file mode 100644
index 9c9027b..0000000
--- a/frontend/app/routes/book/selectedBook.test.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { describe, expect, it, vi } from "vitest";
-import { screen } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import { fetchWork, searchBooks } from "~/api/openLibrary";
-import { renderWithQueryClient } from "~/test/renderWithQueryClient";
-import { bookWithCover as book } from "~/test/fixtures/books";
-import searchResponse from "~/test/fixtures/searchResponse.json";
-import workResponse from "~/test/fixtures/workResponse.json";
-import SelectedBookPage from "~/routes/book/selectedBook";
-import HomePage from "~/routes/home";
-import { createRoutesStub } from "react-router";
-
-// SelectedBookPage needs both halves of the Open Library data: the book list
-// through useBooks -> searchBooks, and the description through
-// useBookDescription -> fetchWork. Mock both so no requests are made.
-vi.mock(import("~/api/openLibrary"), async (importOriginal) => ({
- ...(await importOriginal()),
- searchBooks: vi.fn(),
- fetchWork: vi.fn(),
-}));
-
-const Stub = createRoutesStub([
- {
- path: "/",
- Component: HomePage,
- },
- {
- path: "/book/:bookId",
- Component: SelectedBookPage,
- },
-]);
-
-describe("SelectedBookPage", () => {
- it("shows the description once it has loaded", async () => {
- vi.mocked(searchBooks).mockResolvedValue(searchResponse);
- vi.mocked(fetchWork).mockResolvedValue(workResponse);
-
- renderWithQueryClient();
-
- expect(
- await screen.findByRole("heading", { name: "The Blade Itself" }),
- ).toBeInTheDocument();
- expect(await screen.findByText(/Logen Ninefingers/)).toBeInTheDocument();
- expect(fetchWork).toHaveBeenCalledWith(book.id);
- });
-
- it("goes back to the grid when the back button is clicked", async () => {
- vi.mocked(searchBooks).mockResolvedValue(searchResponse);
- vi.mocked(fetchWork).mockResolvedValue(workResponse);
- const user = userEvent.setup();
-
- renderWithQueryClient();
-
- await user.click(
- await screen.findByRole("link", { name: "Go back to search results" }),
- );
-
- // The grid comes back, so the navigation to "/" resolved.
- expect(await screen.findByRole("list")).toBeInTheDocument();
- expect(screen.queryByText(/Logen Ninefingers/)).toBe(null);
- });
-
- it("shows a message when the book is missing from the list", async () => {
- vi.mocked(searchBooks).mockResolvedValue(searchResponse);
- vi.mocked(fetchWork).mockResolvedValue(workResponse);
-
- renderWithQueryClient();
-
- expect(
- await screen.findByRole("heading", { name: "Book Not Found" }),
- ).toBeInTheDocument();
- });
-});
diff --git a/frontend/app/routes/home.test.tsx b/frontend/app/routes/home.test.tsx
deleted file mode 100644
index c6b2ae6..0000000
--- a/frontend/app/routes/home.test.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { describe, expect, it, vi } from "vitest";
-import { screen } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import HomePage from "~/routes/home";
-import SelectedBookPage from "~/routes/book/selectedBook";
-import { fetchWork, searchBooks } from "~/api/openLibrary";
-import { renderWithQueryClient } from "~/test/renderWithQueryClient";
-import searchResponse from "~/test/fixtures/searchResponse.json";
-import workResponse from "~/test/fixtures/workResponse.json";
-import { createRoutesStub } from "react-router";
-
-// Mock both API functions so nothing is fetched from openlibrary.org.
-vi.mock(import("~/api/openLibrary"), async (importOriginal) => ({
- ...(await importOriginal()),
- searchBooks: vi.fn(),
- fetchWork: vi.fn(),
-}));
-
-describe("HomePage", () => {
- const HomePageStub = createRoutesStub([
- {
- path: "/",
- Component: HomePage,
- },
- {
- path: "/book/:bookId",
- Component: SelectedBookPage,
- },
- ]);
-
- it("opens a book's details from the grid and goes back", async () => {
- vi.mocked(searchBooks).mockResolvedValue(searchResponse);
- vi.mocked(fetchWork).mockResolvedValue(workResponse);
- const user = userEvent.setup();
- renderWithQueryClient();
-
- await user.click(
- await screen.findByRole("link", {
- name: "View details for The Blade Itself",
- }),
- );
-
- expect(
- screen.getByRole("heading", { name: "The Blade Itself" }),
- ).toBeInTheDocument();
- expect(await screen.findByText(/Logen Ninefingers/)).toBeInTheDocument();
- expect(screen.queryByRole("list")).not.toBeInTheDocument();
-
- await user.click(
- screen.getByRole("link", { name: "Go back to search results" }),
- );
-
- expect(screen.getByRole("list")).toBeInTheDocument();
- expect(screen.getAllByRole("article")).toHaveLength(5);
-
- expect(searchBooks).toHaveBeenCalledTimes(1);
- expect(fetchWork).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/frontend/app/test/fixtures/books.ts b/frontend/app/test/fixtures/books.ts
index 67914f3..269cc18 100644
--- a/frontend/app/test/fixtures/books.ts
+++ b/frontend/app/test/fixtures/books.ts
@@ -1,8 +1,6 @@
import type { Book } from "~/api/types";
-// Sample book for component tests. Matches the first doc in searchResponse.json
-// and the work in workResponse.json. The id has no "/works/" prefix, the same
-// way toBook() maps an Open Library doc (app/api/openLibrary.ts).
+// Sample book for component tests.
const bookWithCover: Book = {
id: "OL8400950W",
title: "The Blade Itself",
diff --git a/frontend/app/test/fixtures/searchResponse.json b/frontend/app/test/fixtures/searchResponse.json
deleted file mode 100644
index 6c253da..0000000
--- a/frontend/app/test/fixtures/searchResponse.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "numFound": 118610,
- "start": 0,
- "numFoundExact": true,
- "num_found": 118610,
- "documentation_url": "https://openlibrary.org/dev/docs/api/search",
- "q": "subject:fantasy",
- "offset": null,
- "docs": [
- {
- "author_name": ["Joe Abercrombie"],
- "cover_i": 14543422,
- "first_publish_year": 2001,
- "key": "/works/OL8400950W",
- "title": "The Blade Itself"
- },
- {
- "author_name": ["Brandon Sanderson"],
- "cover_i": 14658094,
- "first_publish_year": 2008,
- "key": "/works/OL5738154W",
- "title": "The Hero of Ages"
- },
- {
- "author_name": ["Scott Lynch"],
- "cover_i": 6307636,
- "first_publish_year": 2001,
- "key": "/works/OL8369445W",
- "title": "The Lies of Locke Lamora"
- },
- {
- "author_name": ["Sarah J. Maas"],
- "cover_i": 7794980,
- "first_publish_year": 2014,
- "key": "/works/OL17546674W",
- "title": "The Assassin’s Blade"
- },
- {
- "author_name": ["Terry Goodkind"],
- "cover_i": 1005963,
- "first_publish_year": 1994,
- "key": "/works/OL2010436W",
- "title": "Wizard's First Rule"
- }
- ]
-}
diff --git a/frontend/app/test/fixtures/workResponse.json b/frontend/app/test/fixtures/workResponse.json
deleted file mode 100644
index d4d7d98..0000000
--- a/frontend/app/test/fixtures/workResponse.json
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "key": "/works/OL8400950W",
- "title": "The Blade Itself",
- "authors": [
- {
- "author": { "key": "/authors/OL2801083A" },
- "type": { "key": "/type/author_role" }
- }
- ],
- "type": { "key": "/type/work" },
- "covers": [14543422],
- "description": {
- "type": "/type/text",
- "value": "Logen Ninefingers, infamous barbarian, has finally run out of luck. Caught in one feud too many, he\u2019s on the verge of becoming a dead barbarian \u2013 leaving nothing behind him but bad songs, dead friends, and a lot of happy enemies.\r\nNobleman Captain Jezal dan Luthar, dashing officer, and paragon of selfishness, has nothing more dangerous in mind than fleecing his friends at cards and dreaming of glory in the fencing circle. But war is brewing, and on the battlefields of the frozen North they fight by altogether bloodier rules.\r\n\r\nInquisitor Glokta, cripple turned torturer, would like nothing better than to see Jezal come home in a box. But then Glokta hates everyone: cutting treason out of the Union one confession at a time leaves little room for friendship. His latest trail of corpses may lead him right to the rotten heart of government, if he can stay alive long enough to follow it.\r\n\r\nEnter the wizard, Bayaz. A bald old man with a terrible temper and a pathetic assistant, he could be the First of the Magi, he could be a spectacular fraud, but whatever he is, he's about to make the lives of Logen, Jezal, and Glotka a whole lot more difficult.\r\n\r\nMurderous conspiracies rise to the surface, old scores are ready to be settled, and the line between hero and villain is sharp enough to draw blood."
- },
- "subjects": [
- "Fiction, fantasy, general",
- "Fantasy fiction",
- "Fantasy",
- "Fiction"
- ],
- "genres": ["/tags/OL169T"],
- "latest_revision": 11,
- "revision": 11,
- "created": {
- "type": "/type/datetime",
- "value": "2009-12-10T22:45:29.339312"
- },
- "last_modified": {
- "type": "/type/datetime",
- "value": "2026-08-30T09:00:45.760177"
- }
-}
diff --git a/frontend/app/test/renderWithQueryClient.tsx b/frontend/app/test/renderWithQueryClient.tsx
deleted file mode 100644
index bfc4ba6..0000000
--- a/frontend/app/test/renderWithQueryClient.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import type { PropsWithChildren, ReactElement } from "react";
-import { render } from "@testing-library/react";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-
-// Components that use TanStack Query (via useBooks) must be rendered inside a
-// QueryClientProvider. Every test gets a fresh client so cached data never leaks
-// between tests. retry is off so error tests fail immediately instead of
-// waiting for retries. staleTime matches main.tsx so cached data is reused.
-function createQueryClient() {
- return new QueryClient({
- defaultOptions: {
- queries: { retry: false, staleTime: 10 * 60 * 1000 },
- },
- });
-}
-
-// Wrapper for renderHook(), e.g. renderHook(() => useBooks(...), { wrapper: createQueryClientWrapper() })
-function createQueryClientWrapper() {
- const queryClient = createQueryClient();
- return function QueryClientWrapper({ children }: PropsWithChildren) {
- return (
- {children}
- );
- };
-}
-
-// Drop-in replacement for render() from Testing Library.
-function renderWithQueryClient(ui: ReactElement) {
- return render(ui, { wrapper: createQueryClientWrapper() });
-}
-
-export { createQueryClientWrapper, renderWithQueryClient };