// @vitest-environment node // No DOM needed — jsdom is not installed in this workspace, so the web // default environment (jsdom, vitest.config.ts) is unreachable; node-env // tests are bannered, matching realtime-frame-handler.test.ts. import { describe, expect, it } from "vitest"; import { between, clamp, comparePositionStrings, fuzzyMatch, positionAfter, positionBefore, positionForInsert, sortByPosition, truncate, } from "../utils"; import { cn } from "../utils"; describe("comparePositionStrings / sortByPosition", () => { it("orders fractional positions lexicographically", () => { expect(comparePositionStrings("a0", "a1")).toBeLessThan(0); expect(comparePositionStrings("a1", "a1")).toBe(0); expect(comparePositionStrings("a1", "a0")).toBeGreaterThan(0); // Longer prefix sorts after its own prefix: "a00" > "a0". expect(comparePositionStrings("a0", "a00")).toBeLessThan(0); expect(comparePositionStrings("a00", "a01")).toBeLessThan(0); expect(comparePositionStrings("a0z", "a1")).toBeLessThan(0); }); it("sorts a block list by position", () => { const items = [{ position: "a2" }, { position: "a0" }, { position: "a10" }, { position: "a1" }]; expect(sortByPosition(items).map((i) => i.position)).toEqual(["a0", "a1", "a10", "a2"]); }); it("keeps input order stable for equal positions", () => { const items = [ { position: "a0", id: 1 }, { position: "a0", id: 2 }, ]; const sorted = sortByPosition(items); expect(sorted.map((i) => i.id)).toEqual([1, 2]); }); it("treats missing positions as sorting last", () => { const items = [{ position: undefined }, { position: "a1" }, { position: null }]; const sorted = sortByPosition(items); expect(sorted.map((i) => i.position)).toEqual(["a1", undefined, null]); }); }); // Position ORDER assertions use comparePositionStrings (the engine's own // comparison) because vitest's toBeGreaterThan/toBeLessThan are // number-only — position keys are strings compared lexicographically with // the prefix rule. describe("between() — shared engine wrapper", () => { it("inserts strictly between non-adjacent positions", () => { expect(between("a0", "a2")).toBe("a1"); }); it("handles multi-char prefixes", () => { const mid = between("a0b", "a0d"); expect(comparePositionStrings("a0b", mid)).toBeLessThan(0); expect(comparePositionStrings(mid, "a0d")).toBeLessThan(0); }); it("descends past the adjacent char (appends the balanced midpoint)", () => { const mid = between("a0", "a1"); // base-36 alphabet: the midpoint char is 'i' (18 of 36), not base-32 'p'. expect(mid).toBe("a0i"); expect(comparePositionStrings("a0", mid)).toBeLessThan(0); expect(comparePositionStrings(mid, "a1")).toBeLessThan(0); }); it("splits a strict-prefix gap", () => { // a is a prefix of b: room between a and a + bTail. const mid = between("a0", "a05"); expect(mid).toBe("a02"); expect(comparePositionStrings("a0", mid)).toBeLessThan(0); expect(comparePositionStrings(mid, "a05")).toBeLessThan(0); }); it("throws for adjacent positions with no room (empty gap)", () => { expect(() => between("a", "a0")).toThrow(/empty gap/); expect(() => between("a0", "a00")).toThrow(/empty gap/); }); it("rejects a >= b", () => { expect(() => between("b", "a")).toThrow(RangeError); expect(() => between("a", "a")).toThrow(RangeError); }); it("is stable under repeated halving of the same gap", () => { // Alternate halving from both ends (the strategy the API uses for many // inserts between the same two neighbors) — single-side descent would // terminate at the empty gap (the rebalance signal). let lo = "a0"; let hi = "a3"; for (let k = 0; k < 12; k += 1) { const mid = between(lo, hi); expect(comparePositionStrings(lo, mid)).toBeLessThan(0); expect(comparePositionStrings(mid, hi)).toBeLessThan(0); if (k % 2 === 0) hi = mid; else lo = mid; } }); }); describe("positionBefore / positionAfter / positionForInsert", () => { it("positionBefore yields a strictly smaller position", () => { expect(positionBefore("a2")).toBe("a1"); expect(comparePositionStrings(positionBefore("a2"), "a2")).toBeLessThan(0); // Trailing zeros: drop, then decrement the new last char. expect(positionBefore("a10")).toBe("a0"); expect(comparePositionStrings(positionBefore("a10"), "a10")).toBeLessThan(0); expect(positionBefore("a102")).toBe("a101"); expect(positionBefore("b")).toBe("a"); // Bottom of the range: no smaller position — fall back to p. expect(positionBefore("0")).toBe("0"); }); it("positionAfter yields a strictly larger position", () => { expect(positionAfter("a0")).toBe("a0i"); expect(comparePositionStrings("a0", positionAfter("a0"))).toBeLessThan(0); }); it("positionForInsert composes neighbors", () => { expect(positionForInsert(null, null)).toBe("a0"); expect(comparePositionStrings(positionForInsert(null, "a2"), "a2")).toBeLessThan(0); expect(positionForInsert("a2", null)).toBe("a2i"); expect(positionForInsert("a0", "a2")).toBe("a1"); // Adjacent neighbors with an empty gap propagate the shared engine's // rebalance signal — the caller re-spaces via the rebalance endpoint. expect(() => positionForInsert("a0", "a00")).toThrow(/empty gap/); }); }); describe("truncate", () => { it("returns short strings untouched", () => { expect(truncate("hello", 80)).toBe("hello"); expect(truncate("exactly8", 8)).toBe("exactly8"); }); it("truncates long strings with an ellipsis", () => { const out = truncate("a".repeat(100), 10); expect(out.length).toBeLessThanOrEqual(10); expect(out.endsWith("…")).toBe(true); expect(out.startsWith("a")).toBe(true); }); it("treats max <= 0 as empty", () => { expect(truncate("abc", 0)).toBe(""); expect(truncate("abc", -5)).toBe(""); }); }); describe("cn", () => { it("merges class names and resolves tailwind conflicts", () => { expect(cn("px-2", "px-4")).toBe("px-4"); expect(cn("p-2", undefined, null, false, "m-1")).toBe("p-2 m-1"); }); }); describe("fuzzyMatch", () => { it("matches substrings and subsequences", () => { expect(fuzzyMatch("h1", "Heading 1")).toBe(true); expect(fuzzyMatch("db", "Add database")).toBe(true); expect(fuzzyMatch("xyz", "no match here")).toBe(false); expect(fuzzyMatch("", "anything")).toBe(true); expect(fuzzyMatch(" ", "anything")).toBe(true); }); }); describe("clamp", () => { it("bounds values", () => { expect(clamp(5, 0, 10)).toBe(5); expect(clamp(-1, 0, 10)).toBe(0); expect(clamp(11, 0, 10)).toBe(10); }); });