import { describe, expect, it } from "vitest"; import { sortRows, parseSortState, cycleSort, pickSort, healSort, hydrateSort, type SortOption, type SortState, } from "./columnSort"; interface Row { name: string; views: number; } const rows: Row[] = [ { name: "beta", views: 30 }, { name: "alpha", views: 100 }, { name: "gamma", views: 5 }, ]; const columns = [ { key: "name", sortValue: (r: Row) => r.name }, { key: "views", sortValue: (r: Row) => r.views }, { key: "nosort" }, // a column with no sortValue ]; describe("sortRows", () => { it("returns the same array reference when there is no sort", () => { expect(sortRows(rows, columns, null)).toBe(rows); }); it("returns the input unchanged for a column that has no sortValue", () => { expect(sortRows(rows, columns, { key: "nosort", dir: "asc" })).toBe(rows); }); it("sorts numbers by numeric difference, not string order", () => { // string order would put "100" before "30"; numeric must not. const asc = sortRows(rows, columns, { key: "views", dir: "asc" }).map((r) => r.views); expect(asc).toEqual([5, 30, 100]); const desc = sortRows(rows, columns, { key: "views", dir: "desc" }).map((r) => r.views); expect(desc).toEqual([100, 30, 5]); }); it("sorts strings with locale compare, ascending and descending", () => { expect(sortRows(rows, columns, { key: "name", dir: "asc" }).map((r) => r.name)).toEqual([ "alpha", "beta", "gamma", ]); expect(sortRows(rows, columns, { key: "name", dir: "desc" }).map((r) => r.name)).toEqual([ "gamma", "beta", "alpha", ]); }); it("does not mutate the input array", () => { const before = rows.map((r) => r.name); sortRows(rows, columns, { key: "name", dir: "asc" }); expect(rows.map((r) => r.name)).toEqual(before); }); }); describe("parseSortState", () => { it("accepts a well-formed state", () => { expect(parseSortState({ key: "views", dir: "desc" })).toEqual({ key: "views", dir: "desc" }); }); it.each([ ["null", null], ["a string", "views"], ["missing dir", { key: "views" }], ["a bad dir", { key: "views", dir: "sideways" }], ["a non-string key", { key: 3, dir: "asc" }], ])("coerces %s to null rather than passing junk to the comparator", (_label, raw) => { expect(parseSortState(raw)).toBeNull(); }); }); describe("cycleSort", () => { it("walks unsorted → asc → desc → unsorted for the same key", () => { let s: SortState = null; s = cycleSort(s, "views"); expect(s).toEqual({ key: "views", dir: "asc" }); s = cycleSort(s, "views"); expect(s).toEqual({ key: "views", dir: "desc" }); s = cycleSort(s, "views"); expect(s).toBeNull(); }); it("jumps straight to ascending when a different column is clicked", () => { expect(cycleSort({ key: "name", dir: "desc" }, "views")).toEqual({ key: "views", dir: "asc" }); }); }); const OPTIONS: SortOption[] = [ { key: "date", label: "Date", defaultDir: "desc" }, { key: "title", label: "Title" }, // defaultDir omitted → asc { key: "shuffle", label: "Shuffle", directionless: true }, ]; describe("pickSort", () => { it("starts a freshly-picked option at its defaultDir (asc fallback)", () => { expect(pickSort(OPTIONS, null, "date")).toEqual({ key: "date", dir: "desc" }); expect(pickSort(OPTIONS, null, "title")).toEqual({ key: "title", dir: "asc" }); }); it("keeps the current direction when re-picking the same key", () => { expect(pickSort(OPTIONS, { key: "date", dir: "asc" }, "date")).toEqual({ key: "date", dir: "asc", }); }); it("carries a stable dir for a directionless option and never shows a flip", () => { expect(pickSort(OPTIONS, { key: "date", dir: "asc" }, "shuffle")).toEqual({ key: "shuffle", dir: "desc", }); }); it("clears to null on an empty key or an unknown option", () => { expect(pickSort(OPTIONS, { key: "date", dir: "desc" }, "")).toBeNull(); expect(pickSort(OPTIONS, { key: "date", dir: "desc" }, "ghost")).toBeNull(); }); }); describe("healSort", () => { it("keeps a sort whose key is still offered", () => { const s: SortState = { key: "title", dir: "asc" }; expect(healSort(s, OPTIONS)).toBe(s); }); it("collapses a sort naming an absent option to the fallback", () => { expect(healSort({ key: "gone", dir: "asc" }, OPTIONS)).toBeNull(); expect(healSort({ key: "gone", dir: "asc" }, OPTIONS, { key: "date", dir: "desc" })).toEqual({ key: "date", dir: "desc", }); }); }); describe("hydrateSort", () => { it("returns the fallback for missing storage", () => { expect(hydrateSort(null, OPTIONS)).toBeNull(); expect(hydrateSort(null, OPTIONS, { key: "date", dir: "desc" })).toEqual({ key: "date", dir: "desc", }); }); it("parses and heals a stored JSON state", () => { expect(hydrateSort(JSON.stringify({ key: "title", dir: "asc" }), OPTIONS)).toEqual({ key: "title", dir: "asc", }); expect(hydrateSort(JSON.stringify({ key: "gone", dir: "asc" }), OPTIONS)).toBeNull(); }); it("tolerates a legacy bare-key string, applying the option's defaultDir", () => { expect(hydrateSort("date", OPTIONS)).toEqual({ key: "date", dir: "desc" }); expect(hydrateSort("title", OPTIONS)).toEqual({ key: "title", dir: "asc" }); }); it("falls back when a legacy bare-key names an unknown option", () => { expect(hydrateSort("ghost", OPTIONS, { key: "date", dir: "desc" })).toEqual({ key: "date", dir: "desc", }); }); it("parses without healing when no options are supplied (caller heals for display)", () => { // A table/provider that owns no vocabulary keeps whatever was stored, even an unknown key. expect(hydrateSort(JSON.stringify({ key: "anything", dir: "desc" }), [])).toEqual({ key: "anything", dir: "desc", }); // A malformed stored value still coerces to the fallback. expect(hydrateSort(JSON.stringify({ key: 3, dir: "up" }), [], null)).toBeNull(); }); });