import { describe, it, expect } from "vitest"; import { type CollapseAccessors, collapsibleKeys, parseStoredExpanded, serializeExpanded, visibleRows, } from "./collapsibleRows"; /** Tiny hierarchy row for the tests: key, depth, is-parent. */ interface Row { k: string; d: number; p: boolean; } const acc: CollapseAccessors = { keyOf: (r) => r.k, depthOf: (r) => r.d, isParent: (r) => r.p, }; const row = (k: string, d: number, p: boolean): Row => ({ k, d, p }); // A two-group tree with one nested sub-group: // Housing (parent, d0) // Rent (leaf, d1) // Utilities (parent, d1) <- intermediate parent // Power (leaf, d2) // Salary (parent, d0) // Paycheck (leaf, d1) // Misc (leaf, d0) <- top-level leaf, not a group const tree: Row[] = [ row("housing", 0, true), row("rent", 1, false), row("utilities", 1, true), row("power", 2, false), row("salary", 0, true), row("paycheck", 1, false), row("misc", 0, false), ]; // Default = collapsed: a group is collapsed unless its key is in the expanded set. const isCollapsed = (expanded: Set) => (r: Row) => !expanded.has(r.k); describe("collapsibleRows.visibleRows", () => { it("collapses every group by default (empty expanded set)", () => { const visible = visibleRows(tree, acc, isCollapsed(new Set())); // Only depth-0 rows survive: the two group headers + the top-level leaf. expect(visible.map((r) => r.k)).toEqual(["housing", "salary", "misc"]); }); it("reveals a group's full subtree (incl. nested sub-groups) when expanded", () => { const visible = visibleRows(tree, acc, isCollapsed(new Set(["housing"]))); expect(visible.map((r) => r.k)).toEqual([ "housing", "rent", "utilities", "power", "salary", "misc", ]); }); it("keeps groups independent — expanding one leaves the others collapsed", () => { const visible = visibleRows(tree, acc, isCollapsed(new Set(["salary"]))); expect(visible.map((r) => r.k)).toEqual(["housing", "salary", "paycheck", "misc"]); }); it("shows everything when all groups are expanded", () => { const visible = visibleRows(tree, acc, isCollapsed(new Set(["housing", "salary"]))); expect(visible).toHaveLength(tree.length); }); it("always keeps a top-level leaf that has no group", () => { const visible = visibleRows(tree, acc, isCollapsed(new Set())); expect(visible.some((r) => r.k === "misc")).toBe(true); }); }); describe("collapsibleRows.collapsibleKeys", () => { it("lists only top-level parent rows, in order", () => { expect(collapsibleKeys(tree, acc)).toEqual(["housing", "salary"]); }); it("returns [] when there is no hierarchy", () => { const flat = [row("a", 0, false), row("b", 0, false)]; expect(collapsibleKeys(flat, acc)).toEqual([]); }); }); describe("collapsibleRows.parseStoredExpanded / serializeExpanded", () => { it("treats absent storage as the collapsed default (empty set)", () => { expect(parseStoredExpanded(null).size).toBe(0); expect(parseStoredExpanded("").size).toBe(0); }); it("parses a JSON string array back into a set", () => { expect([...parseStoredExpanded('["housing","salary"]')].sort()).toEqual(["housing", "salary"]); }); it("falls back to empty on corrupt or non-array JSON", () => { expect(parseStoredExpanded("{not json").size).toBe(0); expect(parseStoredExpanded('{"a":1}').size).toBe(0); expect(parseStoredExpanded("42").size).toBe(0); }); it("drops non-string members defensively", () => { expect([...parseStoredExpanded('["ok", 3, null, "yes"]')].sort()).toEqual(["ok", "yes"]); }); it("round-trips through serialize", () => { const set = new Set(["a", "b"]); expect([...parseStoredExpanded(serializeExpanded(set))].sort()).toEqual(["a", "b"]); }); });