diff --git a/src/components/BookJumpList.css b/src/components/BookJumpList.css new file mode 100644 index 0000000..0e54246 --- /dev/null +++ b/src/components/BookJumpList.css @@ -0,0 +1,26 @@ +.book-jump-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + max-width: 24rem; + color: #243b53; + font-weight: 600; +} + +.book-jump-list select { + width: 100%; + min-height: 2.75rem; + padding: 0.625rem 0.75rem; + border: 1px solid #d5dde5; + border-radius: 0.5rem; + background-color: #ffffff; + color: #1f2933; + font: inherit; + font-weight: 400; +} + +.book-jump-list select:focus-visible { + outline: 0.1875rem solid #2c3e50; + outline-offset: 0.125rem; +} diff --git a/src/components/BookJumpList.test.tsx b/src/components/BookJumpList.test.tsx new file mode 100644 index 0000000..0128c75 --- /dev/null +++ b/src/components/BookJumpList.test.tsx @@ -0,0 +1,57 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { BookJumpList } from "./BookJumpList"; + +// Inline fixture data keeps the tests independent of the API and network. +const titles = ["The Hobbit", "Dune", "Pride and Prejudice"]; + +describe("BookJumpList", () => { + it('renders a native select associated with the label "Jump to book"', () => { + // Render the component with controlled test props. + render(); + + // Find the select by its accessible role and label. + const select = screen.getByRole("combobox", { name: "Jump to book" }); + + // Verify that the accessible control is a native HTML select element. + expect(select.tagName).toBe("SELECT"); + }); + + it("renders one option per title", () => { + render(); + + // Verify that every fixture title is rendered as an option in the same order. + expect(screen.getAllByRole("option")).toHaveLength(titles.length); + expect(screen.getAllByRole("option").map((option) => option.textContent)).toEqual(titles); + }); + + it("selects the option at currentIndex", () => { + render(); + + // currentIndex 1 should select the second title, "Dune". + expect(screen.getByRole("option", { name: "Dune" })).toHaveProperty("selected", true); + }); + + it("calls onSelect with the selected zero-based index", () => { + // Create a mock function so we can inspect how onSelect is called. + const onSelect = vi.fn(); + render(); + + // Simulate the user selecting the option whose value is index 2. + fireEvent.change(screen.getByRole("combobox", { name: "Jump to book" }), { + target: { value: "2" }, + }); + + // Verify that the component reports the selected index to its parent. + expect(onSelect).toHaveBeenCalledWith(2); + }); + + it("matches the rendered snapshot", () => { + const { container } = render( + , + ); + + // Detect unexpected changes to the component's rendered HTML structure. + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/components/BookJumpList.tsx b/src/components/BookJumpList.tsx new file mode 100644 index 0000000..b923a2c --- /dev/null +++ b/src/components/BookJumpList.tsx @@ -0,0 +1,28 @@ +import "./BookJumpList.css"; + +interface BookJumpListProps { + titles: string[]; // Titles available for direct navigation. + currentIndex: number; // Zero-based index of the currently selected book. + onSelect: (index: number) => void; // Notifies the parent when the user selects another book. +} + +export function BookJumpList({ titles, currentIndex, onSelect }: BookJumpListProps) { + return ( + <> + + + ); +} diff --git a/src/components/__snapshots__/BookJumpList.test.tsx.snap b/src/components/__snapshots__/BookJumpList.test.tsx.snap new file mode 100644 index 0000000..e6b93a5 --- /dev/null +++ b/src/components/__snapshots__/BookJumpList.test.tsx.snap @@ -0,0 +1,31 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`BookJumpList > matches the rendered snapshot 1`] = ` +
+ +
+`;