1
2
3
4
5import { describe, expect, it } from "vitest";
6import {
7 between,
8 clamp,
9 comparePositionStrings,
10 fuzzyMatch,
11 positionAfter,
12 positionBefore,
13 positionForInsert,
14 sortByPosition,
15 truncate,
16} from "../utils";
17import { cn } from "../utils";
18
19describe("comparePositionStrings / sortByPosition", () => {
20 it("orders fractional positions lexicographically", () => {
21 expect(comparePositionStrings("a0", "a1")).toBeLessThan(0);
22 expect(comparePositionStrings("a1", "a1")).toBe(0);
23 expect(comparePositionStrings("a1", "a0")).toBeGreaterThan(0);
24
25 expect(comparePositionStrings("a0", "a00")).toBeLessThan(0);
26 expect(comparePositionStrings("a00", "a01")).toBeLessThan(0);
27 expect(comparePositionStrings("a0z", "a1")).toBeLessThan(0);
28 });
29
30 it("sorts a block list by position", () => {
31 const items = [{ position: "a2" }, { position: "a0" }, { position: "a10" }, { position: "a1" }];
32 expect(sortByPosition(items).map((i) => i.position)).toEqual(["a0", "a1", "a10", "a2"]);
33 });
34
35 it("keeps input order stable for equal positions", () => {
36 const items = [
37 { position: "a0", id: 1 },
38 { position: "a0", id: 2 },
39 ];
40 const sorted = sortByPosition(items);
41 expect(sorted.map((i) => i.id)).toEqual([1, 2]);
42 });
43
44 it("treats missing positions as sorting last", () => {
45 const items = [{ position: undefined }, { position: "a1" }, { position: null }];
46 const sorted = sortByPosition(items);
47 expect(sorted.map((i) => i.position)).toEqual(["a1", undefined, null]);
48 });
49});
50
51
52
53
54
55describe("between() — shared engine wrapper", () => {
56 it("inserts strictly between non-adjacent positions", () => {
57 expect(between("a0", "a2")).toBe("a1");
58 });
59
60 it("handles multi-char prefixes", () => {
61 const mid = between("a0b", "a0d");
62 expect(comparePositionStrings("a0b", mid)).toBeLessThan(0);
63 expect(comparePositionStrings(mid, "a0d")).toBeLessThan(0);
64 });
65
66 it("descends past the adjacent char (appends the balanced midpoint)", () => {
67 const mid = between("a0", "a1");
68
69 expect(mid).toBe("a0i");
70 expect(comparePositionStrings("a0", mid)).toBeLessThan(0);
71 expect(comparePositionStrings(mid, "a1")).toBeLessThan(0);
72 });
73
74 it("splits a strict-prefix gap", () => {
75
76 const mid = between("a0", "a05");
77 expect(mid).toBe("a02");
78 expect(comparePositionStrings("a0", mid)).toBeLessThan(0);
79 expect(comparePositionStrings(mid, "a05")).toBeLessThan(0);
80 });
81
82 it("throws for adjacent positions with no room (empty gap)", () => {
83 expect(() => between("a", "a0")).toThrow(/empty gap/);
84 expect(() => between("a0", "a00")).toThrow(/empty gap/);
85 });
86
87 it("rejects a >= b", () => {
88 expect(() => between("b", "a")).toThrow(RangeError);
89 expect(() => between("a", "a")).toThrow(RangeError);
90 });
91
92 it("is stable under repeated halving of the same gap", () => {
93
94
95
96 let lo = "a0";
97 let hi = "a3";
98 for (let k = 0; k < 12; k += 1) {
99 const mid = between(lo, hi);
100 expect(comparePositionStrings(lo, mid)).toBeLessThan(0);
101 expect(comparePositionStrings(mid, hi)).toBeLessThan(0);
102 if (k % 2 === 0) hi = mid;
103 else lo = mid;
104 }
105 });
106});
107
108describe("positionBefore / positionAfter / positionForInsert", () => {
109 it("positionBefore yields a strictly smaller position", () => {
110 expect(positionBefore("a2")).toBe("a1");
111 expect(comparePositionStrings(positionBefore("a2"), "a2")).toBeLessThan(0);
112
113 expect(positionBefore("a10")).toBe("a0");
114 expect(comparePositionStrings(positionBefore("a10"), "a10")).toBeLessThan(0);
115 expect(positionBefore("a102")).toBe("a101");
116 expect(positionBefore("b")).toBe("a");
117
118 expect(positionBefore("0")).toBe("0");
119 });
120
121 it("positionAfter yields a strictly larger position", () => {
122 expect(positionAfter("a0")).toBe("a0i");
123 expect(comparePositionStrings("a0", positionAfter("a0"))).toBeLessThan(0);
124 });
125
126 it("positionForInsert composes neighbors", () => {
127 expect(positionForInsert(null, null)).toBe("a0");
128 expect(comparePositionStrings(positionForInsert(null, "a2"), "a2")).toBeLessThan(0);
129 expect(positionForInsert("a2", null)).toBe("a2i");
130 expect(positionForInsert("a0", "a2")).toBe("a1");
131
132
133 expect(() => positionForInsert("a0", "a00")).toThrow(/empty gap/);
134 });
135});
136
137describe("truncate", () => {
138 it("returns short strings untouched", () => {
139 expect(truncate("hello", 80)).toBe("hello");
140 expect(truncate("exactly8", 8)).toBe("exactly8");
141 });
142
143 it("truncates long strings with an ellipsis", () => {
144 const out = truncate("a".repeat(100), 10);
145 expect(out.length).toBeLessThanOrEqual(10);
146 expect(out.endsWith("…")).toBe(true);
147 expect(out.startsWith("a")).toBe(true);
148 });
149
150 it("treats max <= 0 as empty", () => {
151 expect(truncate("abc", 0)).toBe("");
152 expect(truncate("abc", -5)).toBe("");
153 });
154});
155
156describe("cn", () => {
157 it("merges class names and resolves tailwind conflicts", () => {
158 expect(cn("px-2", "px-4")).toBe("px-4");
159 expect(cn("p-2", undefined, null, false, "m-1")).toBe("p-2 m-1");
160 });
161});
162
163describe("fuzzyMatch", () => {
164 it("matches substrings and subsequences", () => {
165 expect(fuzzyMatch("h1", "Heading 1")).toBe(true);
166 expect(fuzzyMatch("db", "Add database")).toBe(true);
167 expect(fuzzyMatch("xyz", "no match here")).toBe(false);
168 expect(fuzzyMatch("", "anything")).toBe(true);
169 expect(fuzzyMatch(" ", "anything")).toBe(true);
170 });
171});
172
173describe("clamp", () => {
174 it("bounds values", () => {
175 expect(clamp(5, 0, 10)).toBe(5);
176 expect(clamp(-1, 0, 10)).toBe(0);
177 expect(clamp(11, 0, 10)).toBe(10);
178 });
179});
180