import { describe, expect, it } from "vitest"; import { useState } from "react"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { CERTIFICATION_AGENCIES } from "@/lib/api/certifications"; import { COURSE_STATUSES } from "@/lib/api/courses"; import { CoursesFilters, NO_COURSE_FILTERS, type CourseListFilters, type CoursesFiltersProps, } from "./courses-filters"; // The row narrows one query rather than four, so what these pin is that each // control reports its own field and leaves the other three where they were - // the failure that turns "PADI courses in 2025" into "PADI courses". function Row({ initial = NO_COURSE_FILTERS, ...options }: { initial?: CourseListFilters; } & Pick) { const [filters, setFilters] = useState(initial); return ( <> {JSON.stringify(filters)} ); } const asked = () => JSON.parse(screen.getByTestId("asked").textContent ?? "{}") as Record< string, string >; const optionsOf = (label: string) => [...screen.getByLabelText(label).options].map( (option) => option.text, ); const agencyOptions = () => optionsOf("Agency"); const statusOptions = () => optionsOf("Status"); describe("CoursesFilters", () => { it("opens on no filter at all, with every control reachable by its label", () => { render(); expect(screen.getByLabelText("From")).toHaveValue(""); expect(screen.getByLabelText("To")).toHaveValue(""); expect(screen.getByLabelText("Agency")).toHaveValue(""); expect(screen.getByLabelText("Status")).toHaveValue(""); }); it("offers an 'any' option on each select, so a pick can be taken back", () => { render(); expect( screen.getByRole("option", { name: "Any agency" }), ).toBeInTheDocument(); expect( screen.getByRole("option", { name: "Any status" }), ).toBeInTheDocument(); }); it("reports a picked agency and leaves the rest of the row alone", async () => { render(); await userEvent.selectOptions(screen.getByLabelText("Agency"), "tdi"); expect(asked()).toMatchObject({ agency: "tdi", status: "completed" }); }); it("reports a picked status by its wire value, not its label", async () => { render(); await userEvent.selectOptions( screen.getByLabelText("Status"), "in_progress", ); expect(asked().status).toBe("in_progress"); }); it("reports a typed date once the field is left", async () => { render(); await userEvent.type(screen.getByLabelText("From"), "2025-01-01"); await userEvent.tab(); expect(asked().dateFrom).toBe("2025-01-01"); }); it("holds both ends of the window at once", async () => { render(); await userEvent.type(screen.getByLabelText("To"), "2025-12-31"); await userEvent.tab(); expect(asked()).toMatchObject({ dateFrom: "2025-01-01", dateTo: "2025-12-31", }); }); it("empties the end of the window when the start moves past it", async () => { render( , ); await userEvent.clear(screen.getByLabelText("From")); await userEvent.type(screen.getByLabelText("From"), "2025-04-01"); await userEvent.tab(); expect(asked()).toMatchObject({ dateFrom: "2025-04-01", dateTo: "" }); }); it("keeps an end the moved start has not passed", async () => { render( , ); await userEvent.clear(screen.getByLabelText("From")); await userEvent.type(screen.getByLabelText("From"), "2025-03-31"); await userEvent.tab(); expect(asked()).toMatchObject({ dateFrom: "2025-03-31", dateTo: "2025-03-31", }); }); // Clearing the lot in one go belongs to the button that shuts the panel // (`CoursesPageFrame`), so the row itself offers only per-control clears. it("carries no control that clears the row", () => { render(); expect( screen.queryByRole("button", { name: /^Clear filters/ }), ).not.toBeInTheDocument(); }); // Every other agency is a way to empty the list for a diver who trained with // one of them. What the selects offer is the vocabulary the courses themselves // carry, which the page reads off its own list. it("offers only the agencies and statuses it is given", () => { render(); expect(agencyOptions()).toEqual(["Any agency", "PADI", "SDI"]); expect(statusOptions()).toEqual(["Any status", "Completed"]); }); it("falls back to the whole vocabulary when it is given none", () => { render(); expect(agencyOptions()).toHaveLength(CERTIFICATION_AGENCIES.length + 1); expect(statusOptions()).toHaveLength(COURSE_STATUSES.length + 1); }); // The last PADI course deleted while the filter still says PADI. Dropping the // option would blank the select while the list stayed narrowed by it. it("keeps a picked value that is no longer in use", () => { render( , ); expect(screen.getByLabelText("Agency")).toHaveValue("padi"); expect(agencyOptions()).toEqual(["Any agency", "SDI", "PADI"]); }); });