Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<App />);
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(<App />);
Expand Down Expand Up @@ -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(<App />);

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();
Expand Down
18 changes: 18 additions & 0 deletions src/__snapshots__/App.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ exports[`App > matches the snapshot 1`] = `
>
Title Z-A
</option>
<option
value="author-asc"
>
Author A-Z
</option>
<option
value="rating-desc"
>
Highest rated
</option>
<option
value="newest"
>
Expand Down Expand Up @@ -121,6 +131,14 @@ exports[`App > matches the snapshot 1`] = `
First published:
1968
</p>
<p
class="book-card-rating"
>
Rating:
4.8
/ 5
(20 ratings)
</p>
<ul
aria-label="Subjects, showing 2 of 2"
class="book-card-subjects"
Expand Down
8 changes: 8 additions & 0 deletions src/api/openLibrary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const searchResponse = {
title: "A Book",
author_name: ["Jane Doe"],
first_publish_year: 2001,
ratings_average: 4.5,
ratings_count: 23,
cover_i: 123,
subject: ["fantasy"],
},
Expand Down Expand Up @@ -37,6 +39,8 @@ describe("searchBooksBySubject", () => {
title: "A Book",
authors: ["Jane Doe"],
firstPublishYear: 2001,
averageRating: 4.5,
ratingsCount: 23,
coverId: 123,
subjects: ["fantasy"],
});
Expand All @@ -45,6 +49,8 @@ describe("searchBooksBySubject", () => {
title: "Bare Bones",
authors: [],
firstPublishYear: null,
averageRating: null,
ratingsCount: null,
coverId: null,
subjects: [],
});
Expand All @@ -58,6 +64,8 @@ describe("searchBooksBySubject", () => {
expect(url.searchParams.get("subject")).toBe("history");
expect(url.searchParams.get("limit")).toBe(String(BOOKS_PER_SUBJECT));
expect(url.searchParams.get("fields")).toContain("title");
expect(url.searchParams.get("fields")).toContain("ratings_average");
expect(url.searchParams.get("fields")).toContain("ratings_count");
});

it("throws on a failed response", async () => {
Expand Down
9 changes: 8 additions & 1 deletion src/api/openLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const COVER_URL = "https://covers.openlibrary.org/b/id";

export const BOOKS_PER_SUBJECT = 10;

const BOOK_FIELDS = "key,title,author_name,first_publish_year,cover_i,subject";
const BOOK_FIELDS =
"key,title,author_name,first_publish_year,ratings_average,ratings_count,cover_i,subject";

export async function searchBooksBySubject(
subject: string,
Expand All @@ -30,7 +31,13 @@ function toBook(doc: OpenLibrarySearchDoc): Book {
title: doc.title,
authors: doc.author_name ?? [],
firstPublishYear: doc.first_publish_year ?? null,
averageRating: numberOrNull(doc.ratings_average),
ratingsCount: numberOrNull(doc.ratings_count),
coverId: doc.cover_i ?? null,
subjects: doc.subject ?? [],
};
}

function numberOrNull(value: number | undefined): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
5 changes: 5 additions & 0 deletions src/components/BookCard.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
font-weight: 600;
}

.book-card-rating {
color: #243b53;
font-weight: 600;
}

.book-card-subjects {
display: flex;
flex-wrap: wrap;
Expand Down
6 changes: 6 additions & 0 deletions src/components/BookCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const fullBook: Book = {
title: "A Wizard of Earthsea",
authors: ["Ursula K. Le Guin", "Another Author"],
firstPublishYear: 1968,
averageRating: 4.2,
ratingsCount: 100,
coverId: 123,
subjects: ["Fantasy", "Magic"],
};
Expand All @@ -17,6 +19,8 @@ const minimalBook: Book = {
title: "Untitled Book",
authors: [],
firstPublishYear: null,
averageRating: null,
ratingsCount: null,
coverId: null,
subjects: [],
};
Expand All @@ -27,6 +31,7 @@ describe("BookCard", () => {

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",
Expand All @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions src/components/BookCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export function BookCard({ book, isFavorite = false, onFavoriteToggle }: BookCar
<h2>{book.title}</h2>
<p className="book-card-authors">{authors}</p>
{book.firstPublishYear !== null && <p>First published: {book.firstPublishYear}</p>}
{book.averageRating !== null && (
<p className="book-card-rating">
Rating: {book.averageRating.toFixed(1)} / 5
{book.ratingsCount !== null && ` (${book.ratingsCount} ratings)`}
</p>
)}
{book.subjects.length > 0 && (
<ul
className="book-card-subjects"
Expand Down
2 changes: 2 additions & 0 deletions src/components/FavoritesView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const books: Book[] = [
title: "The Hobbit",
authors: ["J. R. R. Tolkien"],
firstPublishYear: 1937,
averageRating: 4.2,
ratingsCount: 100,
coverId: 123,
subjects: ["fantasy"],
},
Expand Down
6 changes: 4 additions & 2 deletions src/components/SortSelect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ describe("SortSelect", () => {
expect(screen.getAllByRole("option").map((option) => option.textContent)).toEqual([
"Title A-Z",
"Title Z-A",
"Author A-Z",
"Highest rated",
"Newest first",
"Oldest first",
]);
Expand All @@ -22,9 +24,9 @@ describe("SortSelect", () => {
render(<SortSelect sort="title-asc" onSortChange={onSortChange} />);

fireEvent.change(screen.getByRole("combobox", { name: "Sort by" }), {
target: { value: "newest" },
target: { value: "rating-desc" },
});

expect(onSortChange).toHaveBeenCalledWith("newest");
expect(onSortChange).toHaveBeenCalledWith("rating-desc");
});
});
8 changes: 8 additions & 0 deletions src/components/__snapshots__/BookCard.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ exports[`BookCard > renders a complete book 1`] = `
First published:
1968
</p>
<p
class="book-card-rating"
>
Rating:
4.2
/ 5
(100 ratings)
</p>
<ul
aria-label="Subjects, showing 2 of 2"
class="book-card-subjects"
Expand Down
6 changes: 6 additions & 0 deletions src/test/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const fixtureDocs: OpenLibrarySearchDoc[] = [
title: "The Hobbit",
author_name: ["J. R. R. Tolkien"],
first_publish_year: 1937,
ratings_average: 4.2,
ratings_count: 100,
cover_i: 123,
subject: ["fantasy", "dragons"],
},
Expand All @@ -15,13 +17,17 @@ export const fixtureDocs: OpenLibrarySearchDoc[] = [
title: "A Wizard of Earthsea",
author_name: ["Ursula K. Le Guin"],
first_publish_year: 1968,
ratings_average: 4.8,
ratings_count: 20,
subject: ["fantasy", "magic"],
},
{
key: "/works/OL3",
title: "The Left Hand of Darkness",
author_name: ["Ursula K. Le Guin"],
first_publish_year: 1969,
ratings_average: 4.8,
ratings_count: 50,
},
];

Expand Down
7 changes: 6 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export interface Book {
title: string;
authors: string[];
firstPublishYear: number | null;
averageRating: number | null;
ratingsCount: number | null;
coverId: number | null;
subjects: string[];
}
Expand All @@ -12,6 +14,8 @@ export interface OpenLibrarySearchDoc {
title: string;
author_name?: string[];
first_publish_year?: number;
ratings_average?: number;
ratings_count?: number;
cover_i?: number;
subject?: string[];
}
Expand All @@ -24,4 +28,5 @@ export interface OpenLibrarySearchResponse {

export type CoverSize = "S" | "M" | "L";

export type SortOption = "title-asc" | "title-desc" | "newest" | "oldest";
export type SortOption =
"title-asc" | "title-desc" | "author-asc" | "rating-desc" | "newest" | "oldest";
38 changes: 38 additions & 0 deletions src/utils/sortBooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const books: Book[] = fixtureDocs.map((doc) => ({
title: doc.title,
authors: doc.author_name ?? [],
firstPublishYear: doc.first_publish_year ?? null,
averageRating: doc.ratings_average ?? null,
ratingsCount: doc.ratings_count ?? null,
coverId: doc.cover_i ?? null,
subjects: doc.subject ?? [],
}));
Expand All @@ -19,6 +21,8 @@ describe("sortBooks", () => {
it.each([
["title-asc", ["A Wizard of Earthsea", "The Hobbit", "The Left Hand of Darkness"]],
["title-desc", ["The Left Hand of Darkness", "The Hobbit", "A Wizard of Earthsea"]],
["author-asc", ["The Hobbit", "A Wizard of Earthsea", "The Left Hand of Darkness"]],
["rating-desc", ["The Left Hand of Darkness", "A Wizard of Earthsea", "The Hobbit"]],
["newest", ["The Left Hand of Darkness", "A Wizard of Earthsea", "The Hobbit"]],
["oldest", ["The Hobbit", "A Wizard of Earthsea", "The Left Hand of Darkness"]],
] as const)("sorts books with %s", (sort, expected) => {
Expand All @@ -32,4 +36,38 @@ describe("sortBooks", () => {

expect(books).toEqual(originalBooks);
});

it("places books with missing authors and ratings after available values", () => {
const booksWithMissingValues: Book[] = [
{
key: "/works/OL4",
title: "No Details",
authors: [],
firstPublishYear: null,
averageRating: null,
ratingsCount: null,
coverId: null,
subjects: [],
},
{
key: "/works/OL5",
title: "Rated Book",
authors: ["Ada Author"],
firstPublishYear: null,
averageRating: 4.5,
ratingsCount: 10,
coverId: null,
subjects: [],
},
];

expect(sortBooks(booksWithMissingValues, "author-asc").map((book) => book.title)).toEqual([
"Rated Book",
"No Details",
]);
expect(sortBooks(booksWithMissingValues, "rating-desc").map((book) => book.title)).toEqual([
"Rated Book",
"No Details",
]);
});
});
Loading