Files
siftlode/frontend/src/components/ui/YearPicker.dom.test.tsx
T
peter 0eca18bcb4 fix(filters): the year dialog fits a tablet, walks by arrow keys, names one year once
- a decade cell is a 36-high box with its count under the year (owner A)
- arrow keys walk the cells: along a decade, then to the nearest year below
- clearing the kept older year hands the focus on instead of dropping it
- More... shows while a year with titles is hidden, as a plain chip
- the decade caption is not read twice; the Plex range fields have names
- one Plex year is labelled 1999, not 1999-1999, via oneYear/singleYear
- tests for the Plex wiring and the feed year clearing the upload date
2026-11-14 17:56:37 +01:00

141 lines
5.7 KiB
TypeScript

// @vitest-environment jsdom
import { describe, it, expect, vi, afterEach } from "vitest";
import { render, screen, fireEvent, cleanup, within, act } from "@testing-library/react";
vi.mock("react-i18next", async (importOriginal) => {
const actual = await importOriginal<typeof import("react-i18next")>();
return {
...actual,
useTranslation: () => ({
t: (k: string, o?: Record<string, unknown>) =>
o && "from" in o ? `${k}:${String(o.from)}-${String(o.to)}` : k,
i18n: { language: "en" },
}),
};
});
vi.mock("../../lib/history", () => ({ useBackToClose: () => {} }));
import YearPicker from "./YearPicker";
afterEach(cleanup);
// Eight years over three decades, 1987 and 2014 without titles.
const YEARS = [
{ year: 2026, count: 3 },
{ year: 2024, count: 9 },
{ year: 2023, count: 1 },
{ year: 2021, count: 4 },
{ year: 2020, count: 2 },
{ year: 2015, count: 5 },
{ year: 2013, count: 7 },
{ year: 1985, count: 6 },
];
const open = () => fireEvent.click(screen.getByRole("button", { name: /sidebar\.year\.more/ }));
describe("YearPicker", () => {
it("shows the six newest years as chips, the selected one kept beside them", () => {
render(<YearPicker years={YEARS} selected={1985} onPick={() => {}} />);
const chips = screen.getAllByRole("button", { pressed: false }).map((b) => b.textContent);
expect(chips.slice(0, 6)).toEqual(["20263", "20249", "20231", "20214", "20202", "20155"]);
expect(screen.getByRole("button", { pressed: true }).textContent).toBe("19856");
});
it("lays every year out by decade in the dialog; a year without titles is not a choice", () => {
render(<YearPicker years={YEARS} onPick={() => {}} />);
open();
const dialog = screen.getByRole("dialog");
const rows = within(dialog).getAllByRole("group", { name: /sidebar\.year\.decade/ });
expect(rows.map((r) => r.getAttribute("aria-label"))).toEqual([
"sidebar.year.decade:2020-2029",
"sidebar.year.decade:2010-2019",
"sidebar.year.decade:1980-1989",
]);
const in1980s = within(rows[2]!)
.getAllByRole("button")
.map((b) => b.dataset.year);
expect(in1980s).toEqual(["1985"]);
expect(rows[1]!.querySelector('[data-year="2014"]')).toBeNull();
});
it("sorted by count, the busiest year comes first", () => {
render(<YearPicker years={YEARS} onPick={() => {}} />);
open();
fireEvent.click(screen.getByRole("button", { name: "sidebar.year.sortCount" }));
const order = within(screen.getByRole("dialog"))
.getAllByRole("button")
.map((b) => b.dataset.year)
.filter(Boolean);
expect(order.slice(0, 3)).toEqual(["2024", "2013", "1985"]);
});
it("a pick in the dialog names the year and closes it; the selected year again clears it", () => {
const onPick = vi.fn();
const { rerender } = render(<YearPicker years={YEARS} onPick={onPick} />);
open();
fireEvent.click(screen.getByRole("dialog").querySelector('[data-year="1985"]')!);
expect(onPick).toHaveBeenLastCalledWith(1985);
expect(screen.queryByRole("dialog")).toBeNull();
rerender(<YearPicker years={YEARS} selected={2024} onPick={onPick} />);
fireEvent.click(screen.getByRole("button", { pressed: true }));
expect(onPick).toHaveBeenLastCalledWith(undefined);
});
it("offers More… while a year with titles is not among the chips, the kept selection aside", () => {
// Seven years, and a selected year with no titles kept beside the six newest: 2013 is hidden.
render(<YearPicker years={YEARS.slice(0, 7)} selected={1999} onPick={() => {}} />);
expect(screen.getByRole("button", { name: "sidebar.year.more" })).toBeTruthy();
});
it("More… is a plain chip: the ellipsis says a dialog follows, no chevron", () => {
render(<YearPicker years={YEARS} onPick={() => {}} />);
expect(
screen.getByRole("button", { name: "sidebar.year.more" }).querySelector("svg"),
).toBeNull();
});
it("clearing the kept older year hands the focus on, not to <body>", () => {
const { rerender } = render(<YearPicker years={YEARS} selected={1985} onPick={() => {}} />);
const kept = screen.getByRole("button", { pressed: true });
kept.focus();
fireEvent.click(kept);
rerender(<YearPicker years={YEARS} onPick={() => {}} />);
expect(document.activeElement).toBe(screen.getByRole("button", { name: "sidebar.year.more" }));
});
it("a decade row is named once: its caption is not read again", () => {
render(<YearPicker years={YEARS} onPick={() => {}} />);
open();
const row = screen.getAllByRole("group", { name: /sidebar\.year\.decade/ })[0]!;
expect(row.firstElementChild?.getAttribute("aria-hidden")).toBe("true");
});
it("a cell carries its count under the year, compact", () => {
render(<YearPicker years={[...YEARS, { year: 2025, count: 17243 }]} onPick={() => {}} />);
open();
const cell = screen.getByRole("dialog").querySelector<HTMLElement>('[data-year="2025"]')!;
expect(cell.textContent).toBe("202517.2K");
});
it("the arrows walk the cells: along a decade, then down to the next one", () => {
render(<YearPicker years={YEARS} onPick={() => {}} />);
open();
const dialog = screen.getByRole("dialog");
dialog.querySelector<HTMLElement>('[data-year="2020"]')!.focus();
press("ArrowRight");
expect((document.activeElement as HTMLElement).dataset.year).toBe("2021");
press("ArrowDown");
expect((document.activeElement as HTMLElement).dataset.year).toMatch(/^201/);
});
});
/** A keydown where a real one starts: on whatever holds focus, bubbling to the window listener. */
function press(key: string): void {
act(() => {
(document.activeElement ?? document.body).dispatchEvent(
new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }),
);
});
}