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