From 7b9925e2a9917be3ecc25d204874dec318e2d407 Mon Sep 17 00:00:00 2001 From: Rachel Stiansen Date: Thu, 10 Sep 2026 12:04:01 +0200 Subject: [PATCH 1/2] feat: add book sorting with sessionStorage persistence (#20) --- src/App.test.tsx | 69 ++++++++++++++++++++++++----- src/App.tsx | 16 ++++--- src/__snapshots__/App.test.tsx.snap | 49 +++++++++++++++----- src/components/SortSelect.css | 25 +++++++++++ src/components/SortSelect.test.tsx | 30 +++++++++++++ src/components/SortSelect.tsx | 27 +++++++++++ src/hooks/usePreferences.ts | 27 ++++++++++- src/types.ts | 2 + src/utils/sortBooks.test.ts | 35 +++++++++++++++ src/utils/sortBooks.ts | 38 ++++++++++++++++ 10 files changed, 290 insertions(+), 28 deletions(-) create mode 100644 src/components/SortSelect.css create mode 100644 src/components/SortSelect.test.tsx create mode 100644 src/components/SortSelect.tsx create mode 100644 src/utils/sortBooks.test.ts create mode 100644 src/utils/sortBooks.ts diff --git a/src/App.test.tsx b/src/App.test.tsx index 9821139..818f54b 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -31,7 +31,7 @@ describe("App", () => { it("renders the first book once loaded", async () => { withProviders(); - const heading = await screen.findByRole("heading", { name: "The Hobbit" }); + const heading = await screen.findByRole("heading", { name: "A Wizard of Earthsea" }); expect(heading).toBeInTheDocument(); expect(screen.getByRole("region", { name: "Book viewer" })).toBeInTheDocument(); }); @@ -51,23 +51,23 @@ describe("App", () => { 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 screen.findByRole("heading", { name: "A Wizard of Earthsea" }); await user.click(screen.getByRole("button", { name: "Show next book" })); - expect( - await screen.findByRole("heading", { name: "A Wizard of Earthsea" }), - ).toBeInTheDocument(); + expect(await screen.findByRole("heading", { name: "The Hobbit" })).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( + await screen.findByRole("heading", { name: "A Wizard of Earthsea" }), + ).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 screen.findByRole("heading", { name: "A Wizard of Earthsea" }); await user.selectOptions(screen.getByLabelText("Jump to book"), "2"); expect( @@ -80,27 +80,45 @@ describe("App", () => { resetSearchRequestCount(); const user = userEvent.setup(); withProviders(); - await screen.findByRole("heading", { name: "The Hobbit" }); + await screen.findByRole("heading", { name: "A Wizard of Earthsea" }); 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.click(screen.getByRole("button", { name: "Show previous book" })); + await screen.findByRole("heading", { name: "A Wizard of Earthsea" }); await user.selectOptions(screen.getByLabelText("Jump to book"), "2"); await screen.findByRole("heading", { name: "The Left Hand of Darkness" }); expect(getSearchRequestCount()).toBe(1); }); + it("sorts the displayed books without making another API request", async () => { + const user = userEvent.setup(); + withProviders(); + await screen.findByRole("heading", { name: "A Wizard of Earthsea" }); + + await user.selectOptions(screen.getByRole("combobox", { name: "Sort by" }), "newest"); + + expect( + await screen.findByRole("heading", { name: "The Left Hand of Darkness" }), + ).toBeInTheDocument(); + expect(getSearchRequestCount()).toBe(1); + expect(screen.getByRole("combobox", { name: "Jump to book" })).toHaveValue("0"); + expect(screen.getByRole("option", { name: "The Left Hand of Darkness" })).toBeInTheDocument(); + }); + it("fetches once more when the subject changes", async () => { withProviders(); - await screen.findByRole("heading", { name: "The Hobbit" }); + await screen.findByRole("heading", { name: "A Wizard of Earthsea" }); fireEvent.change(screen.getByRole("combobox", { name: "Subject" }), { target: { value: "mystery" }, }); await waitFor(() => expect(getSearchRequestCount()).toBe(2)); + expect( + await screen.findByRole("heading", { name: "A Wizard of Earthsea" }), + ).toBeInTheDocument(); expect(JSON.parse(sessionStorage.getItem("t19.preferences") ?? "{}")).toMatchObject({ subject: "mystery", }); @@ -114,11 +132,40 @@ describe("App", () => { await waitFor(() => expect(getSearchRequestCount()).toBe(1)); }); + it("restores the selected sort and preserves the subject preference", async () => { + sessionStorage.setItem( + "t19.preferences", + JSON.stringify({ subject: "mystery", sort: "newest" }), + ); + withProviders(); + + expect(screen.getByRole("combobox", { name: "Subject" })).toHaveValue("mystery"); + expect(screen.getByRole("combobox", { name: "Sort by" })).toHaveValue("newest"); + expect( + await screen.findByRole("heading", { name: "The Left Hand of Darkness" }), + ).toBeInTheDocument(); + }); + + it("preserves unrelated preferences when the sort changes", async () => { + const user = userEvent.setup(); + sessionStorage.setItem("t19.preferences", JSON.stringify({ subject: "mystery" })); + withProviders(); + await screen.findByRole("heading", { name: "A Wizard of Earthsea" }); + + await user.selectOptions(screen.getByRole("combobox", { name: "Sort by" }), "oldest"); + + expect(JSON.parse(sessionStorage.getItem("t19.preferences") ?? "{}")).toEqual({ + subject: "mystery", + sort: "oldest", + }); + }); + it("uses the default subject when stored preferences are corrupt", () => { sessionStorage.setItem("t19.preferences", "not valid json"); withProviders(); expect(screen.getByRole("combobox", { name: "Subject" })).toHaveValue("fantasy"); + expect(screen.getByRole("combobox", { name: "Sort by" })).toHaveValue("title-asc"); }); it("matches the snapshot", async () => { diff --git a/src/App.tsx b/src/App.tsx index a7c2db0..d936f8b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,15 +5,18 @@ import { NavigationControls } from "./components/NavigationControls"; import { SubjectFilter } from "./components/SubjectFilter"; import { useBooks } from "./hooks/useBooks"; import { usePreferences } from "./hooks/usePreferences"; +import { SortSelect } from "./components/SortSelect"; +import { sortBooks } from "./utils/sortBooks"; import "./App.css"; function App() { - const { subject, setSubject } = usePreferences(); + const { subject, setSubject, sort, setSort } = usePreferences(); const { books, isLoading, isError } = useBooks(subject); + const sortedBooks = sortBooks(books, sort); const [index, setIndex] = useState(0); - const safeIndex = books.length > 0 ? Math.min(index, books.length - 1) : 0; - const current = books[safeIndex]; + const safeIndex = sortedBooks.length > 0 ? Math.min(index, sortedBooks.length - 1) : 0; + const current = sortedBooks[safeIndex]; return (
@@ -23,6 +26,7 @@ function App() {
+ {isLoading &&

Loading books…

} {isError &&

Could not load books. Please try refreshing the page.

} {!isLoading && !isError && books.length === 0 && ( @@ -33,12 +37,12 @@ function App() { setIndex(Math.max(0, safeIndex - 1))} - onNext={() => setIndex(Math.min(books.length - 1, safeIndex + 1))} + onNext={() => setIndex(Math.min(sortedBooks.length - 1, safeIndex + 1))} /> book.title)} + titles={sortedBooks.map((book) => book.title)} currentIndex={safeIndex} onSelect={setIndex} /> diff --git a/src/__snapshots__/App.test.tsx.snap b/src/__snapshots__/App.test.tsx.snap index 10bad78..d920d45 100644 --- a/src/__snapshots__/App.test.tsx.snap +++ b/src/__snapshots__/App.test.tsx.snap @@ -58,6 +58,36 @@ exports[`App > matches the snapshot 1`] = ` +
matches the snapshot 1`] = `
- Cover of The Hobbit +

+ No cover available +

- The Hobbit + A Wizard of Earthsea

- J. R. R. Tolkien + Ursula K. Le Guin

First published: - 1937 + 1968

    matches the snapshot 1`] = ` fantasy
  • - dragons + magic
@@ -138,12 +167,12 @@ exports[`App > matches the snapshot 1`] = `