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 && ( +
+ +
+ )}