diff --git a/src/App.test.tsx b/src/App.test.tsx
index a613eef..9821139 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it } from "vitest";
import { http, HttpResponse, delay } from "msw";
import { fireEvent, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
import App from "./App";
import { withProviders } from "./test/utils";
import { server } from "./test/server";
@@ -47,6 +48,50 @@ describe("App", () => {
expect(await screen.findByText(/No books found/)).toBeInTheDocument();
});
+ it("navigates to the next and previous book with the controls", async () => {
+ const user = userEvent.setup();
+ withProviders();
+ await screen.findByRole("heading", { name: "The Hobbit" });
+
+ await user.click(screen.getByRole("button", { name: "Show next book" }));
+ expect(
+ await screen.findByRole("heading", { name: "A Wizard of Earthsea" }),
+ ).toBeInTheDocument();
+ expect(screen.getByText("2 / 3")).toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "Show previous book" }));
+ expect(await screen.findByRole("heading", { name: "The Hobbit" })).toBeInTheDocument();
+ expect(screen.getByText("1 / 3")).toBeInTheDocument();
+ });
+
+ it("jumps directly to a book from the list", async () => {
+ const user = userEvent.setup();
+ withProviders();
+ await screen.findByRole("heading", { name: "The Hobbit" });
+
+ await user.selectOptions(screen.getByLabelText("Jump to book"), "2");
+ expect(
+ await screen.findByRole("heading", { name: "The Left Hand of Darkness" }),
+ ).toBeInTheDocument();
+ expect(screen.getByText("3 / 3")).toBeInTheDocument();
+ });
+
+ it("does not refetch while navigating between books", async () => {
+ resetSearchRequestCount();
+ const user = userEvent.setup();
+ withProviders();
+ await screen.findByRole("heading", { name: "The Hobbit" });
+
+ await user.click(screen.getByRole("button", { name: "Show next book" }));
+ await screen.findByRole("heading", { name: "A Wizard of Earthsea" });
+ await user.click(screen.getByRole("button", { name: "Show previous book" }));
+ await screen.findByRole("heading", { name: "The Hobbit" });
+ await user.selectOptions(screen.getByLabelText("Jump to book"), "2");
+ await screen.findByRole("heading", { name: "The Left Hand of Darkness" });
+
+ expect(getSearchRequestCount()).toBe(1);
+ });
+
it("fetches once more when the subject changes", async () => {
withProviders();
await screen.findByRole("heading", { name: "The Hobbit" });
diff --git a/src/App.tsx b/src/App.tsx
index f566c60..a7c2db0 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,4 +1,7 @@
+import { useState } from "react";
import { BookCard } from "./components/BookCard";
+import { BookJumpList } from "./components/BookJumpList";
+import { NavigationControls } from "./components/NavigationControls";
import { SubjectFilter } from "./components/SubjectFilter";
import { useBooks } from "./hooks/useBooks";
import { usePreferences } from "./hooks/usePreferences";
@@ -7,7 +10,10 @@ import "./App.css";
function App() {
const { subject, setSubject } = usePreferences();
const { books, isLoading, isError } = useBooks(subject);
- const current = books[0];
+
+ const [index, setIndex] = useState(0);
+ const safeIndex = books.length > 0 ? Math.min(index, books.length - 1) : 0;
+ const current = books[safeIndex];
return (
@@ -25,6 +31,17 @@ function App() {
{current && (
+ setIndex(Math.max(0, safeIndex - 1))}
+ onNext={() => setIndex(Math.min(books.length - 1, safeIndex + 1))}
+ />
+ book.title)}
+ currentIndex={safeIndex}
+ onSelect={setIndex}
+ />
)}
diff --git a/src/__snapshots__/App.test.tsx.snap b/src/__snapshots__/App.test.tsx.snap
index 4c98b2e..10bad78 100644
--- a/src/__snapshots__/App.test.tsx.snap
+++ b/src/__snapshots__/App.test.tsx.snap
@@ -101,6 +101,57 @@ exports[`App > matches the snapshot 1`] = `
+
+