1
2
3
4
5import { describe, expect, it, vi } from "vitest";
6
7vi.mock("@/lib/api", () => ({
8 apiFetch: vi.fn(async () => ({ data: [] })),
9 blockApi: {},
10}));
11
12import {
13 findOption,
14 normalizePropertyType,
15 pageUrlFromPath,
16 valueToText
17} from "@/hooks/use-database";
18import type { DatabaseProperty, SelectOption } from "@/hooks/use-database";
19
20const OPTIONS: SelectOption[] = [
21 { id: "opt-to-do", name: "To Do", color: "default" },
22 { id: "opt-in-progress", name: "In Progress", color: "blue" },
23 { id: "opt-done", name: "Done", color: "green" }
24];
25
26const statusProp: DatabaseProperty = {
27 id: "prop_status",
28 databaseId: "db1",
29 name: "Status",
30 type: "status",
31 position: "a1",
32 isTitle: false,
33 isVisible: true,
34 required: false,
35 config: { options: OPTIONS },
36 default: null,
37 createdAt: new Date().toISOString(),
38 updatedAt: new Date().toISOString()
39} as DatabaseProperty;
40
41describe("findOption (tolerant id-or-name option lookup)", () => {
42 it("matches by id (the canonical contract)", () => {
43 expect(findOption(OPTIONS, "opt-done")?.name).toBe("Done");
44 });
45
46 it("falls back to NAME for a legacy stored option name", () => {
47 expect(findOption(OPTIONS, "To Do")?.id).toBe("opt-to-do");
48 expect(findOption(OPTIONS, "In Progress")?.id).toBe("opt-in-progress");
49 });
50
51 it("returns undefined for unknown refs, empty, non-string, or no options", () => {
52 expect(findOption(OPTIONS, "Unknown")).toBeUndefined();
53 expect(findOption(OPTIONS, "")).toBeUndefined();
54 expect(findOption(OPTIONS, null)).toBeUndefined();
55 expect(findOption(OPTIONS, 42)).toBeUndefined();
56 expect(findOption(undefined, "opt-done")).toBeUndefined();
57 expect(findOption(null, "To Do")).toBeUndefined();
58 });
59});
60
61describe("pageUrlFromPath (the only page route is /w/[workspace]/page/[id])", () => {
62 it("builds the workspace-scoped page URL from the second path segment", () => {
63 expect(pageUrlFromPath("/w/personal-fep9/page/abc", "page123")).toBe(
64 "/w/personal-fep9/page/page123"
65 );
66
67
68 expect(pageUrlFromPath("/w/default/page/cmuj/page2", "page2")).toBe(
69 "/w/default/page/page2"
70 );
71 });
72
73 it("falls back to the workspace-less URL when the slug cannot be read", () => {
74 expect(pageUrlFromPath("/page/abc", "page123")).toBe("/page/page123");
75 expect(pageUrlFromPath("/", "page123")).toBe("/page/page123");
76 expect(pageUrlFromPath(null, "page123")).toBe("/page/page123");
77 expect(pageUrlFromPath(undefined, "page123")).toBe("/page/page123");
78 });
79
80 it("does NOT mistake the literal 'w' segment for the slug (the reported 404)", () => {
81
82 expect(pageUrlFromPath("/w/personal-fep9/page/abc", "x")).not.toBe("/w/w/page/x");
83 expect(pageUrlFromPath("/w/personal-fep9/page/abc", "x")).not.toContain("/p/");
84 });
85});
86
87describe("normalizePropertyType (read-boundary option-id backfill — the fix)", () => {
88
89
90
91
92 const legacyProp: DatabaseProperty = {
93 id: "prop_status",
94 databaseId: "db1",
95 name: "Status",
96 type: "STATUS",
97 position: "a1",
98 isTitle: false,
99 isVisible: true,
100 required: false,
101 config: {
102 options: [
103 { id: "", name: "To Do", color: "default" },
104 { id: "", name: "In Progress", color: "blue" },
105 { id: "", name: "Done", color: "green" }
106 ] as SelectOption[]
107 },
108 default: null,
109 createdAt: new Date().toISOString(),
110 updatedAt: new Date().toISOString()
111 } as unknown as DatabaseProperty;
112
113 it("backfills deterministic option ids AND lowercases the wire type", () => {
114 const out = normalizePropertyType(legacyProp);
115 expect(out.type).toBe("status");
116 expect(out.config.options?.map((o) => o.id)).toEqual([
117 "opt-to-do",
118 "opt-in-progress",
119 "opt-done"
120 ]);
121 });
122
123 it("a canonical stored optionId RESOLVES after normalization (no blank cell)", () => {
124 const out = normalizePropertyType(legacyProp);
125
126 expect(findOption(out.config.options, "opt-in-progress")?.name).toBe("In Progress");
127 expect(valueToText({ optionId: "opt-in-progress" } as never, out)).toBe("In Progress");
128 expect(valueToText({ optionId: "opt-done" } as never, out)).toBe("Done");
129 });
130
131 it("a legacy stored option NAME still resolves (double safety)", () => {
132 const out = normalizePropertyType(legacyProp);
133 expect(findOption(out.config.options, "To Do")?.name).toBe("To Do");
134 });
135
136 it("is referentially stable for an already-conformant (id'd, lowercase) property", () => {
137 const good: DatabaseProperty = {
138 ...statusProp,
139 config: {
140 options: [
141 { id: "opt-to-do", name: "To Do", color: "default" },
142 { id: "opt-in-progress", name: "In Progress", color: "blue" },
143 { id: "opt-done", name: "Done", color: "green" }
144 ]
145 }
146 } as DatabaseProperty;
147 expect(normalizePropertyType(good)).toBe(good);
148 });
149
150 it("does NOT mutate the input property", () => {
151 const snapshot = legacyProp.config.options?.[1];
152 normalizePropertyType(legacyProp);
153 expect(snapshot).toEqual({ id: "", name: "In Progress", color: "blue" });
154 });
155
156 it("leaves non-select configs untouched", () => {
157 const numProp = {
158 ...legacyProp,
159 type: "NUMBER",
160 config: { format: "dollar" }
161 } as unknown as DatabaseProperty;
162 const out = normalizePropertyType(numProp);
163 expect(out.type).toBe("number");
164 expect(out.config.options).toBeUndefined();
165 });
166});
167
168describe("valueToText (option values render the option NAME)", () => {
169 it("resolves a canonical option id to the name", () => {
170 expect(valueToText({ optionId: "opt-done" } as never, statusProp)).toBe("Done");
171 });
172
173 it("resolves a legacy option NAME to the name (no blank cell)", () => {
174 expect(valueToText({ optionId: "To Do" } as never, statusProp)).toBe("To Do");
175 });
176
177 it("resolves multi_select by id and by legacy name", () => {
178 expect(valueToText({ optionIds: ["opt-to-do", "opt-done"] } as never, statusProp)).toBe(
179 "To Do, Done"
180 );
181 expect(valueToText({ optionIds: ["To Do", "Done"] } as never, statusProp)).toBe("To Do, Done");
182 });
183});
184