@player-tools/fluent 0.12.1--canary.241.6077

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/cjs/index.cjs +2396 -0
  2. package/dist/cjs/index.cjs.map +1 -0
  3. package/dist/index.legacy-esm.js +2276 -0
  4. package/dist/index.mjs +2276 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/package.json +38 -0
  7. package/src/core/base-builder/__tests__/fluent-builder-base.test.ts +2423 -0
  8. package/src/core/base-builder/__tests__/fluent-partial.test.ts +179 -0
  9. package/src/core/base-builder/__tests__/id-generator.test.ts +658 -0
  10. package/src/core/base-builder/__tests__/registry.test.ts +534 -0
  11. package/src/core/base-builder/__tests__/resolution-mixed-arrays.test.ts +319 -0
  12. package/src/core/base-builder/__tests__/resolution-pipeline.test.ts +416 -0
  13. package/src/core/base-builder/__tests__/resolution-switches.test.ts +468 -0
  14. package/src/core/base-builder/__tests__/resolution-templates.test.ts +255 -0
  15. package/src/core/base-builder/__tests__/switch.test.ts +815 -0
  16. package/src/core/base-builder/__tests__/template.test.ts +596 -0
  17. package/src/core/base-builder/__tests__/value-extraction.test.ts +200 -0
  18. package/src/core/base-builder/__tests__/value-storage.test.ts +459 -0
  19. package/src/core/base-builder/conditional/index.ts +64 -0
  20. package/src/core/base-builder/context.ts +152 -0
  21. package/src/core/base-builder/errors.ts +69 -0
  22. package/src/core/base-builder/fluent-builder-base.ts +308 -0
  23. package/src/core/base-builder/guards.ts +137 -0
  24. package/src/core/base-builder/id/generator.ts +290 -0
  25. package/src/core/base-builder/id/registry.ts +152 -0
  26. package/src/core/base-builder/index.ts +72 -0
  27. package/src/core/base-builder/resolution/path-resolver.ts +116 -0
  28. package/src/core/base-builder/resolution/pipeline.ts +103 -0
  29. package/src/core/base-builder/resolution/steps/__tests__/nested-asset-wrappers.test.ts +206 -0
  30. package/src/core/base-builder/resolution/steps/asset-id.ts +77 -0
  31. package/src/core/base-builder/resolution/steps/asset-wrappers.ts +64 -0
  32. package/src/core/base-builder/resolution/steps/builders.ts +84 -0
  33. package/src/core/base-builder/resolution/steps/mixed-arrays.ts +95 -0
  34. package/src/core/base-builder/resolution/steps/nested-asset-wrappers.ts +124 -0
  35. package/src/core/base-builder/resolution/steps/static-values.ts +35 -0
  36. package/src/core/base-builder/resolution/steps/switches.ts +71 -0
  37. package/src/core/base-builder/resolution/steps/templates.ts +40 -0
  38. package/src/core/base-builder/resolution/value-resolver.ts +333 -0
  39. package/src/core/base-builder/storage/auxiliary-storage.ts +82 -0
  40. package/src/core/base-builder/storage/value-storage.ts +282 -0
  41. package/src/core/base-builder/types.ts +266 -0
  42. package/src/core/base-builder/utils.ts +10 -0
  43. package/src/core/flow/__tests__/index.test.ts +292 -0
  44. package/src/core/flow/index.ts +118 -0
  45. package/src/core/index.ts +8 -0
  46. package/src/core/mocks/generated/action.builder.ts +92 -0
  47. package/src/core/mocks/generated/choice-item.builder.ts +120 -0
  48. package/src/core/mocks/generated/choice.builder.ts +134 -0
  49. package/src/core/mocks/generated/collection.builder.ts +93 -0
  50. package/src/core/mocks/generated/field-collection.builder.ts +86 -0
  51. package/src/core/mocks/generated/index.ts +10 -0
  52. package/src/core/mocks/generated/info.builder.ts +64 -0
  53. package/src/core/mocks/generated/input.builder.ts +63 -0
  54. package/src/core/mocks/generated/overview-collection.builder.ts +65 -0
  55. package/src/core/mocks/generated/splash-collection.builder.ts +93 -0
  56. package/src/core/mocks/generated/text.builder.ts +47 -0
  57. package/src/core/mocks/index.ts +1 -0
  58. package/src/core/mocks/types/action.ts +92 -0
  59. package/src/core/mocks/types/choice.ts +129 -0
  60. package/src/core/mocks/types/collection.ts +140 -0
  61. package/src/core/mocks/types/info.ts +7 -0
  62. package/src/core/mocks/types/input.ts +7 -0
  63. package/src/core/mocks/types/text.ts +5 -0
  64. package/src/core/schema/__tests__/index.test.ts +127 -0
  65. package/src/core/schema/index.ts +195 -0
  66. package/src/core/schema/types.ts +7 -0
  67. package/src/core/switch/__tests__/index.test.ts +156 -0
  68. package/src/core/switch/index.ts +81 -0
  69. package/src/core/tagged-template/README.md +448 -0
  70. package/src/core/tagged-template/__tests__/extract-bindings-from-schema.test.ts +207 -0
  71. package/src/core/tagged-template/__tests__/index.test.ts +190 -0
  72. package/src/core/tagged-template/__tests__/schema-std-integration.test.ts +580 -0
  73. package/src/core/tagged-template/binding.ts +95 -0
  74. package/src/core/tagged-template/expression.ts +92 -0
  75. package/src/core/tagged-template/extract-bindings-from-schema.ts +120 -0
  76. package/src/core/tagged-template/index.ts +5 -0
  77. package/src/core/tagged-template/std.ts +472 -0
  78. package/src/core/tagged-template/types.ts +123 -0
  79. package/src/core/template/__tests__/index.test.ts +380 -0
  80. package/src/core/template/index.ts +196 -0
  81. package/src/core/utils/index.ts +160 -0
  82. package/src/fp/README.md +411 -0
  83. package/src/fp/__tests__/index.test.ts +1178 -0
  84. package/src/fp/index.ts +386 -0
  85. package/src/gen/common.ts +15 -0
  86. package/src/index.ts +5 -0
  87. package/src/types.ts +203 -0
  88. package/types/core/base-builder/conditional/index.d.ts +21 -0
  89. package/types/core/base-builder/context.d.ts +39 -0
  90. package/types/core/base-builder/errors.d.ts +45 -0
  91. package/types/core/base-builder/fluent-builder-base.d.ts +147 -0
  92. package/types/core/base-builder/guards.d.ts +58 -0
  93. package/types/core/base-builder/id/generator.d.ts +69 -0
  94. package/types/core/base-builder/id/registry.d.ts +93 -0
  95. package/types/core/base-builder/index.d.ts +9 -0
  96. package/types/core/base-builder/resolution/path-resolver.d.ts +15 -0
  97. package/types/core/base-builder/resolution/pipeline.d.ts +27 -0
  98. package/types/core/base-builder/resolution/steps/asset-id.d.ts +14 -0
  99. package/types/core/base-builder/resolution/steps/asset-wrappers.d.ts +14 -0
  100. package/types/core/base-builder/resolution/steps/builders.d.ts +14 -0
  101. package/types/core/base-builder/resolution/steps/mixed-arrays.d.ts +14 -0
  102. package/types/core/base-builder/resolution/steps/nested-asset-wrappers.d.ts +14 -0
  103. package/types/core/base-builder/resolution/steps/static-values.d.ts +14 -0
  104. package/types/core/base-builder/resolution/steps/switches.d.ts +15 -0
  105. package/types/core/base-builder/resolution/steps/templates.d.ts +14 -0
  106. package/types/core/base-builder/resolution/value-resolver.d.ts +62 -0
  107. package/types/core/base-builder/storage/auxiliary-storage.d.ts +50 -0
  108. package/types/core/base-builder/storage/value-storage.d.ts +82 -0
  109. package/types/core/base-builder/types.d.ts +183 -0
  110. package/types/core/base-builder/utils.d.ts +2 -0
  111. package/types/core/flow/index.d.ts +23 -0
  112. package/types/core/index.d.ts +8 -0
  113. package/types/core/mocks/index.d.ts +2 -0
  114. package/types/core/mocks/types/action.d.ts +58 -0
  115. package/types/core/mocks/types/choice.d.ts +95 -0
  116. package/types/core/mocks/types/collection.d.ts +102 -0
  117. package/types/core/mocks/types/info.d.ts +7 -0
  118. package/types/core/mocks/types/input.d.ts +7 -0
  119. package/types/core/mocks/types/text.d.ts +5 -0
  120. package/types/core/schema/index.d.ts +34 -0
  121. package/types/core/schema/types.d.ts +5 -0
  122. package/types/core/switch/index.d.ts +21 -0
  123. package/types/core/tagged-template/binding.d.ts +19 -0
  124. package/types/core/tagged-template/expression.d.ts +11 -0
  125. package/types/core/tagged-template/extract-bindings-from-schema.d.ts +7 -0
  126. package/types/core/tagged-template/index.d.ts +6 -0
  127. package/types/core/tagged-template/std.d.ts +174 -0
  128. package/types/core/tagged-template/types.d.ts +69 -0
  129. package/types/core/template/index.d.ts +97 -0
  130. package/types/core/utils/index.d.ts +47 -0
  131. package/types/fp/index.d.ts +149 -0
  132. package/types/gen/common.d.ts +6 -0
  133. package/types/index.d.ts +3 -0
  134. package/types/types.d.ts +163 -0
@@ -0,0 +1,534 @@
1
+ import { describe, test, expect, beforeEach, vi } from "vitest";
2
+ import {
3
+ IDRegistry,
4
+ createIdRegistry,
5
+ globalIdRegistry,
6
+ genId,
7
+ } from "../id/generator";
8
+ import { BaseBuildContext } from "../types";
9
+
10
+ describe("IDRegistry", () => {
11
+ let registry: IDRegistry;
12
+
13
+ beforeEach(() => {
14
+ registry = new IDRegistry();
15
+ });
16
+
17
+ describe("ensureUnique", () => {
18
+ test("returns original ID when no collision", () => {
19
+ const id = registry.ensureUnique("test-id");
20
+ expect(id).toBe("test-id");
21
+ });
22
+
23
+ test("appends counter when collision detected", () => {
24
+ const id1 = registry.ensureUnique("duplicate");
25
+ const id2 = registry.ensureUnique("duplicate");
26
+ const id3 = registry.ensureUnique("duplicate");
27
+
28
+ expect(id1).toBe("duplicate");
29
+ expect(id2).toBe("duplicate-1");
30
+ expect(id3).toBe("duplicate-2");
31
+ });
32
+
33
+ test("handles complex ID patterns", () => {
34
+ const id1 = registry.ensureUnique("parent-slot-child");
35
+ const id2 = registry.ensureUnique("parent-slot-child");
36
+
37
+ expect(id1).toBe("parent-slot-child");
38
+ expect(id2).toBe("parent-slot-child-1");
39
+ });
40
+
41
+ test("maintains separate counters for different base IDs", () => {
42
+ registry.ensureUnique("id-a");
43
+ registry.ensureUnique("id-a"); // id-a-1
44
+ registry.ensureUnique("id-b");
45
+ registry.ensureUnique("id-a"); // id-a-2
46
+ registry.ensureUnique("id-b"); // id-b-1
47
+
48
+ expect(registry.ensureUnique("id-a")).toBe("id-a-3");
49
+ expect(registry.ensureUnique("id-b")).toBe("id-b-2");
50
+ });
51
+
52
+ test("handles empty string IDs", () => {
53
+ const id1 = registry.ensureUnique("");
54
+ const id2 = registry.ensureUnique("");
55
+
56
+ expect(id1).toBe("");
57
+ expect(id2).toBe("-1");
58
+ });
59
+
60
+ test("returns ID as-is when registry is disabled", () => {
61
+ registry.setEnabled(false);
62
+
63
+ const id1 = registry.ensureUnique("test");
64
+ const id2 = registry.ensureUnique("test");
65
+
66
+ expect(id1).toBe("test");
67
+ expect(id2).toBe("test"); // No collision detection
68
+ });
69
+
70
+ test("allows template placeholder IDs as duplicates", () => {
71
+ const id1 = registry.ensureUnique("parent-_index_");
72
+ const id2 = registry.ensureUnique("parent-_index_");
73
+ const id3 = registry.ensureUnique("parent-_index1_");
74
+ const id4 = registry.ensureUnique("parent-_row_");
75
+
76
+ // Template placeholders should not trigger collision detection
77
+ expect(id1).toBe("parent-_index_");
78
+ expect(id2).toBe("parent-_index_");
79
+ expect(id3).toBe("parent-_index1_");
80
+ expect(id4).toBe("parent-_row_");
81
+ });
82
+
83
+ test("enforces uniqueness for non-template IDs with similar patterns", () => {
84
+ const id1 = registry.ensureUnique("parent-_index_-field");
85
+ const id2 = registry.ensureUnique("parent-_index_-field");
86
+ const id3 = registry.ensureUnique("parent-something");
87
+ const id4 = registry.ensureUnique("parent-something");
88
+
89
+ // Non-template IDs should still enforce uniqueness
90
+ expect(id1).toBe("parent-_index_-field");
91
+ expect(id2).toBe("parent-_index_-field-1");
92
+ expect(id3).toBe("parent-something");
93
+ expect(id4).toBe("parent-something-1");
94
+ });
95
+ });
96
+
97
+ describe("has", () => {
98
+ test("returns false for unregistered IDs", () => {
99
+ expect(registry.has("unknown")).toBe(false);
100
+ });
101
+
102
+ test("returns true for registered IDs", () => {
103
+ registry.ensureUnique("known");
104
+ expect(registry.has("known")).toBe(true);
105
+ });
106
+
107
+ test("tracks modified IDs", () => {
108
+ registry.ensureUnique("base");
109
+ registry.ensureUnique("base"); // Creates "base-1"
110
+
111
+ expect(registry.has("base")).toBe(true);
112
+ expect(registry.has("base-1")).toBe(true);
113
+ expect(registry.has("base-2")).toBe(false);
114
+ });
115
+ });
116
+
117
+ describe("reset", () => {
118
+ test("clears all registered IDs", () => {
119
+ registry.ensureUnique("id1");
120
+ registry.ensureUnique("id2");
121
+ registry.ensureUnique("id3");
122
+
123
+ expect(registry.size()).toBe(3);
124
+
125
+ registry.reset();
126
+
127
+ expect(registry.size()).toBe(0);
128
+ expect(registry.has("id1")).toBe(false);
129
+ });
130
+
131
+ test("allows reuse of IDs after reset", () => {
132
+ registry.ensureUnique("reusable");
133
+ registry.ensureUnique("reusable"); // Would be "reusable-1"
134
+
135
+ registry.reset();
136
+
137
+ const id2 = registry.ensureUnique("reusable");
138
+ expect(id2).toBe("reusable"); // Not "reusable-2"
139
+ });
140
+ });
141
+
142
+ describe("size", () => {
143
+ test("returns 0 for empty registry", () => {
144
+ expect(registry.size()).toBe(0);
145
+ });
146
+
147
+ test("counts unique registered IDs", () => {
148
+ registry.ensureUnique("a");
149
+ registry.ensureUnique("b");
150
+ registry.ensureUnique("a"); // Creates "a-1"
151
+
152
+ expect(registry.size()).toBe(3);
153
+ });
154
+ });
155
+
156
+ describe("getRegisteredIds", () => {
157
+ test("returns empty array for empty registry", () => {
158
+ expect(registry.getRegisteredIds()).toEqual([]);
159
+ });
160
+
161
+ test("returns all registered IDs", () => {
162
+ registry.ensureUnique("first");
163
+ registry.ensureUnique("second");
164
+ registry.ensureUnique("first"); // Creates "first-1"
165
+
166
+ const ids = registry.getRegisteredIds();
167
+ expect(ids).toContain("first");
168
+ expect(ids).toContain("second");
169
+ expect(ids).toContain("first-1");
170
+ expect(ids).toHaveLength(3);
171
+ });
172
+ });
173
+ });
174
+
175
+ describe("createIdRegistry", () => {
176
+ test("creates independent registry instances", () => {
177
+ const registry1 = createIdRegistry();
178
+ const registry2 = createIdRegistry();
179
+
180
+ registry1.ensureUnique("shared");
181
+
182
+ // registry2 should not know about registry1's IDs
183
+ const id = registry2.ensureUnique("shared");
184
+ expect(id).toBe("shared");
185
+ });
186
+
187
+ test("respects enabled parameter", () => {
188
+ const disabled = createIdRegistry(false);
189
+
190
+ const id1 = disabled.ensureUnique("test");
191
+ const id2 = disabled.ensureUnique("test");
192
+
193
+ expect(id1).toBe("test");
194
+ expect(id2).toBe("test");
195
+ });
196
+ });
197
+
198
+ describe("genId with IDRegistry integration", () => {
199
+ beforeEach(() => {
200
+ // Reset the global registry before each test
201
+ globalIdRegistry.reset();
202
+ globalIdRegistry.setEnabled(true);
203
+ });
204
+
205
+ test("prevents ID collisions in slot branches", () => {
206
+ const ctx1: BaseBuildContext = {
207
+ parentId: "form",
208
+ branch: { type: "slot", name: "label" },
209
+ };
210
+
211
+ const ctx2: BaseBuildContext = {
212
+ parentId: "form",
213
+ branch: { type: "slot", name: "label" },
214
+ };
215
+
216
+ const id1 = genId(ctx1);
217
+ const id2 = genId(ctx2);
218
+
219
+ expect(id1).toBe("form-label");
220
+ expect(id2).toBe("form-label-1");
221
+ });
222
+
223
+ test("prevents collisions across different branch types", () => {
224
+ // These contexts would generate the same base ID
225
+ const slotCtx: BaseBuildContext = {
226
+ parentId: "parent",
227
+ branch: { type: "slot", name: "0" },
228
+ };
229
+
230
+ const arrayCtx: BaseBuildContext = {
231
+ parentId: "parent",
232
+ branch: { type: "array-item", index: 0 },
233
+ };
234
+
235
+ const id1 = genId(slotCtx);
236
+ const id2 = genId(arrayCtx);
237
+
238
+ expect(id1).toBe("parent-0");
239
+ expect(id2).toBe("parent-0-1");
240
+ });
241
+
242
+ test("handles complex nested contexts", () => {
243
+ const ctx1: BaseBuildContext = {
244
+ parentId: "collection-values-0",
245
+ branch: { type: "slot", name: "label" },
246
+ };
247
+
248
+ const ctx2: BaseBuildContext = {
249
+ parentId: "collection-values-0",
250
+ branch: { type: "slot", name: "label" },
251
+ };
252
+
253
+ const id1 = genId(ctx1);
254
+ const id2 = genId(ctx2);
255
+
256
+ expect(id1).toBe("collection-values-0-label");
257
+ expect(id2).toBe("collection-values-0-label-1");
258
+ });
259
+
260
+ test("warns about collisions in development", () => {
261
+ const originalEnv = process.env.NODE_ENV;
262
+ process.env.NODE_ENV = "development";
263
+
264
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
265
+
266
+ const ctx: BaseBuildContext = {
267
+ parentId: "test",
268
+ branch: { type: "slot", name: "slot" },
269
+ };
270
+
271
+ genId(ctx);
272
+ genId(ctx); // Should trigger collision warning
273
+
274
+ expect(warnSpy).toHaveBeenCalledWith(
275
+ expect.stringContaining("ID collision detected"),
276
+ );
277
+
278
+ warnSpy.mockRestore();
279
+ process.env.NODE_ENV = originalEnv;
280
+ });
281
+
282
+ test("allows template placeholders as duplicates", () => {
283
+ const templateCtx1: BaseBuildContext = {
284
+ parentId: "list",
285
+ branch: { type: "template", depth: 0 },
286
+ };
287
+
288
+ const templateCtx2: BaseBuildContext = {
289
+ parentId: "list",
290
+ branch: { type: "template", depth: 0 },
291
+ };
292
+
293
+ const id1 = genId(templateCtx1);
294
+ const id2 = genId(templateCtx2);
295
+
296
+ // Template placeholders should be allowed as duplicates
297
+ expect(id1).toBe("list-_index_");
298
+ expect(id2).toBe("list-_index_"); // Should be the same, not "list-_index_-1"
299
+ });
300
+
301
+ test("handles switch branches with collision detection", () => {
302
+ const switchCtx1: BaseBuildContext = {
303
+ parentId: "condition",
304
+ branch: { type: "switch", index: 0, kind: "static" },
305
+ };
306
+
307
+ const switchCtx2: BaseBuildContext = {
308
+ parentId: "condition",
309
+ branch: { type: "switch", index: 0, kind: "static" },
310
+ };
311
+
312
+ const id1 = genId(switchCtx1);
313
+ const id2 = genId(switchCtx2);
314
+
315
+ expect(id1).toBe("condition-staticSwitch-0");
316
+ expect(id2).toBe("condition-staticSwitch-0-1");
317
+ });
318
+ });
319
+
320
+ describe("ID Registry with real-world scenarios", () => {
321
+ beforeEach(() => {
322
+ globalIdRegistry.reset();
323
+ });
324
+
325
+ test("collection with duplicate slot names", () => {
326
+ // Simulates a collection with multiple items having the same structure
327
+ const contexts = [
328
+ {
329
+ parentId: "collection",
330
+ branch: { type: "slot" as const, name: "values" },
331
+ },
332
+ {
333
+ parentId: "collection-values",
334
+ branch: { type: "array-item" as const, index: 0 },
335
+ },
336
+ {
337
+ parentId: "collection-values-0",
338
+ branch: { type: "slot" as const, name: "label" },
339
+ },
340
+ {
341
+ parentId: "collection-values",
342
+ branch: { type: "array-item" as const, index: 1 },
343
+ },
344
+ {
345
+ parentId: "collection-values-1",
346
+ branch: { type: "slot" as const, name: "label" },
347
+ },
348
+ ];
349
+
350
+ const ids = contexts.map((ctx) => genId(ctx as BaseBuildContext));
351
+
352
+ // All IDs should be unique
353
+ const uniqueIds = new Set(ids);
354
+ expect(uniqueIds.size).toBe(ids.length);
355
+ });
356
+
357
+ test("deeply nested components with potential collisions", () => {
358
+ // Form > Section > Field > Validation > Error
359
+ const deepNesting: BaseBuildContext[] = [
360
+ { parentId: "form", branch: { type: "slot", name: "sections" } },
361
+ { parentId: "form-sections", branch: { type: "array-item", index: 0 } },
362
+ { parentId: "form-sections-0", branch: { type: "slot", name: "fields" } },
363
+ {
364
+ parentId: "form-sections-0-fields",
365
+ branch: { type: "array-item", index: 0 },
366
+ },
367
+ {
368
+ parentId: "form-sections-0-fields-0",
369
+ branch: { type: "slot", name: "validation" },
370
+ },
371
+ {
372
+ parentId: "form-sections-0-fields-0-validation",
373
+ branch: { type: "slot", name: "error" },
374
+ },
375
+ // Duplicate path (e.g., from a template)
376
+ {
377
+ parentId: "form-sections-0-fields-0-validation",
378
+ branch: { type: "slot", name: "error" },
379
+ },
380
+ ];
381
+
382
+ const ids = deepNesting.map((ctx) => genId(ctx));
383
+ const lastTwo = ids.slice(-2);
384
+
385
+ expect(lastTwo[0]).toBe("form-sections-0-fields-0-validation-error");
386
+ expect(lastTwo[1]).toBe("form-sections-0-fields-0-validation-error-1");
387
+ });
388
+
389
+ test("mixed static and dynamic content", () => {
390
+ const contexts: BaseBuildContext[] = [
391
+ // Static content
392
+ { parentId: "page", branch: { type: "slot", name: "header" } },
393
+ // Dynamic switch
394
+ {
395
+ parentId: "page",
396
+ branch: { type: "switch", index: 0, kind: "dynamic" },
397
+ },
398
+ // Template-generated content
399
+ { parentId: "page", branch: { type: "template", depth: 0 } },
400
+ // Another header (collision with first)
401
+ { parentId: "page", branch: { type: "slot", name: "header" } },
402
+ ];
403
+
404
+ const ids = contexts.map((ctx) => genId(ctx));
405
+
406
+ expect(ids[0]).toBe("page-header");
407
+ expect(ids[1]).toBe("page-dynamicSwitch-0");
408
+ expect(ids[2]).toBe("page-_index_");
409
+ expect(ids[3]).toBe("page-header-1");
410
+ });
411
+ });
412
+
413
+ describe("Template Placeholder Handling", () => {
414
+ let registry: IDRegistry;
415
+
416
+ beforeEach(() => {
417
+ registry = new IDRegistry();
418
+ });
419
+
420
+ test("allows duplicate IDs ending with _index_", () => {
421
+ const id1 = registry.ensureUnique("list-_index_");
422
+ const id2 = registry.ensureUnique("list-_index_");
423
+ const id3 = registry.ensureUnique("list-_index_");
424
+
425
+ expect(id1).toBe("list-_index_");
426
+ expect(id2).toBe("list-_index_");
427
+ expect(id3).toBe("list-_index_");
428
+ });
429
+
430
+ test("allows duplicate IDs ending with _index1_", () => {
431
+ const id1 = registry.ensureUnique("nested-_index1_");
432
+ const id2 = registry.ensureUnique("nested-_index1_");
433
+
434
+ expect(id1).toBe("nested-_index1_");
435
+ expect(id2).toBe("nested-_index1_");
436
+ });
437
+
438
+ test("allows duplicate IDs ending with _row_", () => {
439
+ const id1 = registry.ensureUnique("table-_row_");
440
+ const id2 = registry.ensureUnique("table-_row_");
441
+
442
+ expect(id1).toBe("table-_row_");
443
+ expect(id2).toBe("table-_row_");
444
+ });
445
+
446
+ test("allows duplicate IDs ending with _item_", () => {
447
+ const id1 = registry.ensureUnique("list-_item_");
448
+ const id2 = registry.ensureUnique("list-_item_");
449
+
450
+ expect(id1).toBe("list-_item_");
451
+ expect(id2).toBe("list-_item_");
452
+ });
453
+
454
+ test("enforces uniqueness for IDs with placeholder in middle", () => {
455
+ const id1 = registry.ensureUnique("list-_index_-field");
456
+ const id2 = registry.ensureUnique("list-_index_-field");
457
+
458
+ // Should enforce uniqueness because placeholder is not at the end
459
+ expect(id1).toBe("list-_index_-field");
460
+ expect(id2).toBe("list-_index_-field-1");
461
+ });
462
+
463
+ test("enforces uniqueness for IDs with _index prefix but not template placeholder", () => {
464
+ // _index is not a valid placeholder, only _index_ is
465
+ const id1 = registry.ensureUnique("list-_index");
466
+ const id2 = registry.ensureUnique("list-_index");
467
+
468
+ expect(id1).toBe("list-_index");
469
+ expect(id2).toBe("list-_index-1");
470
+ });
471
+ });
472
+
473
+ describe("Registry Edge Cases", () => {
474
+ let registry: IDRegistry;
475
+
476
+ beforeEach(() => {
477
+ registry = new IDRegistry();
478
+ });
479
+
480
+ test("handles IDs with special characters", () => {
481
+ const id1 = registry.ensureUnique("id-with-special/chars");
482
+ const id2 = registry.ensureUnique("id-with-special/chars");
483
+
484
+ expect(id1).toBe("id-with-special/chars");
485
+ expect(id2).toBe("id-with-special/chars-1");
486
+ });
487
+
488
+ test("handles very long IDs", () => {
489
+ const longId = "a".repeat(500);
490
+ const id1 = registry.ensureUnique(longId);
491
+ const id2 = registry.ensureUnique(longId);
492
+
493
+ expect(id1).toBe(longId);
494
+ expect(id2).toBe(longId + "-1");
495
+ });
496
+
497
+ test("setEnabled(false) bypasses all checks", () => {
498
+ registry.setEnabled(false);
499
+
500
+ const id1 = registry.ensureUnique("test");
501
+ const id2 = registry.ensureUnique("test");
502
+ const id3 = registry.ensureUnique("test");
503
+
504
+ expect(id1).toBe("test");
505
+ expect(id2).toBe("test");
506
+ expect(id3).toBe("test");
507
+
508
+ // Also should not track them
509
+ registry.setEnabled(true);
510
+ const id4 = registry.ensureUnique("test");
511
+ expect(id4).toBe("test"); // First unique since registry was disabled
512
+ });
513
+
514
+ test("handles numeric-looking IDs", () => {
515
+ const id1 = registry.ensureUnique("123");
516
+ const id2 = registry.ensureUnique("123");
517
+
518
+ expect(id1).toBe("123");
519
+ expect(id2).toBe("123-1");
520
+ });
521
+
522
+ test("handles IDs with hyphens that look like collision suffixes", () => {
523
+ // First register "test-1" as a base ID
524
+ const id1 = registry.ensureUnique("test-1");
525
+ // Try to register "test" which would normally get "-1" suffix
526
+ const id2 = registry.ensureUnique("test");
527
+ const id3 = registry.ensureUnique("test");
528
+
529
+ expect(id1).toBe("test-1");
530
+ expect(id2).toBe("test");
531
+ // The registry uses a simple counter per base ID, so "test" -> "test-2" (not "test-1-1")
532
+ expect(id3).toBe("test-2");
533
+ });
534
+ });