// @vitest-environment jsdom // // The rendering integration of SortControl (its pickSort/heal decision logic is covered in // columnSort.test.ts). Labels are plain props here — no i18n needed. It's a floating-ui popover now // (a labeled pill + a menu of `menuitemradio` sort keys with a footer direction segment), so we stub // the two autoUpdate observers and wrap in a FloatingTree; rows only exist while open → role queries // pass { hidden: true }. import { describe, it, expect, vi, afterEach, beforeAll } from "vitest"; import { render, screen, fireEvent, cleanup } from "@testing-library/react"; import { FloatingTree } from "@floating-ui/react"; import SortControl from "./SortControl"; import type { SortOption, SortState } from "../lib/columnSort"; beforeAll(() => { class Stub { observe() {} unobserve() {} disconnect() {} takeRecords() { return []; } } globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver; globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver; }); const OPTS: SortOption[] = [ { key: "date", label: "Date", defaultDir: "desc" }, { key: "title", label: "Title" }, { key: "shuffle", label: "Shuffle", directionless: true }, ]; function renderControl(sort: SortState, extra: Record = {}) { const onChange = vi.fn(); render( , ); return { onChange }; } const openPanel = () => fireEvent.click(screen.getByRole("button", { name: /^Sort/ })); afterEach(cleanup); describe("SortControl", () => { it("renders one option per SortOption, no no-sort row without defaultLabel", () => { renderControl({ key: "title", dir: "asc" }); openPanel(); expect( screen.getAllByRole("menuitemradio", { hidden: true }).map((o) => o.textContent), ).toEqual(["Date", "Title", "Shuffle"]); }); it("adds a leading no-sort option when defaultLabel is set (tables)", () => { renderControl(null, { defaultLabel: "None" }); openPanel(); expect( screen.getAllByRole("menuitemradio", { hidden: true }).map((o) => o.textContent), ).toEqual(["None", "Date", "Title", "Shuffle"]); }); it("summarises the trigger with the first option (not blank) for an unavailable key without defaultLabel", () => { renderControl({ key: "ghost", dir: "asc" }); expect(screen.getByRole("button", { name: /^Sort/ }).textContent).toContain("Date"); }); it("picking a new option fires onChange at that option's defaultDir", () => { const { onChange } = renderControl({ key: "title", dir: "asc" }); openPanel(); fireEvent.click(screen.getAllByRole("menuitemradio", { hidden: true })[0]!); // Date expect(onChange).toHaveBeenCalledWith({ key: "date", dir: "desc" }); }); it("shows the direction segment for a directional sort and sets asc on click", () => { const { onChange } = renderControl({ key: "date", dir: "desc" }); openPanel(); fireEvent.click(screen.getByText("Ascending")); expect(onChange).toHaveBeenCalledWith({ key: "date", dir: "asc" }); }); it("hides the direction segment for a directionless sort", () => { renderControl({ key: "shuffle", dir: "desc" }); openPanel(); expect(screen.queryByText("Ascending")).toBeNull(); expect(screen.queryByText("Descending")).toBeNull(); }); it("renders the trailing adornment in the footer", () => { renderControl({ key: "shuffle", dir: "desc" }, { trailing: }); openPanel(); expect(screen.getByText("Reshuffle")).toBeTruthy(); }); });