@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,2423 @@
1
+ import { describe, test, expect, beforeEach } from "vitest";
2
+ import type { Asset } from "@player-ui/types";
3
+ import type { TextAsset } from "../../mocks/types/text";
4
+ import {
5
+ action,
6
+ text,
7
+ input,
8
+ collection,
9
+ choice,
10
+ choiceItem,
11
+ } from "../../mocks/generated";
12
+ import { resetGlobalIdSet } from "../";
13
+ import { binding as b, expression as e } from "../../tagged-template";
14
+ import { template } from "../../template";
15
+ import { Collection } from "../../mocks/types/collection";
16
+ import { InputAsset } from "../../mocks/types/input";
17
+ import { ActionAsset } from "../../mocks/types/action";
18
+
19
+ describe("FluentBuilderBase - Basic Asset Creation", () => {
20
+ beforeEach(() => {
21
+ resetGlobalIdSet();
22
+ });
23
+
24
+ test("creates a simple text asset with auto-generated ID", () => {
25
+ const textAsset = text()
26
+ .withValue("Hello World")
27
+ .build({ parentId: "view-1" });
28
+
29
+ expect(textAsset).toMatchObject({
30
+ id: "view-1-text",
31
+ type: "text",
32
+ value: "Hello World",
33
+ });
34
+ });
35
+
36
+ test("creates an asset with explicit ID", () => {
37
+ const textAsset = text()
38
+ .withValue("Hello World")
39
+ .withId("custom-id")
40
+ .build({ parentId: "view-1" });
41
+
42
+ expect(textAsset.id).toBe("custom-id");
43
+ });
44
+
45
+ test("creates an action asset with metadata", () => {
46
+ const actionAsset = action()
47
+ .withValue("next")
48
+ .withLabel(text().withValue("Continue"))
49
+ .withMetaData({
50
+ role: "primary",
51
+ size: "large",
52
+ })
53
+ .build({ parentId: "view-1" });
54
+
55
+ expect(actionAsset).toMatchObject({
56
+ id: "view-1-action-next",
57
+ type: "action",
58
+ value: "next",
59
+ metaData: {
60
+ role: "primary",
61
+ size: "large",
62
+ },
63
+ });
64
+ });
65
+
66
+ test("creates an input asset with binding", () => {
67
+ const inputAsset = input()
68
+ .withBinding(b`user.firstName`)
69
+ .withLabel(text().withValue("First Name"))
70
+ .withPlaceholder("Enter your first name")
71
+ .build({ parentId: "view-1" });
72
+
73
+ expect(inputAsset).toMatchObject({
74
+ id: "view-1-input-firstName",
75
+ type: "input",
76
+ binding: "user.firstName", // binding property gets raw value without {{}}
77
+ placeholder: "Enter your first name",
78
+ });
79
+ });
80
+
81
+ test("generates unique IDs for multiple assets of same type", () => {
82
+ const context = { parentId: "view-1" };
83
+
84
+ const text1 = text().withValue("First").build(context);
85
+ const text2 = text().withValue("Second").build(context);
86
+ const text3 = text().withValue("Third").build(context);
87
+
88
+ expect(text1.id).toBe("view-1-text");
89
+ expect(text2.id).toBe("view-1-text-1");
90
+ expect(text3.id).toBe("view-1-text-2");
91
+ });
92
+ });
93
+
94
+ describe("FluentBuilderBase - Nested Assets with ID Hierarchy", () => {
95
+ beforeEach(() => {
96
+ resetGlobalIdSet();
97
+ });
98
+
99
+ test("creates nested assets with proper ID hierarchy", () => {
100
+ const collectionAsset = collection()
101
+ .withLabel(text().withValue("Options"))
102
+ .withValues([
103
+ text().withValue("Option 1"),
104
+ text().withValue("Option 2"),
105
+ text().withValue("Option 3"),
106
+ ])
107
+ .build({ parentId: "view-1" });
108
+
109
+ expect(collectionAsset.id).toBe("view-1-collection");
110
+
111
+ const expected: Collection = {
112
+ id: "view-1-collection",
113
+ label: {
114
+ asset: {
115
+ id: "view-1-collection-label-text",
116
+ type: "text",
117
+ value: "Options",
118
+ },
119
+ },
120
+ type: "collection",
121
+ values: [
122
+ {
123
+ asset: {
124
+ id: "view-1-collection-values-0-text",
125
+ type: "text",
126
+ value: "Option 1",
127
+ },
128
+ },
129
+ {
130
+ asset: {
131
+ id: "view-1-collection-values-1-text",
132
+ type: "text",
133
+ value: "Option 2",
134
+ },
135
+ },
136
+ {
137
+ asset: {
138
+ id: "view-1-collection-values-2-text",
139
+ type: "text",
140
+ value: "Option 3",
141
+ },
142
+ },
143
+ ],
144
+ };
145
+
146
+ expect(collectionAsset).toMatchObject(expected);
147
+ });
148
+
149
+ test("creates deeply nested asset structure", () => {
150
+ const collectionAsset = collection()
151
+ .withLabel(text().withValue("Actions"))
152
+ .withActions([
153
+ action().withValue("submit").withLabel(text().withValue("Submit")),
154
+ action().withValue("cancel").withLabel(text().withValue("Cancel")),
155
+ ])
156
+ .build({ parentId: "view-1" });
157
+
158
+ const expected: Collection = {
159
+ actions: [
160
+ {
161
+ asset: {
162
+ id: "view-1-collection-actions-0-action-submit",
163
+ label: {
164
+ asset: {
165
+ id: "view-1-collection-actions-0-action-submit-label-text",
166
+ type: "text",
167
+ value: "Submit",
168
+ },
169
+ },
170
+ type: "action",
171
+ value: "submit",
172
+ },
173
+ },
174
+ {
175
+ asset: {
176
+ id: "view-1-collection-actions-1-action-cancel",
177
+ label: {
178
+ asset: {
179
+ id: "view-1-collection-actions-1-action-cancel-label-text",
180
+ type: "text",
181
+ value: "Cancel",
182
+ },
183
+ },
184
+ type: "action",
185
+ value: "cancel",
186
+ },
187
+ },
188
+ ],
189
+ id: "view-1-collection",
190
+ label: {
191
+ asset: {
192
+ id: "view-1-collection-label-text",
193
+ type: "text",
194
+ value: "Actions",
195
+ },
196
+ },
197
+ type: "collection",
198
+ };
199
+
200
+ expect(collectionAsset).toMatchObject(expected);
201
+ });
202
+
203
+ test("handles mixed arrays with builders and plain objects", () => {
204
+ const collectionAsset = collection()
205
+ .withValues([
206
+ text().withValue("Built with builder"),
207
+ { id: "manual-1", type: "text", value: "Manual object" } as TextAsset,
208
+ text().withValue("Another builder"),
209
+ ])
210
+ .build({ parentId: "view-1" });
211
+
212
+ const expected: Collection["values"] = [
213
+ {
214
+ asset: {
215
+ id: "view-1-collection-values-0-text",
216
+ type: "text",
217
+ value: "Built with builder",
218
+ },
219
+ },
220
+ {
221
+ asset: {
222
+ id: "manual-1",
223
+ type: "text",
224
+ value: "Manual object",
225
+ },
226
+ },
227
+ {
228
+ asset: {
229
+ id: "view-1-collection-values-2-text",
230
+ type: "text",
231
+ value: "Another builder",
232
+ },
233
+ },
234
+ ];
235
+
236
+ expect(collectionAsset.values).toMatchObject(expected);
237
+ });
238
+ });
239
+
240
+ describe("FluentBuilderBase - Conditional Methods (if, ifElse)", () => {
241
+ beforeEach(() => {
242
+ resetGlobalIdSet();
243
+ });
244
+
245
+ test("conditionally sets property using if() when predicate is true", () => {
246
+ const showPlaceholder = true;
247
+
248
+ const inputAsset = input()
249
+ .withBinding(b`user.email`)
250
+ .withLabel(text().withValue("Email"))
251
+ .if(() => showPlaceholder, "placeholder", "Enter your email address")
252
+ .build({ parentId: "view-1" });
253
+
254
+ const expected: InputAsset = {
255
+ binding: "user.email",
256
+ id: "view-1-input-email",
257
+ label: {
258
+ asset: {
259
+ id: "view-1-input-email-label-text",
260
+ type: "text",
261
+ value: "Email",
262
+ },
263
+ },
264
+ placeholder: "Enter your email address",
265
+ type: "input",
266
+ };
267
+
268
+ expect(inputAsset).toMatchObject(expected);
269
+ });
270
+
271
+ test("does not set property using if() when predicate is false", () => {
272
+ const showPlaceholder = false;
273
+
274
+ const inputAsset = input()
275
+ .withBinding(b`user.email`)
276
+ .withLabel(text().withValue("Email"))
277
+ .if(() => showPlaceholder, "placeholder", "Enter your email address")
278
+ .build({ parentId: "view-1" });
279
+
280
+ const expected: InputAsset = {
281
+ binding: "user.email",
282
+ id: "view-1-input-email",
283
+ label: {
284
+ asset: {
285
+ id: "view-1-input-email-label-text",
286
+ type: "text",
287
+ value: "Email",
288
+ },
289
+ },
290
+ type: "input",
291
+ };
292
+
293
+ expect(inputAsset).toMatchObject(expected);
294
+ });
295
+
296
+ test("conditionally sets property using ifElse() - true case", () => {
297
+ const isPrimary = true;
298
+
299
+ const actionAsset = action()
300
+ .withLabel(text().withValue("Submit"))
301
+ .ifElse(() => isPrimary, "value", "submit", "cancel")
302
+ .ifElse(
303
+ () => isPrimary,
304
+ "metaData",
305
+ { role: "primary", size: "large" },
306
+ { role: "secondary", size: "medium" },
307
+ )
308
+ .build({ parentId: "view-1" });
309
+
310
+ const expected: ActionAsset = {
311
+ id: "view-1-action-submit",
312
+ label: {
313
+ asset: {
314
+ id: "view-1-action-submit-label-text",
315
+ type: "text",
316
+ value: "Submit",
317
+ },
318
+ },
319
+ metaData: {
320
+ role: "primary",
321
+ size: "large",
322
+ },
323
+ type: "action",
324
+ value: "submit",
325
+ };
326
+
327
+ expect(actionAsset).toMatchObject(expected);
328
+ });
329
+
330
+ test("conditionally sets property using ifElse() - false case", () => {
331
+ const isPrimary = false;
332
+
333
+ const actionAsset = action()
334
+ .withLabel(text().withValue("Cancel"))
335
+ .ifElse(() => isPrimary, "value", "submit", "cancel")
336
+ .ifElse(
337
+ () => isPrimary,
338
+ "metaData",
339
+ { role: "primary", size: "large" },
340
+ { role: "secondary", size: "medium" },
341
+ )
342
+ .build({ parentId: "view-1" });
343
+
344
+ const expected: ActionAsset = {
345
+ id: "view-1-action-cancel",
346
+ label: {
347
+ asset: {
348
+ id: "view-1-action-cancel-label-text",
349
+ type: "text",
350
+ value: "Cancel",
351
+ },
352
+ },
353
+ metaData: {
354
+ role: "secondary",
355
+ size: "medium",
356
+ },
357
+ type: "action",
358
+ value: "cancel",
359
+ };
360
+
361
+ expect(actionAsset).toMatchObject(expected);
362
+ });
363
+
364
+ test("uses if() with simple value properties", () => {
365
+ const includeAccessibility = true;
366
+
367
+ const actionAsset = action()
368
+ .withValue("next")
369
+ .withLabel(text().withValue("Continue"))
370
+ .if(
371
+ () => includeAccessibility,
372
+ "accessibility",
373
+ "Click to continue to the next step",
374
+ )
375
+ .build({ parentId: "view-1" });
376
+
377
+ const expected: ActionAsset = {
378
+ accessibility: "Click to continue to the next step",
379
+ id: "view-1-action-next",
380
+ label: {
381
+ asset: {
382
+ id: "view-1-action-next-label-text",
383
+ type: "text",
384
+ value: "Continue",
385
+ },
386
+ },
387
+ type: "action",
388
+ value: "next",
389
+ };
390
+
391
+ expect(actionAsset).toMatchObject(expected);
392
+ });
393
+
394
+ test("chains multiple if() calls", () => {
395
+ const hasPlaceholder = true;
396
+ const hasAccessibility = false;
397
+
398
+ const actionAsset = action()
399
+ .withValue("submit")
400
+ .withLabel(text().withValue("Submit"))
401
+ .if(() => hasPlaceholder, "metaData", { role: "primary" })
402
+ .if(() => hasAccessibility, "accessibility", "Submit button")
403
+ .build({ parentId: "view-1" });
404
+
405
+ const expected: ActionAsset = {
406
+ id: "view-1-action-submit",
407
+ label: {
408
+ asset: {
409
+ id: "view-1-action-submit-label-text",
410
+ type: "text",
411
+ value: "Submit",
412
+ },
413
+ },
414
+ metaData: {
415
+ role: "primary",
416
+ },
417
+ type: "action",
418
+ value: "submit",
419
+ };
420
+
421
+ expect(actionAsset).toMatchObject(expected);
422
+ });
423
+
424
+ test("uses predicate that accesses builder state", () => {
425
+ const actionAsset = action()
426
+ .withValue("next")
427
+ .withLabel(text().withValue("Continue"))
428
+ .if((builder) => builder.has("value"), "metaData", { role: "primary" })
429
+ .build({ parentId: "view-1" });
430
+
431
+ const expected: ActionAsset = {
432
+ id: "view-1-action-next",
433
+ label: {
434
+ asset: {
435
+ id: "view-1-action-next-label-text",
436
+ type: "text",
437
+ value: "Continue",
438
+ },
439
+ },
440
+ metaData: {
441
+ role: "primary",
442
+ },
443
+ type: "action",
444
+ value: "next",
445
+ };
446
+
447
+ expect(actionAsset).toMatchObject(expected);
448
+ });
449
+
450
+ test("uses if() with nested builder for AssetWrapper properties", () => {
451
+ const includeLabel = true;
452
+
453
+ const actionAsset = action()
454
+ .withValue("submit")
455
+ .if(() => includeLabel, "label", text().withValue("Submit"))
456
+ .build({ parentId: "view-1" });
457
+
458
+ const expected: ActionAsset = {
459
+ id: "view-1-action-submit",
460
+ label: {
461
+ asset: {
462
+ id: "view-1-action-submit-label-text",
463
+ type: "text",
464
+ value: "Submit",
465
+ },
466
+ },
467
+ type: "action",
468
+ value: "submit",
469
+ };
470
+
471
+ expect(actionAsset).toMatchObject(expected);
472
+ });
473
+
474
+ test("uses ifElse() with nested builders for AssetWrapper properties", () => {
475
+ const isActive = true;
476
+
477
+ const actionAsset = action()
478
+ .withValue("toggle")
479
+ .ifElse(
480
+ () => isActive,
481
+ "label",
482
+ text().withValue("Deactivate"),
483
+ text().withValue("Activate"),
484
+ )
485
+ .build({ parentId: "view-1" });
486
+
487
+ const expected: ActionAsset = {
488
+ id: "view-1-action-toggle",
489
+ label: {
490
+ asset: {
491
+ id: "view-1-action-toggle-label-text",
492
+ type: "text",
493
+ value: "Deactivate",
494
+ },
495
+ },
496
+ type: "action",
497
+ value: "toggle",
498
+ };
499
+
500
+ expect(actionAsset).toMatchObject(expected);
501
+ });
502
+
503
+ test("uses if() with array of builders for AssetWrapper properties", () => {
504
+ const hasValues = true;
505
+
506
+ const collectionAsset = collection()
507
+ .withLabel(text().withValue("List"))
508
+ .if(() => hasValues, "values", [
509
+ text().withValue("Item 1"),
510
+ text().withValue("Item 2"),
511
+ text().withValue("Item 3"),
512
+ ])
513
+ .build({ parentId: "view-1" });
514
+
515
+ const expected: Collection = {
516
+ id: "view-1-collection",
517
+ label: {
518
+ asset: {
519
+ id: "view-1-collection-label-text",
520
+ type: "text",
521
+ value: "List",
522
+ },
523
+ },
524
+ type: "collection",
525
+ values: [
526
+ {
527
+ asset: {
528
+ id: "view-1-collection-values-0-text",
529
+ type: "text",
530
+ value: "Item 1",
531
+ },
532
+ },
533
+ {
534
+ asset: {
535
+ id: "view-1-collection-values-1-text",
536
+ type: "text",
537
+ value: "Item 2",
538
+ },
539
+ },
540
+ {
541
+ asset: {
542
+ id: "view-1-collection-values-2-text",
543
+ type: "text",
544
+ value: "Item 3",
545
+ },
546
+ },
547
+ ],
548
+ };
549
+
550
+ expect(collectionAsset).toMatchObject(expected);
551
+ });
552
+
553
+ test("uses ifElse() with arrays of builders", () => {
554
+ const showPrimary = false;
555
+
556
+ const collectionAsset = collection()
557
+ .withLabel(text().withValue("Actions"))
558
+ .ifElse(
559
+ () => showPrimary,
560
+ "actions",
561
+ [action().withValue("save").withLabel(text().withValue("Save"))],
562
+ [
563
+ action().withValue("edit").withLabel(text().withValue("Edit")),
564
+ action().withValue("delete").withLabel(text().withValue("Delete")),
565
+ ],
566
+ )
567
+ .build({ parentId: "view-1" });
568
+
569
+ const expected: Collection = {
570
+ actions: [
571
+ {
572
+ asset: {
573
+ id: "view-1-collection-actions-0-action-edit",
574
+ label: {
575
+ asset: {
576
+ id: "view-1-collection-actions-0-action-edit-label-text",
577
+ type: "text",
578
+ value: "Edit",
579
+ },
580
+ },
581
+ type: "action",
582
+ value: "edit",
583
+ },
584
+ },
585
+ {
586
+ asset: {
587
+ id: "view-1-collection-actions-1-action-delete",
588
+ label: {
589
+ asset: {
590
+ id: "view-1-collection-actions-1-action-delete-label-text",
591
+ type: "text",
592
+ value: "Delete",
593
+ },
594
+ },
595
+ type: "action",
596
+ value: "delete",
597
+ },
598
+ },
599
+ ],
600
+ id: "view-1-collection",
601
+ label: {
602
+ asset: {
603
+ id: "view-1-collection-label-text",
604
+ type: "text",
605
+ value: "Actions",
606
+ },
607
+ },
608
+ type: "collection",
609
+ };
610
+
611
+ expect(collectionAsset).toMatchObject(expected);
612
+ });
613
+
614
+ test("uses if() with function that returns a builder", () => {
615
+ const useCustomLabel = true;
616
+
617
+ const actionAsset = action()
618
+ .withValue("submit")
619
+ .if(
620
+ () => useCustomLabel,
621
+ "label",
622
+ () => text().withValue("Custom Submit"),
623
+ )
624
+ .build({ parentId: "view-1" });
625
+
626
+ const expected: ActionAsset = {
627
+ id: "view-1-action-submit",
628
+ label: {
629
+ asset: {
630
+ id: "view-1-action-submit-label-text",
631
+ type: "text",
632
+ value: "Custom Submit",
633
+ },
634
+ },
635
+ type: "action",
636
+ value: "submit",
637
+ };
638
+
639
+ expect(actionAsset).toMatchObject(expected);
640
+ });
641
+ });
642
+
643
+ describe("FluentBuilderBase - Template Integration", () => {
644
+ beforeEach(() => {
645
+ resetGlobalIdSet();
646
+ });
647
+
648
+ test("creates a collection with template for dynamic values", () => {
649
+ const collectionAsset = collection()
650
+ .withLabel(text().withValue("Users"))
651
+ .template(
652
+ template({
653
+ data: b`users`,
654
+ output: "values",
655
+ value: text().withValue(b`users._index_.name`),
656
+ }),
657
+ )
658
+ .build({ parentId: "view-1" });
659
+
660
+ const expected: Collection = {
661
+ id: "view-1-collection",
662
+ label: {
663
+ asset: {
664
+ id: "view-1-collection-label-text",
665
+ type: "text",
666
+ value: "Users",
667
+ },
668
+ },
669
+ template: [
670
+ {
671
+ data: "{{users}}",
672
+ output: "values",
673
+ value: {
674
+ asset: {
675
+ id: "view-1-_index_-text",
676
+ type: "text",
677
+ value: "{{users._index_.name}}",
678
+ },
679
+ },
680
+ },
681
+ ],
682
+ type: "collection",
683
+ };
684
+
685
+ expect(collectionAsset).toMatchObject(expected);
686
+ });
687
+
688
+ test("creates a dynamic template", () => {
689
+ const collectionAsset = collection()
690
+ .withLabel(text().withValue("Items"))
691
+ .template(
692
+ template({
693
+ data: b`items`,
694
+ output: "values",
695
+ dynamic: true,
696
+ value: text().withValue(b`items._index_.label`),
697
+ }),
698
+ )
699
+ .build({ parentId: "view-1" });
700
+
701
+ const expected: Collection = {
702
+ id: "view-1-collection",
703
+ label: {
704
+ asset: {
705
+ id: "view-1-collection-label-text",
706
+ type: "text",
707
+ value: "Items",
708
+ },
709
+ },
710
+ template: [
711
+ {
712
+ data: "{{items}}",
713
+ dynamic: true,
714
+ output: "values",
715
+ value: {
716
+ asset: {
717
+ id: "view-1-_index_-text",
718
+ type: "text",
719
+ value: "{{items._index_.label}}",
720
+ },
721
+ },
722
+ },
723
+ ],
724
+ type: "collection",
725
+ };
726
+
727
+ expect(collectionAsset).toMatchObject(expected);
728
+ });
729
+
730
+ test("creates multiple templates on same asset", () => {
731
+ const collectionAsset = collection()
732
+ .withLabel(text().withValue("Mixed"))
733
+ .template(
734
+ template({
735
+ data: b`primaryItems`,
736
+ output: "values",
737
+ value: text().withValue(b`primaryItems._index_`),
738
+ }),
739
+ )
740
+ .template(
741
+ template({
742
+ data: b`secondaryItems`,
743
+ output: "values",
744
+ value: text().withValue(b`secondaryItems._index_`),
745
+ }),
746
+ )
747
+ .build({ parentId: "view-1" });
748
+
749
+ const expected: Collection = {
750
+ id: "view-1-collection",
751
+ label: {
752
+ asset: {
753
+ id: "view-1-collection-label-text",
754
+ type: "text",
755
+ value: "Mixed",
756
+ },
757
+ },
758
+ template: [
759
+ {
760
+ data: "{{primaryItems}}",
761
+ output: "values",
762
+ value: {
763
+ asset: {
764
+ id: "view-1-_index_-text",
765
+ type: "text",
766
+ value: "{{primaryItems._index_}}",
767
+ },
768
+ },
769
+ },
770
+ {
771
+ data: "{{secondaryItems}}",
772
+ output: "values",
773
+ value: {
774
+ asset: {
775
+ id: "view-1-1-_index_-text",
776
+ type: "text",
777
+ value: "{{secondaryItems._index_}}",
778
+ },
779
+ },
780
+ },
781
+ ],
782
+ type: "collection",
783
+ };
784
+
785
+ expect(collectionAsset).toMatchObject(expected);
786
+ });
787
+
788
+ test("creates template with complex nested asset", () => {
789
+ const collectionAsset = collection()
790
+ .withLabel(text().withValue("Actions"))
791
+ .template(
792
+ template({
793
+ data: b`actionList`,
794
+ output: "values",
795
+ value: action()
796
+ .withValue(b`actionList._index_.transition`)
797
+ .withLabel(text().withValue(b`actionList._index_.label`)),
798
+ }),
799
+ )
800
+ .build({ parentId: "view-1" });
801
+
802
+ const expected: Collection = {
803
+ id: "view-1-collection",
804
+ label: {
805
+ asset: {
806
+ id: "view-1-collection-label-text",
807
+ type: "text",
808
+ value: "Actions",
809
+ },
810
+ },
811
+ template: [
812
+ {
813
+ data: "{{actionList}}",
814
+ output: "values",
815
+ value: {
816
+ asset: {
817
+ id: "view-1-_index_-action-transition",
818
+ label: {
819
+ asset: {
820
+ id: "view-1-_index_-action-transition-label-text",
821
+ type: "text",
822
+ value: "{{actionList._index_.label}}",
823
+ },
824
+ },
825
+ type: "action",
826
+ value: "{{actionList._index_.transition}}",
827
+ },
828
+ },
829
+ },
830
+ ],
831
+ type: "collection",
832
+ };
833
+
834
+ expect(collectionAsset).toMatchObject(expected);
835
+ });
836
+ });
837
+
838
+ describe("FluentBuilderBase - Switch Integration", () => {
839
+ beforeEach(() => {
840
+ resetGlobalIdSet();
841
+ });
842
+
843
+ test("creates a static switch for asset property", () => {
844
+ const collectionAsset = collection()
845
+ .withLabel(text().withValue("Title"))
846
+ .switch(["label"], {
847
+ cases: [
848
+ {
849
+ case: e`user.lang === 'es'`,
850
+ asset: text().withValue("Título"),
851
+ },
852
+ {
853
+ case: e`user.lang === 'fr'`,
854
+ asset: text().withValue("Titre"),
855
+ },
856
+ {
857
+ case: true,
858
+ asset: text().withValue("Title"),
859
+ },
860
+ ],
861
+ })
862
+ .build({ parentId: "view-1" });
863
+
864
+ const expected = {
865
+ id: "view-1-collection",
866
+ label: {
867
+ staticSwitch: [
868
+ {
869
+ asset: {
870
+ id: "view-1-collection-label-staticSwitch-0-text",
871
+ type: "text",
872
+ value: "Título",
873
+ },
874
+ case: "@[user.lang === 'es']@",
875
+ },
876
+ {
877
+ asset: {
878
+ id: "view-1-collection-label-staticSwitch-1-text",
879
+ type: "text",
880
+ value: "Titre",
881
+ },
882
+ case: "@[user.lang === 'fr']@",
883
+ },
884
+ {
885
+ asset: {
886
+ id: "view-1-collection-label-staticSwitch-2-text",
887
+ type: "text",
888
+ value: "Title",
889
+ },
890
+ case: true,
891
+ },
892
+ ],
893
+ },
894
+ type: "collection",
895
+ };
896
+
897
+ expect(collectionAsset).toMatchObject(expected);
898
+ });
899
+
900
+ test("creates a dynamic switch", () => {
901
+ const collectionAsset = collection()
902
+ .withLabel(text().withValue("Status"))
903
+ .switch(["label"], {
904
+ cases: [
905
+ {
906
+ case: e`status === 'active'`,
907
+ asset: text().withValue("Active"),
908
+ },
909
+ {
910
+ case: true,
911
+ asset: text().withValue("Inactive"),
912
+ },
913
+ ],
914
+ isDynamic: true,
915
+ })
916
+ .build({ parentId: "view-1" });
917
+
918
+ const expected = {
919
+ id: "view-1-collection",
920
+ label: {
921
+ dynamicSwitch: [
922
+ {
923
+ asset: {
924
+ id: "view-1-collection-label-dynamicSwitch-0-text",
925
+ type: "text",
926
+ value: "Active",
927
+ },
928
+ case: "@[status === 'active']@",
929
+ },
930
+ {
931
+ asset: {
932
+ id: "view-1-collection-label-dynamicSwitch-1-text",
933
+ type: "text",
934
+ value: "Inactive",
935
+ },
936
+ case: true,
937
+ },
938
+ ],
939
+ },
940
+ type: "collection",
941
+ };
942
+
943
+ expect(collectionAsset).toMatchObject(expected);
944
+ });
945
+
946
+ test("creates multiple switches on same asset", () => {
947
+ const collectionAsset = collection()
948
+ .withLabel(text().withValue("Label"))
949
+ .withAdditionalInfo(text().withValue("Info"))
950
+ .switch(["label"], {
951
+ cases: [
952
+ { case: e`lang === 'es'`, asset: text().withValue("Etiqueta") },
953
+ { case: true, asset: text().withValue("Label") },
954
+ ],
955
+ })
956
+ .switch(["additionalInfo"], {
957
+ cases: [
958
+ { case: e`lang === 'es'`, asset: text().withValue("Información") },
959
+ { case: true, asset: text().withValue("Information") },
960
+ ],
961
+ })
962
+ .build({ parentId: "view-1" });
963
+
964
+ const expected = {
965
+ additionalInfo: {
966
+ staticSwitch: [
967
+ {
968
+ asset: {
969
+ id: "view-1-collection-additionalInfo-staticSwitch-2-text",
970
+ type: "text",
971
+ value: "Información",
972
+ },
973
+ case: "@[lang === 'es']@",
974
+ },
975
+ {
976
+ asset: {
977
+ id: "view-1-collection-additionalInfo-staticSwitch-3-text",
978
+ type: "text",
979
+ value: "Information",
980
+ },
981
+ case: true,
982
+ },
983
+ ],
984
+ },
985
+ id: "view-1-collection",
986
+ label: {
987
+ staticSwitch: [
988
+ {
989
+ asset: {
990
+ id: "view-1-collection-label-staticSwitch-0-text",
991
+ type: "text",
992
+ value: "Etiqueta",
993
+ },
994
+ case: "@[lang === 'es']@",
995
+ },
996
+ {
997
+ asset: {
998
+ id: "view-1-collection-label-staticSwitch-1-text",
999
+ type: "text",
1000
+ value: "Label",
1001
+ },
1002
+ case: true,
1003
+ },
1004
+ ],
1005
+ },
1006
+ type: "collection",
1007
+ };
1008
+
1009
+ expect(collectionAsset).toMatchObject(expected);
1010
+ });
1011
+
1012
+ test("creates switch with complex nested assets", () => {
1013
+ const collectionAsset = collection()
1014
+ .withLabel(text().withValue("Actions"))
1015
+ .switch(["actions"], {
1016
+ cases: [
1017
+ {
1018
+ case: e`user.canEdit`,
1019
+ asset: action()
1020
+ .withValue("edit")
1021
+ .withLabel(text().withValue("Edit")),
1022
+ },
1023
+ {
1024
+ case: true,
1025
+ asset: action()
1026
+ .withValue("view")
1027
+ .withLabel(text().withValue("View")),
1028
+ },
1029
+ ],
1030
+ })
1031
+ .build({ parentId: "view-1" });
1032
+
1033
+ const expected = {
1034
+ actions: [
1035
+ {
1036
+ staticSwitch: [
1037
+ {
1038
+ asset: {
1039
+ id: "view-1-collection-actions-staticSwitch-0-action-edit",
1040
+ label: {
1041
+ asset: {
1042
+ id: "view-1-collection-actions-staticSwitch-0-action-edit-label-text",
1043
+ type: "text",
1044
+ value: "Edit",
1045
+ },
1046
+ },
1047
+ type: "action",
1048
+ value: "edit",
1049
+ },
1050
+ case: "@[user.canEdit]@",
1051
+ },
1052
+ {
1053
+ asset: {
1054
+ id: "view-1-collection-actions-staticSwitch-1-action-view",
1055
+ label: {
1056
+ asset: {
1057
+ id: "view-1-collection-actions-staticSwitch-1-action-view-label-text",
1058
+ type: "text",
1059
+ value: "View",
1060
+ },
1061
+ },
1062
+ type: "action",
1063
+ value: "view",
1064
+ },
1065
+ case: true,
1066
+ },
1067
+ ],
1068
+ },
1069
+ ],
1070
+ id: "view-1-collection",
1071
+ label: {
1072
+ asset: {
1073
+ id: "view-1-collection-label-text",
1074
+ type: "text",
1075
+ value: "Actions",
1076
+ },
1077
+ },
1078
+ type: "collection",
1079
+ };
1080
+
1081
+ expect(collectionAsset).toMatchObject(expected);
1082
+ });
1083
+
1084
+ test("preserves explicit IDs in switch cases", () => {
1085
+ const collectionAsset = collection()
1086
+ .withLabel(text().withValue("Label"))
1087
+ .switch(["label"], {
1088
+ cases: [
1089
+ {
1090
+ case: true,
1091
+ asset: text().withValue("Custom").withId("my-custom-id"),
1092
+ },
1093
+ ],
1094
+ })
1095
+ .build({ parentId: "view-1" });
1096
+
1097
+ const expected = {
1098
+ id: "view-1-collection",
1099
+ label: {
1100
+ staticSwitch: [
1101
+ {
1102
+ asset: {
1103
+ id: "my-custom-id",
1104
+ type: "text",
1105
+ value: "Custom",
1106
+ },
1107
+ case: true,
1108
+ },
1109
+ ],
1110
+ },
1111
+ type: "collection",
1112
+ };
1113
+
1114
+ expect(collectionAsset).toMatchObject(expected);
1115
+ });
1116
+ });
1117
+
1118
+ describe("FluentBuilderBase - Complex Real-World Scenarios", () => {
1119
+ beforeEach(() => {
1120
+ resetGlobalIdSet();
1121
+ });
1122
+
1123
+ test("creates a complete form view with conditional validation", () => {
1124
+ const requiresValidation = true;
1125
+
1126
+ const formCollection = collection()
1127
+ .withLabel(text().withValue("User Registration"))
1128
+ .withValues([
1129
+ input()
1130
+ .withBinding(b`user.firstName`)
1131
+ .withLabel(text().withValue("First Name"))
1132
+ .withPlaceholder("Enter first name"),
1133
+ input()
1134
+ .withBinding(b`user.lastName`)
1135
+ .withLabel(text().withValue("Last Name"))
1136
+ .withPlaceholder("Enter last name"),
1137
+ input()
1138
+ .withBinding(b`user.email`)
1139
+ .withLabel(text().withValue("Email"))
1140
+ .withPlaceholder("Enter email"),
1141
+ ])
1142
+ .withActions([
1143
+ action()
1144
+ .withValue("submit")
1145
+ .withLabel(text().withValue("Register"))
1146
+ .withMetaData({ role: "primary", size: "large" })
1147
+ .if(() => requiresValidation, "validate", [
1148
+ "{{user.firstName}}",
1149
+ "{{user.lastName}}",
1150
+ "{{user.email}}",
1151
+ ]),
1152
+ action()
1153
+ .withValue("cancel")
1154
+ .withLabel(text().withValue("Cancel"))
1155
+ .withMetaData({ role: "secondary" }),
1156
+ ])
1157
+ .build({ parentId: "registration-view" });
1158
+
1159
+ const expected: Collection = {
1160
+ actions: [
1161
+ {
1162
+ asset: {
1163
+ id: "registration-view-collection-actions-0-action-submit",
1164
+ label: {
1165
+ asset: {
1166
+ id: "registration-view-collection-actions-0-action-submit-label-text",
1167
+ type: "text",
1168
+ value: "Register",
1169
+ },
1170
+ },
1171
+ metaData: {
1172
+ role: "primary",
1173
+ size: "large",
1174
+ },
1175
+ type: "action",
1176
+ validate: [
1177
+ "{{user.firstName}}",
1178
+ "{{user.lastName}}",
1179
+ "{{user.email}}",
1180
+ ],
1181
+ value: "submit",
1182
+ },
1183
+ },
1184
+ {
1185
+ asset: {
1186
+ id: "registration-view-collection-actions-1-action-cancel",
1187
+ label: {
1188
+ asset: {
1189
+ id: "registration-view-collection-actions-1-action-cancel-label-text",
1190
+ type: "text",
1191
+ value: "Cancel",
1192
+ },
1193
+ },
1194
+ metaData: {
1195
+ role: "secondary",
1196
+ },
1197
+ type: "action",
1198
+ value: "cancel",
1199
+ },
1200
+ },
1201
+ ],
1202
+ id: "registration-view-collection",
1203
+ label: {
1204
+ asset: {
1205
+ id: "registration-view-collection-label-text",
1206
+ type: "text",
1207
+ value: "User Registration",
1208
+ },
1209
+ },
1210
+ type: "collection",
1211
+ values: [
1212
+ {
1213
+ asset: {
1214
+ binding: "user.firstName",
1215
+ id: "registration-view-collection-values-0-input-firstName",
1216
+ label: {
1217
+ asset: {
1218
+ id: "registration-view-collection-values-0-input-firstName-label-text",
1219
+ type: "text",
1220
+ value: "First Name",
1221
+ },
1222
+ },
1223
+ placeholder: "Enter first name",
1224
+ type: "input",
1225
+ },
1226
+ },
1227
+ {
1228
+ asset: {
1229
+ binding: "user.lastName",
1230
+ id: "registration-view-collection-values-1-input-lastName",
1231
+ label: {
1232
+ asset: {
1233
+ id: "registration-view-collection-values-1-input-lastName-label-text",
1234
+ type: "text",
1235
+ value: "Last Name",
1236
+ },
1237
+ },
1238
+ placeholder: "Enter last name",
1239
+ type: "input",
1240
+ },
1241
+ },
1242
+ {
1243
+ asset: {
1244
+ binding: "user.email",
1245
+ id: "registration-view-collection-values-2-input-email",
1246
+ label: {
1247
+ asset: {
1248
+ id: "registration-view-collection-values-2-input-email-label-text",
1249
+ type: "text",
1250
+ value: "Email",
1251
+ },
1252
+ },
1253
+ placeholder: "Enter email",
1254
+ type: "input",
1255
+ },
1256
+ },
1257
+ ],
1258
+ };
1259
+
1260
+ expect(formCollection).toMatchObject(expected);
1261
+ });
1262
+
1263
+ test("creates a dynamic list with template and internationalization switch", () => {
1264
+ const listCollection = collection()
1265
+ .switch(["label"], {
1266
+ cases: [
1267
+ {
1268
+ case: e`user.locale === 'es'`,
1269
+ asset: text().withValue("Lista de Usuarios"),
1270
+ },
1271
+ {
1272
+ case: e`user.locale === 'fr'`,
1273
+ asset: text().withValue("Liste des Utilisateurs"),
1274
+ },
1275
+ {
1276
+ case: true,
1277
+ asset: text().withValue("User List"),
1278
+ },
1279
+ ],
1280
+ })
1281
+ .template(
1282
+ template({
1283
+ data: b`users`,
1284
+ output: "values",
1285
+ dynamic: true,
1286
+ value: collection()
1287
+ .withLabel(text().withValue(b`users._index_.name`))
1288
+ .withValues([
1289
+ text().withValue(b`users._index_.email`),
1290
+ text().withValue(b`users._index_.role`),
1291
+ ]),
1292
+ }),
1293
+ )
1294
+ .build({ parentId: "user-list-view" });
1295
+
1296
+ const expected = {
1297
+ id: "user-list-view-collection",
1298
+ label: {
1299
+ staticSwitch: [
1300
+ {
1301
+ asset: {
1302
+ id: "user-list-view-collection-label-staticSwitch-0-text",
1303
+ type: "text",
1304
+ value: "Lista de Usuarios",
1305
+ },
1306
+ case: "@[user.locale === 'es']@",
1307
+ },
1308
+ {
1309
+ asset: {
1310
+ id: "user-list-view-collection-label-staticSwitch-1-text",
1311
+ type: "text",
1312
+ value: "Liste des Utilisateurs",
1313
+ },
1314
+ case: "@[user.locale === 'fr']@",
1315
+ },
1316
+ {
1317
+ asset: {
1318
+ id: "user-list-view-collection-label-staticSwitch-2-text",
1319
+ type: "text",
1320
+ value: "User List",
1321
+ },
1322
+ case: true,
1323
+ },
1324
+ ],
1325
+ },
1326
+ template: [
1327
+ {
1328
+ data: "{{users}}",
1329
+ dynamic: true,
1330
+ output: "values",
1331
+ value: {
1332
+ asset: {
1333
+ id: "user-list-view-_index_-collection",
1334
+ label: {
1335
+ asset: {
1336
+ id: "user-list-view-_index_-collection-label-text",
1337
+ type: "text",
1338
+ value: "{{users._index_.name}}",
1339
+ },
1340
+ },
1341
+ type: "collection",
1342
+ values: [
1343
+ {
1344
+ asset: {
1345
+ id: "user-list-view-_index_-collection-values-0-text",
1346
+ type: "text",
1347
+ value: "{{users._index_.email}}",
1348
+ },
1349
+ },
1350
+ {
1351
+ asset: {
1352
+ id: "user-list-view-_index_-collection-values-1-text",
1353
+ type: "text",
1354
+ value: "{{users._index_.role}}",
1355
+ },
1356
+ },
1357
+ ],
1358
+ },
1359
+ },
1360
+ },
1361
+ ],
1362
+ type: "collection",
1363
+ };
1364
+
1365
+ expect(listCollection).toMatchObject(expected);
1366
+ });
1367
+
1368
+ test("creates a wizard-style multi-step form with conditional actions", () => {
1369
+ const currentStep = 2;
1370
+ const totalSteps = 3;
1371
+
1372
+ const stepCollection = collection()
1373
+ .withLabel(text().withValue(`Step ${currentStep} of ${totalSteps}`))
1374
+ .withValues([
1375
+ input()
1376
+ .withBinding(b`wizard.step${currentStep}.field1`)
1377
+ .withLabel(text().withValue("Field 1")),
1378
+ input()
1379
+ .withBinding(b`wizard.step${currentStep}.field2`)
1380
+ .withLabel(text().withValue("Field 2")),
1381
+ ])
1382
+ .withActions([
1383
+ action()
1384
+ .withValue("back")
1385
+ .withLabel(text().withValue("Back"))
1386
+ .withMetaData({ role: "back" })
1387
+ .if(() => currentStep > 1, "metaData", {
1388
+ role: "back",
1389
+ disabled: false,
1390
+ }),
1391
+ action()
1392
+ .ifElse(() => currentStep < totalSteps, "value", "next", "submit")
1393
+ .withLabel(
1394
+ currentStep < totalSteps
1395
+ ? text().withValue("Next")
1396
+ : text().withValue("Submit"),
1397
+ )
1398
+ .withMetaData({ role: "primary" }),
1399
+ ])
1400
+ .build({ parentId: "wizard-view" });
1401
+
1402
+ const expected: Collection = {
1403
+ actions: [
1404
+ {
1405
+ asset: {
1406
+ id: "wizard-view-collection-actions-0-action-back",
1407
+ label: {
1408
+ asset: {
1409
+ id: "wizard-view-collection-actions-0-action-back-label-text",
1410
+ type: "text",
1411
+ value: "Back",
1412
+ },
1413
+ },
1414
+ metaData: {
1415
+ disabled: false,
1416
+ role: "back",
1417
+ },
1418
+ type: "action",
1419
+ value: "back",
1420
+ },
1421
+ },
1422
+ {
1423
+ asset: {
1424
+ id: "wizard-view-collection-actions-1-action-next",
1425
+ label: {
1426
+ asset: {
1427
+ id: "wizard-view-collection-actions-1-action-next-label-text",
1428
+ type: "text",
1429
+ value: "Next",
1430
+ },
1431
+ },
1432
+ metaData: {
1433
+ role: "primary",
1434
+ },
1435
+ type: "action",
1436
+ value: "next",
1437
+ },
1438
+ },
1439
+ ],
1440
+ id: "wizard-view-collection",
1441
+ label: {
1442
+ asset: {
1443
+ id: "wizard-view-collection-label-text",
1444
+ type: "text",
1445
+ value: "Step 2 of 3",
1446
+ },
1447
+ },
1448
+ type: "collection",
1449
+ values: [
1450
+ {
1451
+ asset: {
1452
+ binding: "wizard.step2.field1",
1453
+ id: "wizard-view-collection-values-0-input-field1",
1454
+ label: {
1455
+ asset: {
1456
+ id: "wizard-view-collection-values-0-input-field1-label-text",
1457
+ type: "text",
1458
+ value: "Field 1",
1459
+ },
1460
+ },
1461
+ type: "input",
1462
+ },
1463
+ },
1464
+ {
1465
+ asset: {
1466
+ binding: "wizard.step2.field2",
1467
+ id: "wizard-view-collection-values-1-input-field2",
1468
+ label: {
1469
+ asset: {
1470
+ id: "wizard-view-collection-values-1-input-field2-label-text",
1471
+ type: "text",
1472
+ value: "Field 2",
1473
+ },
1474
+ },
1475
+ type: "input",
1476
+ },
1477
+ },
1478
+ ],
1479
+ };
1480
+
1481
+ expect(stepCollection).toMatchObject(expected);
1482
+ });
1483
+
1484
+ test("creates a complex nested structure with all features combined", () => {
1485
+ const userRole = "admin";
1486
+
1487
+ const collectionAsset = collection()
1488
+ // Conditional label based on user role
1489
+ .withLabel(
1490
+ userRole === "admin"
1491
+ ? text().withValue("Admin Dashboard")
1492
+ : text().withValue("User Dashboard"),
1493
+ )
1494
+ // Main content with template
1495
+ .template(
1496
+ template({
1497
+ data: b`sections`,
1498
+ output: "values",
1499
+ value: collection()
1500
+ .withLabel(text().withValue(b`sections._index_.title`))
1501
+ .withValues([text().withValue(b`sections._index_.description`)]),
1502
+ }),
1503
+ )
1504
+ // Actions with switch for internationalization
1505
+ .withActions([
1506
+ action()
1507
+ .withValue("save")
1508
+ .switch(["label"], {
1509
+ cases: [
1510
+ { case: e`lang === 'es'`, asset: text().withValue("Guardar") },
1511
+ {
1512
+ case: e`lang === 'fr'`,
1513
+ asset: text().withValue("Enregistrer"),
1514
+ },
1515
+ { case: true, asset: text().withValue("Save") },
1516
+ ],
1517
+ })
1518
+ .withMetaData({ role: "primary" }),
1519
+ ])
1520
+ .build({ parentId: "wizard-view" });
1521
+
1522
+ const expected = {
1523
+ actions: [
1524
+ {
1525
+ asset: {
1526
+ id: "wizard-view-collection-actions-0-action-save",
1527
+ label: {
1528
+ staticSwitch: [
1529
+ {
1530
+ asset: {
1531
+ id: "wizard-view-collection-actions-0-action-save-label-staticSwitch-0-text",
1532
+ type: "text",
1533
+ value: "Guardar",
1534
+ },
1535
+ case: "@[lang === 'es']@",
1536
+ },
1537
+ {
1538
+ asset: {
1539
+ id: "wizard-view-collection-actions-0-action-save-label-staticSwitch-1-text",
1540
+ type: "text",
1541
+ value: "Enregistrer",
1542
+ },
1543
+ case: "@[lang === 'fr']@",
1544
+ },
1545
+ {
1546
+ asset: {
1547
+ id: "wizard-view-collection-actions-0-action-save-label-staticSwitch-2-text",
1548
+ type: "text",
1549
+ value: "Save",
1550
+ },
1551
+ case: true,
1552
+ },
1553
+ ],
1554
+ },
1555
+ metaData: {
1556
+ role: "primary",
1557
+ },
1558
+ type: "action",
1559
+ value: "save",
1560
+ },
1561
+ },
1562
+ ],
1563
+ id: "wizard-view-collection",
1564
+ label: {
1565
+ asset: {
1566
+ id: "wizard-view-collection-label-text",
1567
+ type: "text",
1568
+ value: "Admin Dashboard",
1569
+ },
1570
+ },
1571
+ template: [
1572
+ {
1573
+ data: "{{sections}}",
1574
+ output: "values",
1575
+ value: {
1576
+ asset: {
1577
+ id: "wizard-view-_index_-collection",
1578
+ label: {
1579
+ asset: {
1580
+ id: "wizard-view-_index_-collection-label-text",
1581
+ type: "text",
1582
+ value: "{{sections._index_.title}}",
1583
+ },
1584
+ },
1585
+ type: "collection",
1586
+ values: [
1587
+ {
1588
+ asset: {
1589
+ id: "wizard-view-_index_-collection-values-0-text",
1590
+ type: "text",
1591
+ value: "{{sections._index_.description}}",
1592
+ },
1593
+ },
1594
+ ],
1595
+ },
1596
+ },
1597
+ },
1598
+ ],
1599
+ type: "collection",
1600
+ };
1601
+
1602
+ expect(collectionAsset).toMatchObject(expected);
1603
+ });
1604
+ });
1605
+
1606
+ describe("FluentBuilderBase - Type Compliance", () => {
1607
+ beforeEach(() => {
1608
+ resetGlobalIdSet();
1609
+ });
1610
+
1611
+ test("generated assets comply with Asset interface", () => {
1612
+ const assets: Asset[] = [
1613
+ text().withValue("Test").build({ parentId: "view-1" }),
1614
+ input()
1615
+ .withBinding(b`test`)
1616
+ .withLabel(text().withValue("Test"))
1617
+ .build({ parentId: "view-1" }),
1618
+ action()
1619
+ .withValue("test")
1620
+ .withLabel(text().withValue("Test"))
1621
+ .build({ parentId: "view-1" }),
1622
+ collection()
1623
+ .withLabel(text().withValue("Test"))
1624
+ .build({ parentId: "view-1" }),
1625
+ ];
1626
+
1627
+ const expected: Asset[] = [
1628
+ {
1629
+ id: "view-1-text",
1630
+ type: "text",
1631
+ value: "Test",
1632
+ },
1633
+ {
1634
+ binding: "test",
1635
+ id: "view-1-input-test",
1636
+ label: {
1637
+ asset: {
1638
+ id: "view-1-input-test-label-text",
1639
+ type: "text",
1640
+ value: "Test",
1641
+ },
1642
+ },
1643
+ type: "input",
1644
+ },
1645
+ {
1646
+ id: "view-1-action-test",
1647
+ label: {
1648
+ asset: {
1649
+ id: "view-1-action-test-label-text",
1650
+ type: "text",
1651
+ value: "Test",
1652
+ },
1653
+ },
1654
+ type: "action",
1655
+ value: "test",
1656
+ },
1657
+ {
1658
+ id: "view-1-collection",
1659
+ label: {
1660
+ asset: {
1661
+ id: "view-1-collection-label-text",
1662
+ type: "text",
1663
+ value: "Test",
1664
+ },
1665
+ },
1666
+ type: "collection",
1667
+ },
1668
+ ];
1669
+
1670
+ expect(assets).toMatchObject(expected);
1671
+ });
1672
+
1673
+ test("asset wrappers comply with AssetWrapper interface", () => {
1674
+ const collectionAsset = collection()
1675
+ .withLabel(text().withValue("Test"))
1676
+ .build({ parentId: "view-1" });
1677
+
1678
+ const expected: Collection = {
1679
+ id: "view-1-collection",
1680
+ label: {
1681
+ asset: {
1682
+ id: "view-1-collection-label-text",
1683
+ type: "text",
1684
+ value: "Test",
1685
+ },
1686
+ },
1687
+ type: "collection",
1688
+ };
1689
+
1690
+ expect(collectionAsset).toMatchObject(expected);
1691
+ });
1692
+
1693
+ test("switches comply with AssetWrapper interface", () => {
1694
+ const collectionAsset = collection()
1695
+ .withLabel(text().withValue("Original"))
1696
+ .switch(["label"], {
1697
+ cases: [
1698
+ { case: e`test === 1`, asset: text().withValue("Case 1") },
1699
+ { case: true, asset: text().withValue("Default") },
1700
+ ],
1701
+ })
1702
+ .build({ parentId: "view-1" });
1703
+
1704
+ const expected = {
1705
+ id: "view-1-collection",
1706
+ label: {
1707
+ staticSwitch: [
1708
+ {
1709
+ asset: {
1710
+ id: "view-1-collection-label-staticSwitch-0-text",
1711
+ type: "text",
1712
+ value: "Case 1",
1713
+ },
1714
+ case: "@[test === 1]@",
1715
+ },
1716
+ {
1717
+ asset: {
1718
+ id: "view-1-collection-label-staticSwitch-1-text",
1719
+ type: "text",
1720
+ value: "Default",
1721
+ },
1722
+ case: true,
1723
+ },
1724
+ ],
1725
+ },
1726
+ type: "collection",
1727
+ };
1728
+
1729
+ expect(collectionAsset).toMatchObject(expected);
1730
+ });
1731
+
1732
+ test("templates comply with Template interface", () => {
1733
+ const collectionAsset = collection()
1734
+ .withLabel(text().withValue("Test"))
1735
+ .template(
1736
+ template({
1737
+ data: b`items`,
1738
+ output: "values",
1739
+ value: text().withValue(b`items._index_.name`),
1740
+ }),
1741
+ )
1742
+ .build({ parentId: "view-1" });
1743
+
1744
+ const expected: Collection = {
1745
+ id: "view-1-collection",
1746
+ label: {
1747
+ asset: {
1748
+ id: "view-1-collection-label-text",
1749
+ type: "text",
1750
+ value: "Test",
1751
+ },
1752
+ },
1753
+ template: [
1754
+ {
1755
+ data: "{{items}}",
1756
+ output: "values",
1757
+ value: {
1758
+ asset: {
1759
+ id: "view-1-_index_-text",
1760
+ type: "text",
1761
+ value: "{{items._index_.name}}",
1762
+ },
1763
+ },
1764
+ },
1765
+ ],
1766
+ type: "collection",
1767
+ };
1768
+
1769
+ expect(collectionAsset).toMatchObject(expected);
1770
+ });
1771
+
1772
+ test("bindings comply with Binding type", () => {
1773
+ const inputAsset = input()
1774
+ .withBinding(b`user.email`)
1775
+ .withLabel(text().withValue("Email"))
1776
+ .build({ parentId: "view-1" });
1777
+
1778
+ const expected: InputAsset = {
1779
+ binding: "user.email",
1780
+ id: "view-1-input-email",
1781
+ label: {
1782
+ asset: {
1783
+ id: "view-1-input-email-label-text",
1784
+ type: "text",
1785
+ value: "Email",
1786
+ },
1787
+ },
1788
+ type: "input",
1789
+ };
1790
+
1791
+ expect(inputAsset).toMatchObject(expected);
1792
+ });
1793
+
1794
+ test("expressions comply with Expression type", () => {
1795
+ const collectionAsset = collection()
1796
+ .withLabel(text().withValue("Test"))
1797
+ .switch(["label"], {
1798
+ cases: [
1799
+ { case: e`status === 'active'`, asset: text().withValue("Active") },
1800
+ {
1801
+ case: e`status === 'inactive'`,
1802
+ asset: text().withValue("Inactive"),
1803
+ },
1804
+ { case: true, asset: text().withValue("Unknown") },
1805
+ ],
1806
+ })
1807
+ .build({ parentId: "view-1" });
1808
+
1809
+ const expected = {
1810
+ id: "view-1-collection",
1811
+ label: {
1812
+ staticSwitch: [
1813
+ {
1814
+ asset: {
1815
+ id: "view-1-collection-label-staticSwitch-0-text",
1816
+ type: "text",
1817
+ value: "Active",
1818
+ },
1819
+ case: "@[status === 'active']@",
1820
+ },
1821
+ {
1822
+ asset: {
1823
+ id: "view-1-collection-label-staticSwitch-1-text",
1824
+ type: "text",
1825
+ value: "Inactive",
1826
+ },
1827
+ case: "@[status === 'inactive']@",
1828
+ },
1829
+ {
1830
+ asset: {
1831
+ id: "view-1-collection-label-staticSwitch-2-text",
1832
+ type: "text",
1833
+ value: "Unknown",
1834
+ },
1835
+ case: true,
1836
+ },
1837
+ ],
1838
+ },
1839
+ type: "collection",
1840
+ };
1841
+
1842
+ expect(collectionAsset).toMatchObject(expected);
1843
+ });
1844
+ });
1845
+
1846
+ describe("FluentBuilderBase - Builder Utilities", () => {
1847
+ beforeEach(() => {
1848
+ resetGlobalIdSet();
1849
+ });
1850
+
1851
+ test("has() method correctly detects set properties", () => {
1852
+ const textBuilder = text().withValue("Hello");
1853
+
1854
+ expect(textBuilder.has("value")).toBe(true);
1855
+ expect(textBuilder.has("id")).toBe(false);
1856
+ });
1857
+
1858
+ test("peek() method returns static values", () => {
1859
+ const textBuilder = text().withValue("Hello");
1860
+
1861
+ expect(textBuilder.peek("value")).toBe("Hello");
1862
+ expect(textBuilder.peek("id")).toBeUndefined();
1863
+ });
1864
+
1865
+ test("peek() returns undefined for builder values", () => {
1866
+ const actionBuilder = action()
1867
+ .withValue("next")
1868
+ .withLabel(text().withValue("Continue"));
1869
+
1870
+ expect(actionBuilder.peek("value")).toBe("next");
1871
+ expect(actionBuilder.peek("label")).toBeUndefined(); // It's a builder/wrapper
1872
+ });
1873
+
1874
+ test("unset() removes properties", () => {
1875
+ const textBuilder = text().withValue("Hello").withId("custom-id");
1876
+
1877
+ expect(textBuilder.has("id")).toBe(true);
1878
+ textBuilder.unset("id");
1879
+ expect(textBuilder.has("id")).toBe(false);
1880
+
1881
+ const result = textBuilder.build({ parentId: "view-1" });
1882
+ expect(result.id).toBe("view-1-text"); // ID is auto-generated
1883
+ });
1884
+
1885
+ test("clear() removes all properties", () => {
1886
+ const textBuilder = text().withValue("Hello").withId("custom-id");
1887
+
1888
+ expect(textBuilder.has("value")).toBe(true);
1889
+ expect(textBuilder.has("id")).toBe(true);
1890
+
1891
+ textBuilder.clear();
1892
+
1893
+ expect(textBuilder.has("value")).toBe(false);
1894
+ expect(textBuilder.has("id")).toBe(false);
1895
+ });
1896
+
1897
+ test("clone() creates independent copy", () => {
1898
+ const original = text().withValue("Original");
1899
+ const cloned = original.clone();
1900
+
1901
+ cloned.withValue("Cloned");
1902
+
1903
+ const originalResult = original.build({ parentId: "view-1" });
1904
+ const clonedResult = cloned.build({ parentId: "view-1" });
1905
+
1906
+ expect(originalResult.value).toBe("Original");
1907
+ expect(clonedResult.value).toBe("Cloned");
1908
+ });
1909
+
1910
+ test("builder can be reused with different contexts", () => {
1911
+ const textBuilder = text().withValue("Reusable");
1912
+
1913
+ const result1 = textBuilder.build({ parentId: "view-1" });
1914
+ const result2 = textBuilder.build({ parentId: "view-2" });
1915
+
1916
+ expect(result1.id).toBe("view-1-text");
1917
+ expect(result2.id).toBe("view-2-text");
1918
+ expect(result1.value).toBe("Reusable");
1919
+ expect(result2.value).toBe("Reusable");
1920
+ });
1921
+ });
1922
+
1923
+ describe("FluentBuilderBase - Nested Objects ID Generation", () => {
1924
+ beforeEach(() => {
1925
+ resetGlobalIdSet();
1926
+ });
1927
+
1928
+ test("generates deterministic IDs for ChoiceItem in array based on parent slot and index", () => {
1929
+ const choiceAsset = choice()
1930
+ .withBinding(b`user.favoriteColor`)
1931
+ .withLabel(text().withValue("Choose your favorite color"))
1932
+ .withChoices([
1933
+ choiceItem().withValue("red").withLabel(text().withValue("Red")),
1934
+ choiceItem().withValue("blue").withLabel(text().withValue("Blue")),
1935
+ choiceItem().withValue("green").withLabel(text().withValue("Green")),
1936
+ ])
1937
+ .build({ parentId: "view-1" });
1938
+
1939
+ // Verify the choice asset itself has correct ID
1940
+ expect(choiceAsset.id).toBe("view-1-choice-favoriteColor");
1941
+ expect(choiceAsset.type).toBe("choice");
1942
+
1943
+ // Verify the choices array exists
1944
+ expect(choiceAsset.choices).toBeDefined();
1945
+
1946
+ if (choiceAsset.choices && Array.isArray(choiceAsset.choices)) {
1947
+ expect(choiceAsset.choices).toHaveLength(3);
1948
+
1949
+ // First choice item
1950
+ const firstChoice = choiceAsset.choices[0];
1951
+ if (firstChoice && typeof firstChoice === "object") {
1952
+ // ChoiceItem should have an ID following pattern: parent-slot-index-item
1953
+ expect(firstChoice.id).toBe(
1954
+ "view-1-choice-favoriteColor-choices-0-item",
1955
+ );
1956
+ expect(firstChoice.value).toBe("red");
1957
+
1958
+ // Verify nested label in first choice
1959
+ if ("label" in firstChoice && firstChoice.label) {
1960
+ const labelWrapper = firstChoice.label;
1961
+ if (
1962
+ typeof labelWrapper === "object" &&
1963
+ "asset" in labelWrapper &&
1964
+ labelWrapper.asset &&
1965
+ typeof labelWrapper.asset === "object"
1966
+ ) {
1967
+ const labelAsset = labelWrapper.asset;
1968
+ expect(labelAsset).toHaveProperty("type", "text");
1969
+ expect(labelAsset).toHaveProperty(
1970
+ "id",
1971
+ "view-1-choice-favoriteColor-choices-0-item-label-text",
1972
+ );
1973
+ }
1974
+ }
1975
+ }
1976
+
1977
+ // Second choice item
1978
+ const secondChoice = choiceAsset.choices[1];
1979
+ if (secondChoice && typeof secondChoice === "object") {
1980
+ expect(secondChoice.id).toBe(
1981
+ "view-1-choice-favoriteColor-choices-1-item",
1982
+ );
1983
+ expect(secondChoice.value).toBe("blue");
1984
+ }
1985
+
1986
+ // Third choice item
1987
+ const thirdChoice = choiceAsset.choices[2];
1988
+ if (thirdChoice && typeof thirdChoice === "object") {
1989
+ expect(thirdChoice.id).toBe(
1990
+ "view-1-choice-favoriteColor-choices-2-item",
1991
+ );
1992
+ expect(thirdChoice.value).toBe("green");
1993
+ }
1994
+
1995
+ // Verify all IDs are unique
1996
+ const ids = [firstChoice?.id, secondChoice?.id, thirdChoice?.id].filter(
1997
+ (id): id is string => typeof id === "string",
1998
+ );
1999
+ const uniqueIds = new Set(ids);
2000
+ expect(uniqueIds.size).toBe(ids.length);
2001
+ }
2002
+ });
2003
+
2004
+ test("generates proper IDs for deeply nested assets within ChoiceItem", () => {
2005
+ const choiceAsset = choice()
2006
+ .withBinding(b`user.plan`)
2007
+ .withLabel(text().withValue("Select a plan"))
2008
+ .withChoices([
2009
+ choiceItem()
2010
+ .withValue("basic")
2011
+ .withLabel(text().withValue("Basic Plan"))
2012
+ .withDescription(text().withValue("$10/month"))
2013
+ .withHelp(text().withValue("Best for individuals")),
2014
+ choiceItem()
2015
+ .withValue("pro")
2016
+ .withLabel(text().withValue("Pro Plan"))
2017
+ .withDescription(text().withValue("$25/month"))
2018
+ .withHelp(text().withValue("Best for teams"))
2019
+ .withFooter(text().withValue("Most popular")),
2020
+ ])
2021
+ .build({ parentId: "form-1" });
2022
+
2023
+ expect(choiceAsset.id).toBe("form-1-choice-plan");
2024
+
2025
+ if (choiceAsset.choices && Array.isArray(choiceAsset.choices)) {
2026
+ const firstChoice = choiceAsset.choices[0];
2027
+ if (firstChoice && typeof firstChoice === "object") {
2028
+ expect(firstChoice.id).toBe("form-1-choice-plan-choices-0-item");
2029
+
2030
+ // Check label
2031
+ if ("label" in firstChoice && firstChoice.label) {
2032
+ const labelWrapper = firstChoice.label;
2033
+ if (
2034
+ typeof labelWrapper === "object" &&
2035
+ "asset" in labelWrapper &&
2036
+ labelWrapper.asset &&
2037
+ typeof labelWrapper.asset === "object"
2038
+ ) {
2039
+ expect(labelWrapper.asset).toHaveProperty("type", "text");
2040
+ expect(labelWrapper.asset).toHaveProperty(
2041
+ "id",
2042
+ "form-1-choice-plan-choices-0-item-label-text",
2043
+ );
2044
+ }
2045
+ }
2046
+
2047
+ // Check description
2048
+ if ("description" in firstChoice && firstChoice.description) {
2049
+ const descWrapper = firstChoice.description;
2050
+ if (
2051
+ typeof descWrapper === "object" &&
2052
+ "asset" in descWrapper &&
2053
+ descWrapper.asset &&
2054
+ typeof descWrapper.asset === "object"
2055
+ ) {
2056
+ expect(descWrapper.asset).toHaveProperty("type", "text");
2057
+ expect(descWrapper.asset).toHaveProperty(
2058
+ "id",
2059
+ "form-1-choice-plan-choices-0-item-description-text",
2060
+ );
2061
+ }
2062
+ }
2063
+
2064
+ // Check help
2065
+ if ("help" in firstChoice && firstChoice.help) {
2066
+ const helpWrapper = firstChoice.help;
2067
+ if (
2068
+ typeof helpWrapper === "object" &&
2069
+ "asset" in helpWrapper &&
2070
+ helpWrapper.asset &&
2071
+ typeof helpWrapper.asset === "object"
2072
+ ) {
2073
+ expect(helpWrapper.asset).toHaveProperty("type", "text");
2074
+ expect(helpWrapper.asset).toHaveProperty(
2075
+ "id",
2076
+ "form-1-choice-plan-choices-0-item-help-text",
2077
+ );
2078
+ }
2079
+ }
2080
+ }
2081
+
2082
+ const secondChoice = choiceAsset.choices[1];
2083
+ if (secondChoice && typeof secondChoice === "object") {
2084
+ expect(secondChoice.id).toBe("form-1-choice-plan-choices-1-item");
2085
+
2086
+ // Check footer (only on pro plan)
2087
+ if ("footer" in secondChoice && secondChoice.footer) {
2088
+ const footerWrapper = secondChoice.footer;
2089
+ if (
2090
+ typeof footerWrapper === "object" &&
2091
+ "asset" in footerWrapper &&
2092
+ footerWrapper.asset &&
2093
+ typeof footerWrapper.asset === "object"
2094
+ ) {
2095
+ expect(footerWrapper.asset).toHaveProperty("type", "text");
2096
+ expect(footerWrapper.asset).toHaveProperty(
2097
+ "id",
2098
+ "form-1-choice-plan-choices-1-item-footer-text",
2099
+ );
2100
+ }
2101
+ }
2102
+ }
2103
+ }
2104
+ });
2105
+
2106
+ test("handles explicit IDs on ChoiceItem without overriding", () => {
2107
+ const choiceAsset = choice()
2108
+ .withBinding(b`user.answer`)
2109
+ .withChoices([
2110
+ choiceItem()
2111
+ .withId("custom-yes")
2112
+ .withValue("yes")
2113
+ .withLabel(text().withValue("Yes")),
2114
+ choiceItem()
2115
+ .withId("custom-no")
2116
+ .withValue("no")
2117
+ .withLabel(text().withValue("No")),
2118
+ ])
2119
+ .build({ parentId: "question-1" });
2120
+
2121
+ if (choiceAsset.choices && Array.isArray(choiceAsset.choices)) {
2122
+ expect(choiceAsset.choices).toHaveLength(2);
2123
+
2124
+ const firstChoice = choiceAsset.choices[0];
2125
+ if (firstChoice && typeof firstChoice === "object") {
2126
+ expect(firstChoice.id).toBe("custom-yes");
2127
+ }
2128
+
2129
+ const secondChoice = choiceAsset.choices[1];
2130
+ if (secondChoice && typeof secondChoice === "object") {
2131
+ expect(secondChoice.id).toBe("custom-no");
2132
+ }
2133
+ }
2134
+ });
2135
+
2136
+ test("generates unique IDs for mixed explicit and auto-generated ChoiceItems", () => {
2137
+ const choiceAsset = choice()
2138
+ .withBinding(b`user.rating`)
2139
+ .withChoices([
2140
+ choiceItem().withValue("1").withLabel(text().withValue("Poor")),
2141
+ choiceItem()
2142
+ .withId("rating-good")
2143
+ .withValue("2")
2144
+ .withLabel(text().withValue("Good")),
2145
+ choiceItem().withValue("3").withLabel(text().withValue("Excellent")),
2146
+ ])
2147
+ .build({ parentId: "survey-1" });
2148
+
2149
+ expect(choiceAsset.id).toBe("survey-1-choice-rating");
2150
+
2151
+ if (choiceAsset.choices && Array.isArray(choiceAsset.choices)) {
2152
+ const firstChoice = choiceAsset.choices[0];
2153
+ const secondChoice = choiceAsset.choices[1];
2154
+ const thirdChoice = choiceAsset.choices[2];
2155
+
2156
+ if (
2157
+ firstChoice &&
2158
+ typeof firstChoice === "object" &&
2159
+ secondChoice &&
2160
+ typeof secondChoice === "object" &&
2161
+ thirdChoice &&
2162
+ typeof thirdChoice === "object"
2163
+ ) {
2164
+ // First should have auto-generated ID
2165
+ expect(firstChoice.id).toBe("survey-1-choice-rating-choices-0-item");
2166
+
2167
+ // Second one should have explicit ID
2168
+ expect(secondChoice.id).toBe("rating-good");
2169
+
2170
+ // Third should have auto-generated ID
2171
+ expect(thirdChoice.id).toBe("survey-1-choice-rating-choices-2-item");
2172
+
2173
+ // Collect all IDs
2174
+ const allIds = [firstChoice.id, secondChoice.id, thirdChoice.id];
2175
+
2176
+ // Verify all are non-empty
2177
+ allIds.forEach((id) => {
2178
+ expect(id).toBeDefined();
2179
+ expect(id).not.toBe("");
2180
+ });
2181
+
2182
+ // Verify all are unique
2183
+ const uniqueIds = new Set(allIds);
2184
+ expect(uniqueIds.size).toBe(3);
2185
+ }
2186
+ }
2187
+ });
2188
+
2189
+ test("generates consistent IDs for ChoiceItem arrays across multiple builds", () => {
2190
+ const choiceBuilder = choice()
2191
+ .withBinding(b`user.preference`)
2192
+ .withChoices([
2193
+ choiceItem().withValue("opt1").withLabel(text().withValue("Option 1")),
2194
+ choiceItem().withValue("opt2").withLabel(text().withValue("Option 2")),
2195
+ ]);
2196
+
2197
+ resetGlobalIdSet();
2198
+ const firstBuild = choiceBuilder.build({ parentId: "view-1" });
2199
+
2200
+ resetGlobalIdSet();
2201
+ const secondBuild = choiceBuilder.build({ parentId: "view-1" });
2202
+
2203
+ // IDs should be deterministic across builds with same context
2204
+ expect(firstBuild.id).toBe(secondBuild.id);
2205
+
2206
+ if (
2207
+ firstBuild.choices &&
2208
+ Array.isArray(firstBuild.choices) &&
2209
+ secondBuild.choices &&
2210
+ Array.isArray(secondBuild.choices)
2211
+ ) {
2212
+ const firstChoices = firstBuild.choices;
2213
+ const secondChoices = secondBuild.choices;
2214
+
2215
+ expect(firstChoices).toHaveLength(secondChoices.length);
2216
+
2217
+ for (let i = 0; i < firstChoices.length; i++) {
2218
+ const first = firstChoices[i];
2219
+ const second = secondChoices[i];
2220
+ if (
2221
+ first &&
2222
+ typeof first === "object" &&
2223
+ second &&
2224
+ typeof second === "object"
2225
+ ) {
2226
+ expect(first.id).toBe(second.id);
2227
+ }
2228
+ }
2229
+ }
2230
+ });
2231
+
2232
+ test("preserves ChoiceItem structure without type field", () => {
2233
+ const choiceAsset = choice()
2234
+ .withBinding(b`user.choice`)
2235
+ .withChoices([
2236
+ choiceItem()
2237
+ .withValue("a")
2238
+ .withLabel(text().withValue("A"))
2239
+ .withAutomationId("automation-a"),
2240
+ ])
2241
+ .build({ parentId: "test-1" });
2242
+
2243
+ if (choiceAsset.choices && Array.isArray(choiceAsset.choices)) {
2244
+ const item = choiceAsset.choices[0];
2245
+ if (item && typeof item === "object") {
2246
+ // ChoiceItem should NOT have a type field
2247
+ expect("type" in item).toBe(false);
2248
+
2249
+ // But should have all other expected fields
2250
+ expect(item.id).toBeDefined();
2251
+ expect(item.value).toBe("a");
2252
+ expect(item.automationId).toBe("automation-a");
2253
+ expect(item.label).toBeDefined();
2254
+ }
2255
+ }
2256
+ });
2257
+ });
2258
+
2259
+ describe("FluentBuilderBase - Conditional Edge Cases", () => {
2260
+ beforeEach(() => {
2261
+ resetGlobalIdSet();
2262
+ });
2263
+
2264
+ test("if() evaluates predicate during if() call", () => {
2265
+ let predicateEvaluationCount = 0;
2266
+
2267
+ const builder = action()
2268
+ .withValue("test")
2269
+ .if(
2270
+ () => {
2271
+ predicateEvaluationCount++;
2272
+ return true;
2273
+ },
2274
+ "metaData",
2275
+ { role: "primary" },
2276
+ );
2277
+
2278
+ // Predicate is evaluated during the if() call (not lazily during build)
2279
+ expect(predicateEvaluationCount).toBe(1);
2280
+
2281
+ // Building should not re-evaluate the predicate
2282
+ const previousCount = predicateEvaluationCount;
2283
+ builder.build({ parentId: "test" });
2284
+ expect(predicateEvaluationCount).toBe(previousCount);
2285
+ });
2286
+
2287
+ test("if() handles value that is undefined", () => {
2288
+ const includeMetaData = true;
2289
+
2290
+ const actionAsset = action()
2291
+ .withValue("test")
2292
+ .if(() => includeMetaData, "metaData", undefined)
2293
+ .build({ parentId: "view-1" });
2294
+
2295
+ // When value is undefined, property should not be set
2296
+ expect(actionAsset.metaData).toBeUndefined();
2297
+ });
2298
+
2299
+ test("ifElse() evaluates predicate only once", () => {
2300
+ let predicateEvaluationCount = 0;
2301
+
2302
+ const actionAsset = action()
2303
+ .withValue("test")
2304
+ .ifElse(
2305
+ () => {
2306
+ predicateEvaluationCount++;
2307
+ return predicateEvaluationCount > 0;
2308
+ },
2309
+ "metaData",
2310
+ { role: "primary" },
2311
+ { role: "secondary" },
2312
+ )
2313
+ .build({ parentId: "view-1" });
2314
+
2315
+ // Predicate should be evaluated exactly once
2316
+ expect(predicateEvaluationCount).toBe(1);
2317
+ expect(actionAsset.metaData).toEqual({ role: "primary" });
2318
+ });
2319
+
2320
+ test("ifElse() handles both values being functions", () => {
2321
+ const isPrimary = true;
2322
+
2323
+ const actionAsset = action()
2324
+ .withValue("test")
2325
+ .ifElse(
2326
+ () => isPrimary,
2327
+ "label",
2328
+ () => text().withValue("Primary Label"),
2329
+ () => text().withValue("Secondary Label"),
2330
+ )
2331
+ .build({ parentId: "view-1" });
2332
+
2333
+ expect(actionAsset.label?.asset.value).toBe("Primary Label");
2334
+ });
2335
+
2336
+ test("ifElse() evaluates only the chosen value function", () => {
2337
+ let primaryFnCalled = false;
2338
+ let secondaryFnCalled = false;
2339
+
2340
+ const isPrimary = true;
2341
+
2342
+ const actionAsset = action()
2343
+ .withValue("test")
2344
+ .ifElse(
2345
+ () => isPrimary,
2346
+ "label",
2347
+ () => {
2348
+ primaryFnCalled = true;
2349
+ return text().withValue("Primary");
2350
+ },
2351
+ () => {
2352
+ secondaryFnCalled = true;
2353
+ return text().withValue("Secondary");
2354
+ },
2355
+ )
2356
+ .build({ parentId: "view-1" });
2357
+
2358
+ // Only the chosen value function should be called
2359
+ expect(primaryFnCalled).toBe(true);
2360
+ expect(secondaryFnCalled).toBe(false);
2361
+ expect(actionAsset.label?.asset.value).toBe("Primary");
2362
+ });
2363
+
2364
+ test("if() works with complex predicate accessing builder state", () => {
2365
+ const actionAsset = action()
2366
+ .withValue("submit")
2367
+ .if(
2368
+ (builder) => {
2369
+ const currentValue = builder.peek("value");
2370
+ return currentValue === "submit";
2371
+ },
2372
+ "metaData",
2373
+ { role: "primary" },
2374
+ )
2375
+ .build({ parentId: "view-1" });
2376
+
2377
+ expect(actionAsset.metaData).toEqual({ role: "primary" });
2378
+ });
2379
+
2380
+ test("if() does not set property when predicate returns false with builder", () => {
2381
+ const actionAsset = action()
2382
+ .withValue("cancel")
2383
+ .if(
2384
+ (builder) => {
2385
+ const currentValue = builder.peek("value");
2386
+ return currentValue === "submit";
2387
+ },
2388
+ "metaData",
2389
+ { role: "primary" },
2390
+ )
2391
+ .build({ parentId: "view-1" });
2392
+
2393
+ expect(actionAsset.metaData).toBeUndefined();
2394
+ });
2395
+
2396
+ test("multiple if() calls are all evaluated", () => {
2397
+ const conditions = {
2398
+ first: true,
2399
+ second: false,
2400
+ third: true,
2401
+ };
2402
+
2403
+ const actionAsset = action()
2404
+ .withValue("test")
2405
+ .if(() => conditions.first, "metaData", { role: "primary" })
2406
+ .if(() => conditions.second, "accessibility", "Accessible button")
2407
+ .build({ parentId: "view-1" });
2408
+
2409
+ expect(actionAsset.metaData).toEqual({ role: "primary" });
2410
+ expect(actionAsset.accessibility).toBeUndefined();
2411
+ });
2412
+
2413
+ test("if() with false predicate does not affect subsequent with calls", () => {
2414
+ const actionAsset = action()
2415
+ .if(() => false, "value", "shouldNotAppear")
2416
+ .withValue("actualValue")
2417
+ .withLabel(text().withValue("Label"))
2418
+ .build({ parentId: "view-1" });
2419
+
2420
+ expect(actionAsset.value).toBe("actualValue");
2421
+ expect(actionAsset.label?.asset.value).toBe("Label");
2422
+ });
2423
+ });