diff --git a/src/App.test.tsx b/src/App.test.tsx index 7b7911c..e8edf03 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -32,7 +32,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(); }); @@ -52,23 +52,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( @@ -81,27 +81,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", }); @@ -115,31 +133,62 @@ 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("saves, restores, and removes a favorite", async () => { const firstRender = withProviders(); - await screen.findByRole("heading", { name: "The Hobbit" }); + await screen.findByRole("heading", { name: "A Wizard of Earthsea" }); - fireEvent.click(screen.getByRole("button", { name: "Add The Hobbit to favorites" })); - expect(localStorage.getItem("t19.favorites")).toBe(JSON.stringify(["/works/OL1"])); + fireEvent.click(screen.getByRole("button", { name: "Add A Wizard of Earthsea to favorites" })); + expect(localStorage.getItem("t19.favorites")).toBe(JSON.stringify(["/works/OL2"])); firstRender.unmount(); withProviders(); - await screen.findByRole("button", { name: "Remove The Hobbit from favorites list" }); + await screen.findByRole("button", { name: "Remove A Wizard of Earthsea from favorites list" }); - fireEvent.click(screen.getByRole("button", { name: "Remove The Hobbit from favorites list" })); + fireEvent.click( + screen.getByRole("button", { name: "Remove A Wizard of Earthsea from favorites list" }), + ); expect(localStorage.getItem("t19.favorites")).toBe(JSON.stringify([])); }); it("matches the snapshot", async () => { const { container } = withProviders(); - await screen.findByRole("heading", { level: 2, name: "The Hobbit" }); + await screen.findByRole("heading", { level: 2, name: "A Wizard of Earthsea" }); expect(container).toMatchSnapshot(); }); }); diff --git a/src/App.tsx b/src/App.tsx index 2198822..8ab1884 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,14 +7,20 @@ import { SubjectFilter } from "./components/SubjectFilter"; import { useBooks } from "./hooks/useBooks"; import { useFavorites } from "./hooks/useFavorites"; 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]; + const { favoriteKeys, toggleFavorite, removeFavorite } = useFavorites(); return ( @@ -25,6 +31,7 @@ function App() {
+ {isLoading &&

Loading books…

} {isError &&

Could not load books. Please try refreshing the page.

} {!isLoading && !isError && books.length === 0 && ( @@ -39,12 +46,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 77d621e..69cfc6c 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