From fc35644250ffc059a464914b57862c033dfee2ce Mon Sep 17 00:00:00 2001
From: Robert Andreas Kyllo
Date: Wed, 16 Sep 2026 13:39:16 +0200
Subject: [PATCH 1/2] feat: add author and rating sorting (#36)
---
src/App.test.tsx | 21 ++++++++++--
src/__snapshots__/App.test.tsx.snap | 10 ++++++
src/api/openLibrary.test.ts | 8 +++++
src/api/openLibrary.ts | 9 +++++-
src/components/FavoritesView.test.tsx | 2 ++
src/components/SortSelect.test.tsx | 6 ++--
src/test/handlers.ts | 6 ++++
src/types.ts | 7 +++-
src/utils/sortBooks.test.ts | 38 ++++++++++++++++++++++
src/utils/sortBooks.ts | 46 +++++++++++++++++++++++++++
10 files changed, 146 insertions(+), 7 deletions(-)
diff --git a/src/App.test.tsx b/src/App.test.tsx
index de5f00a..4aa1baf 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -111,6 +111,21 @@ describe("App", () => {
expect(screen.getByRole("option", { name: "The Left Hand of Darkness" })).toBeInTheDocument();
});
+ it("sorts by author and rating 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" }), "author-asc");
+ expect(await screen.findByRole("heading", { name: "The Hobbit" })).toBeInTheDocument();
+
+ await user.selectOptions(screen.getByRole("combobox", { name: "Sort by" }), "rating-desc");
+ expect(
+ await screen.findByRole("heading", { name: "The Left Hand of Darkness" }),
+ ).toBeInTheDocument();
+ expect(getSearchRequestCount()).toBe(1);
+ });
+
it("fetches once more when the subject changes and resets to the first book", async () => {
const user = userEvent.setup();
withProviders();
@@ -140,15 +155,15 @@ describe("App", () => {
await waitFor(() => expect(getSearchRequestCount()).toBe(1));
});
- it("restores the selected sort and preserves the subject preference", async () => {
+ it("restores the selected rating sort and preserves the subject preference", async () => {
sessionStorage.setItem(
"t19.preferences",
- JSON.stringify({ subject: "mystery", sort: "newest" }),
+ JSON.stringify({ subject: "mystery", sort: "rating-desc" }),
);
withProviders();
expect(screen.getByRole("combobox", { name: "Subject" })).toHaveValue("mystery");
- expect(screen.getByRole("combobox", { name: "Sort by" })).toHaveValue("newest");
+ expect(screen.getByRole("combobox", { name: "Sort by" })).toHaveValue("rating-desc");
expect(
await screen.findByRole("heading", { name: "The Left Hand of Darkness" }),
).toBeInTheDocument();
diff --git a/src/__snapshots__/App.test.tsx.snap b/src/__snapshots__/App.test.tsx.snap
index 99d18d7..c9dddbb 100644
--- a/src/__snapshots__/App.test.tsx.snap
+++ b/src/__snapshots__/App.test.tsx.snap
@@ -79,6 +79,16 @@ exports[`App > matches the snapshot 1`] = `
>
Title Z-A
+
+
+
+ Rating:
+ 4.8
+ / 5
+ (20 ratings)
+
{
expect(screen.getByRole("heading", { name: fullBook.title })).toBeInTheDocument();
expect(screen.getByText("Ursula K. Le Guin, Another Author")).toBeInTheDocument();
+ expect(screen.getByText("Rating: 4.2 / 5 (100 ratings)")).toBeInTheDocument();
expect(screen.getByRole("img", { name: "Cover of A Wizard of Earthsea" })).toBeInTheDocument();
expect(screen.getByRole("list", { name: "Subjects, showing 2 of 2" })).toHaveTextContent(
"Fantasy",
@@ -39,6 +44,7 @@ describe("BookCard", () => {
expect(screen.getByRole("heading", { name: minimalBook.title })).toBeInTheDocument();
expect(screen.getByText("Unknown author")).toBeInTheDocument();
+ expect(screen.queryByText(/^Rating:/)).not.toBeInTheDocument();
expect(screen.getByText("No cover available")).toBeInTheDocument();
expect(screen.queryByRole("img")).not.toBeInTheDocument();
expect(container).toMatchSnapshot();
diff --git a/src/components/BookCard.tsx b/src/components/BookCard.tsx
index 7518ce4..cb55999 100644
--- a/src/components/BookCard.tsx
+++ b/src/components/BookCard.tsx
@@ -30,6 +30,12 @@ export function BookCard({ book, isFavorite = false, onFavoriteToggle }: BookCar
{book.title}
{authors}
{book.firstPublishYear !== null && First published: {book.firstPublishYear}
}
+ {book.averageRating !== null && (
+
+ Rating: {book.averageRating.toFixed(1)} / 5
+ {book.ratingsCount !== null && ` (${book.ratingsCount} ratings)`}
+
+ )}
{book.subjects.length > 0 && (