fix(sort): address R13 S1 review findings
- PlexSidebar activeCount now compares the HEALED sort, so an out-of-scope key (movie "duration" after switching to shows) no longer inflates the active-filter badge while the control + grid show the default. - PlexProvider strips the legacy `sortDir` from the filters blob, so it stops leaking into the facets query key and being re-persisted on every change. - PlexProvider sort migration guards the fall-through: valid JSON that isn't a SortState now falls back instead of using the raw JSON string as the key. - New shared healPlexSort(sort, scope) is the single heal both PlexBrowse and PlexSidebar use (removes the noop-label plexSortOptions call + the duplication). - Tests: useSortState.dom.test.ts (hydrate/persist/heal round-trip via renderHook) and SortControl.dom.test.tsx (options, pick, direction toggle, directionless, trailing). vitest 94 -> 105. Gate green: tsc/eslint(0 err)/prettier/knip clean, vitest 105.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
// @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.
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
||||
import SortControl from "./SortControl";
|
||||
import type { SortOption, SortState } from "../lib/columnSort";
|
||||
|
||||
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<string, unknown> = {}) {
|
||||
const onChange = vi.fn();
|
||||
render(
|
||||
<SortControl
|
||||
options={OPTS}
|
||||
sort={sort}
|
||||
onChange={onChange}
|
||||
label="Sort"
|
||||
ascLabel="Ascending"
|
||||
descLabel="Descending"
|
||||
{...extra}
|
||||
/>,
|
||||
);
|
||||
return { onChange };
|
||||
}
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe("SortControl", () => {
|
||||
it("renders one option per SortOption, no empty option without defaultLabel", () => {
|
||||
renderControl({ key: "title", dir: "asc" });
|
||||
expect(screen.getAllByRole("option").map((o) => o.textContent)).toEqual([
|
||||
"Date",
|
||||
"Title",
|
||||
"Shuffle",
|
||||
]);
|
||||
});
|
||||
|
||||
it("adds a leading no-sort option when defaultLabel is set (tables)", () => {
|
||||
renderControl(null, { defaultLabel: "None" });
|
||||
const none = screen.getByRole("option", { name: "None" }) as HTMLOptionElement;
|
||||
expect(none.value).toBe("");
|
||||
});
|
||||
|
||||
it("picking a new option fires onChange at that option's defaultDir", () => {
|
||||
const { onChange } = renderControl({ key: "title", dir: "asc" });
|
||||
fireEvent.change(screen.getByRole("combobox"), { target: { value: "date" } });
|
||||
expect(onChange).toHaveBeenCalledWith({ key: "date", dir: "desc" });
|
||||
});
|
||||
|
||||
it("shows the direction toggle for a directional sort and flips it on click", () => {
|
||||
const { onChange } = renderControl({ key: "date", dir: "desc" });
|
||||
const toggle = screen.getByRole("button", { name: "Descending" });
|
||||
fireEvent.click(toggle);
|
||||
expect(onChange).toHaveBeenCalledWith({ key: "date", dir: "asc" });
|
||||
});
|
||||
|
||||
it("hides the direction toggle for a directionless sort", () => {
|
||||
renderControl({ key: "shuffle", dir: "desc" });
|
||||
expect(screen.queryByRole("button")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders the trailing adornment", () => {
|
||||
renderControl(
|
||||
{ key: "shuffle", dir: "desc" },
|
||||
{
|
||||
trailing: <button>Reshuffle</button>,
|
||||
},
|
||||
);
|
||||
expect(screen.getByRole("button", { name: "Reshuffle" })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user