1import { describe, expect, it } from "vitest";
2
3import {
4 CODECS,
5 canConvert,
6 convertValue,
7 codec,
8 ValueCodecError,
9 PROPERTY_TYPES,
10} from "./registry.js";
11
12const selectConfig = {
13 options: [
14 { id: "opt-a", name: "A", color: "blue" },
15 { id: "opt-b", name: "B", color: "green" },
16 ],
17};
18
19describe("value codecs", () => {
20 it("covers all 24 property types", () => {
21 expect(PROPERTY_TYPES).toHaveLength(24);
22 for (const t of PROPERTY_TYPES) expect(CODECS[t]).toBeDefined();
23 });
24
25 it("number round-trip + coercion", () => {
26 const c = codec("number");
27 expect(c.fromWire("42")).toBe(42);
28 expect(c.fromWire(3.14)).toBe(3.14);
29 expect(c.fromWire(null)).toBeNull();
30 expect(() => c.fromWire("abc")).toThrow(ValueCodecError);
31 expect(c.toWire(5)).toBe(5);
32 expect(c.display(1234.5, { format: "currency", currency: "USD" })).toBe("$1,234.50");
33 expect(c.display(0.25, { format: "percent" })).toBe("25 %");
34 expect(c.compare(1, 2, {})).toBeLessThan(0);
35 });
36
37 it("select: orphan option ids display empty, sort last", () => {
38 const c = codec("select");
39 expect(c.display("opt-a", selectConfig)).toBe("A");
40 expect(c.display("opt-gone", selectConfig)).toBe("");
41 expect(c.compare("opt-gone", "opt-a", selectConfig)).toBeGreaterThan(0);
42 expect(c.compare("opt-a", "opt-b", selectConfig)).toBeLessThan(0);
43 });
44
45 it("multi_select: array normalization + membership display", () => {
46 const c = codec("multi_select");
47
48
49
50
51
52
53 expect(c.fromWire(["opt-a", "bogus", "opt-b"])).toEqual(["opt-a", "bogus", "opt-b"]);
54 expect(c.fromWire("opt-a")).toEqual(["opt-a"]);
55 expect(c.fromWire(null)).toEqual([]);
56
57 expect(c.display(["opt-a", "bogus", "opt-b"], selectConfig)).toBe("A, B");
58 expect(c.display(["opt-b", "opt-a"], selectConfig)).toBe("B, A");
59 });
60
61 it("date: ISO day-only normalization, ranges ordered", () => {
62 const d = codec("date");
63 expect(d.fromWire("2025-03-01")).toBe("2025-03-01");
64 expect(() => d.fromWire("2025-03-01T10:00:00Z")).toThrow(ValueCodecError);
65 const r = codec("date_range");
66 const v = r.fromWire({ start: "2025-03-05", end: "2025-03-01" });
67 expect(v).toEqual({ start: "2025-03-01", end: "2025-03-05" });
68 expect(
69 r.compare(
70 { start: "2025-01-01", end: "2025-01-02" },
71 { start: "2025-02-01", end: "2025-02-02" },
72 {},
73 ),
74 ).toBeLessThan(0);
75 });
76
77 it("checkbox: missing = false", () => {
78 const c = codec("checkbox");
79 expect(c.fromWire(null)).toBe(false);
80 expect(c.fromWire(undefined)).toBe(false);
81 expect(c.fromWire(true)).toBe(true);
82 });
83
84 it("unique_id display with prefix + padding", () => {
85 const c = codec("unique_id");
86 expect(c.display(7, { prefix: "rec-", padding: 4 })).toBe("rec-0007");
87 });
88
89 it("conversion matrix: exact allowed set", () => {
90 expect(canConvert("text", "rich_text")).toBe(true);
91 expect(canConvert("rich_text", "text")).toBe(true);
92 expect(canConvert("number", "text")).toBe(true);
93 expect(canConvert("text", "number")).toBe(false);
94 expect(canConvert("select", "status")).toBe(false);
95 expect(canConvert("relation", "text")).toBe(false);
96 expect(convertValue("number", "text", 42.5)).toBe("42.5");
97 expect(convertValue("text", "number", "42")).toBeNull();
98 });
99
100 it("files: normalization keeps shape", () => {
101 const c = codec("files");
102 const v = c.fromWire([{ id: "f1", name: "a.png", url: "x", size: 10 }, "junk"]);
103 expect(v).toEqual([{ id: "f1", name: "a.png", url: "x", size: 10 }]);
104 expect(c.display(v, {})).toBe("a.png");
105 });
106});
107