@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,200 @@
1
+ import { describe, test, expect, beforeEach } from "vitest";
2
+ import { action } from "../../mocks/generated";
3
+ import { binding as b, expression as e } from "../../tagged-template";
4
+ import { resetGlobalIdSet } from "../index";
5
+
6
+ /**
7
+ * Test suite for value extraction with TaggedTemplateValue
8
+ * Ensures that arrays and objects with TaggedTemplateValue are properly extracted
9
+ * without being stringified to [object Object] or flattened
10
+ */
11
+
12
+ describe("Value Extraction - TaggedTemplateValue Handling", () => {
13
+ beforeEach(() => {
14
+ resetGlobalIdSet();
15
+ });
16
+
17
+ test("preserves array structure with TaggedTemplateValue elements", () => {
18
+ const asset = action()
19
+ .withExp([
20
+ e`conditional(isEmpty({{forms.test}}), setDataVal('forms.test', ''), 'false')`,
21
+ e`conditional(isEmpty({{forms.other}}), setDataVal('forms.other', ''), 'false')`,
22
+ ])
23
+ .build({ parentId: "view-1" });
24
+
25
+ expect(asset.exp).toBeInstanceOf(Array);
26
+ expect(asset.exp).toHaveLength(2);
27
+ // Expressions should preserve @[]@ syntax
28
+ expect(asset.exp?.[0]).toBe(
29
+ "@[conditional(isEmpty({{forms.test}}), setDataVal('forms.test', ''), 'false')]@",
30
+ );
31
+ expect(asset.exp?.[1]).toBe(
32
+ "@[conditional(isEmpty({{forms.other}}), setDataVal('forms.other', ''), 'false')]@",
33
+ );
34
+ });
35
+
36
+ test("preserves array structure with mixed TaggedTemplateValue and strings", () => {
37
+ const asset = action()
38
+ .withExp([
39
+ "static string",
40
+ e`conditional(isEmpty({{forms.test}}), setDataVal('forms.test', ''), 'false')`,
41
+ ])
42
+ .build({ parentId: "view-1" });
43
+
44
+ expect(asset.exp).toBeInstanceOf(Array);
45
+ expect(asset.exp).toHaveLength(2);
46
+ expect(asset.exp?.[0]).toBe("static string");
47
+ // Expression should preserve @[]@ syntax
48
+ expect(asset.exp?.[1]).toBe(
49
+ "@[conditional(isEmpty({{forms.test}}), setDataVal('forms.test', ''), 'false')]@",
50
+ );
51
+ });
52
+
53
+ test("extracts object with TaggedTemplateValue properties correctly", () => {
54
+ const asset = action()
55
+ .withConfirmation({
56
+ message: b`forms.confirmMessage`,
57
+ affirmativeLabel: b`forms.yesLabel`,
58
+ negativeLabel: "Cancel",
59
+ })
60
+ .build({ parentId: "view-1" });
61
+
62
+ // Bindings should preserve {{}} syntax
63
+ expect(asset.confirmation).toEqual({
64
+ message: "{{forms.confirmMessage}}",
65
+ affirmativeLabel: "{{forms.yesLabel}}",
66
+ negativeLabel: "Cancel",
67
+ });
68
+
69
+ // Ensure it's not stringified to [object Object]
70
+ expect(typeof asset.confirmation).toBe("object");
71
+ expect(asset.confirmation).not.toBe("[object Object]");
72
+ });
73
+
74
+ test("handles nested arrays in objects with TaggedTemplateValue", () => {
75
+ // Simulate a complex structure with nested arrays
76
+ const asset = action()
77
+ .withAdditionalProperties({
78
+ listeners: {
79
+ "dataChange.forms.test": [
80
+ e`conditional(isEmpty({{forms.test}}), setDataVal('forms.test', ''), 'false')`,
81
+ ],
82
+ "dataChange.forms.other": [
83
+ e`setDataVal('forms.other', 'value1')`,
84
+ e`setDataVal('forms.other2', 'value2')`,
85
+ ],
86
+ },
87
+ })
88
+ .build({ parentId: "view-1" });
89
+
90
+ // Check listeners object exists and is not stringified
91
+ expect(asset.listeners).toBeDefined();
92
+ expect(typeof asset.listeners).toBe("object");
93
+ expect(asset.listeners).not.toBe("[object Object]");
94
+
95
+ // Check nested arrays are preserved with expression syntax
96
+ const listeners = asset.listeners as Record<string, string[]>;
97
+ expect(Array.isArray(listeners["dataChange.forms.test"])).toBe(true);
98
+ expect(listeners["dataChange.forms.test"]).toHaveLength(1);
99
+ expect(listeners["dataChange.forms.test"][0]).toBe(
100
+ "@[conditional(isEmpty({{forms.test}}), setDataVal('forms.test', ''), 'false')]@",
101
+ );
102
+
103
+ expect(Array.isArray(listeners["dataChange.forms.other"])).toBe(true);
104
+ expect(listeners["dataChange.forms.other"]).toHaveLength(2);
105
+ expect(listeners["dataChange.forms.other"][0]).toBe(
106
+ "@[setDataVal('forms.other', 'value1')]@",
107
+ );
108
+ expect(listeners["dataChange.forms.other"][1]).toBe(
109
+ "@[setDataVal('forms.other2', 'value2')]@",
110
+ );
111
+ });
112
+
113
+ test("handles nested objects with TaggedTemplateValue in arrays", () => {
114
+ // Simulate modifiers array with objects
115
+ const asset = action()
116
+ .withAdditionalProperties({
117
+ modifiers: [
118
+ { type: "tag", value: b`forms.tagValue` },
119
+ { type: "style", value: "important" },
120
+ ],
121
+ })
122
+ .build({ parentId: "view-1" });
123
+
124
+ expect(asset.modifiers).toBeDefined();
125
+ expect(Array.isArray(asset.modifiers)).toBe(true);
126
+
127
+ const modifiers = asset.modifiers as Array<{ type: string; value: string }>;
128
+ expect(modifiers).toHaveLength(2);
129
+
130
+ // First modifier should have extracted TaggedTemplateValue with binding syntax
131
+ expect(modifiers[0]).toEqual({ type: "tag", value: "{{forms.tagValue}}" });
132
+ expect(modifiers[0]).not.toBe("[object Object]");
133
+
134
+ // Second modifier should be unchanged
135
+ expect(modifiers[1]).toEqual({ type: "style", value: "important" });
136
+ expect(modifiers[1]).not.toBe("[object Object]");
137
+ });
138
+
139
+ test("handles empty metadata object correctly", () => {
140
+ const asset = action().withMetaData({}).build({ parentId: "view-1" });
141
+
142
+ expect(asset.metaData).toEqual({});
143
+ expect(asset.metaData).not.toBe("[object Object]");
144
+ });
145
+
146
+ test("handles metadata object with plain values correctly", () => {
147
+ const asset = action()
148
+ .withMetaData({
149
+ role: "primary",
150
+ size: "large",
151
+ })
152
+ .build({ parentId: "view-1" });
153
+
154
+ expect(asset.metaData).toEqual({
155
+ role: "primary",
156
+ size: "large",
157
+ });
158
+ expect(asset.metaData).not.toBe("[object Object]");
159
+ });
160
+
161
+ test("deeply nested structures with TaggedTemplateValue", () => {
162
+ const asset = action()
163
+ .withAdditionalProperties({
164
+ config: {
165
+ validation: {
166
+ rules: [
167
+ e`conditional(isEmpty({{forms.field1}}), 'required', 'valid')`,
168
+ e`conditional(isEmpty({{forms.field2}}), 'required', 'valid')`,
169
+ ],
170
+ messages: {
171
+ error: b`forms.errorMessage`,
172
+ warning: "Please check your input",
173
+ },
174
+ },
175
+ },
176
+ })
177
+ .build({ parentId: "view-1" });
178
+
179
+ const config = asset.config as {
180
+ validation: {
181
+ rules: string[];
182
+ messages: { error: string; warning: string };
183
+ };
184
+ };
185
+
186
+ // Check deeply nested arrays with expression syntax
187
+ expect(Array.isArray(config.validation.rules)).toBe(true);
188
+ expect(config.validation.rules).toHaveLength(2);
189
+ expect(config.validation.rules[0]).toBe(
190
+ "@[conditional(isEmpty({{forms.field1}}), 'required', 'valid')]@",
191
+ );
192
+
193
+ // Check deeply nested objects with binding syntax
194
+ expect(config.validation.messages).toEqual({
195
+ error: "{{forms.errorMessage}}",
196
+ warning: "Please check your input",
197
+ });
198
+ expect(config.validation.messages).not.toBe("[object Object]");
199
+ });
200
+ });
@@ -0,0 +1,459 @@
1
+ import { describe, test, expect, beforeEach, vi } from "vitest";
2
+ import type { BaseBuildContext, FluentBuilder } from "../types";
3
+ import { FLUENT_BUILDER_SYMBOL } from "../types";
4
+ import { ValueStorage } from "../storage/value-storage";
5
+
6
+ // Create a mock builder that satisfies the FluentBuilder interface
7
+ function createMockBuilder<T>(
8
+ buildResult: T,
9
+ ): FluentBuilder<T, BaseBuildContext> {
10
+ return {
11
+ [FLUENT_BUILDER_SYMBOL]: true as const,
12
+ build: vi.fn(() => buildResult),
13
+ } as unknown as FluentBuilder<T, BaseBuildContext>;
14
+ }
15
+
16
+ interface TestType {
17
+ value?: string;
18
+ count?: number;
19
+ items?: string[];
20
+ label?: string;
21
+ nested?: { deep: string };
22
+ builder?: { id: string };
23
+ mixedArray?: unknown[];
24
+ }
25
+
26
+ describe("ValueStorage - set() Routing", () => {
27
+ let storage: ValueStorage<TestType>;
28
+
29
+ beforeEach(() => {
30
+ storage = new ValueStorage<TestType>();
31
+ });
32
+
33
+ test("routes FluentBuilder to builders Map", () => {
34
+ const builder = createMockBuilder({ id: "test", type: "text" });
35
+
36
+ storage.set("builder", builder);
37
+
38
+ expect(storage.getValueType("builder")).toBe("builder");
39
+ expect(storage.getBuilders().has("builder")).toBe(true);
40
+ expect(storage.peek("builder")).toBeUndefined();
41
+ expect(storage.peekBuilder("builder")).toBeDefined();
42
+ });
43
+
44
+ test("routes arrays with builders to mixedArrays Map", () => {
45
+ const builder = createMockBuilder({ id: "test", type: "text" });
46
+
47
+ storage.set("mixedArray", [builder, "static"]);
48
+
49
+ expect(storage.getValueType("mixedArray")).toBe("mixed-array");
50
+ expect(storage.getMixedArrays().has("mixedArray")).toBe(true);
51
+ });
52
+
53
+ test("routes objects containing builders to builders Map", () => {
54
+ const builder = createMockBuilder({ id: "test", type: "text" });
55
+ const objWithBuilder = { asset: builder };
56
+
57
+ storage.set("nested", objWithBuilder as unknown as { deep: string });
58
+
59
+ expect(storage.getValueType("nested")).toBe("builder");
60
+ expect(storage.getBuilders().has("nested")).toBe(true);
61
+ });
62
+
63
+ test("routes static values to values object", () => {
64
+ storage.set("value", "hello");
65
+ storage.set("count", 42);
66
+ storage.set("items", ["a", "b", "c"]);
67
+ storage.set("nested", { deep: "value" });
68
+
69
+ expect(storage.getValueType("value")).toBe("static");
70
+ expect(storage.getValueType("count")).toBe("static");
71
+ expect(storage.getValueType("items")).toBe("static");
72
+ expect(storage.getValueType("nested")).toBe("static");
73
+
74
+ expect(storage.peek("value")).toBe("hello");
75
+ expect(storage.peek("count")).toBe(42);
76
+ expect(storage.peek("items")).toEqual(["a", "b", "c"]);
77
+ expect(storage.peek("nested")).toEqual({ deep: "value" });
78
+ });
79
+
80
+ test("clears previous storage type when setting new value", () => {
81
+ const builder = createMockBuilder({ id: "test" });
82
+
83
+ // First set as builder
84
+ storage.set("value", builder as unknown as string);
85
+ expect(storage.getValueType("value")).toBe("builder");
86
+
87
+ // Then set as static
88
+ storage.set("value", "static");
89
+ expect(storage.getValueType("value")).toBe("static");
90
+ expect(storage.getBuilders().has("value")).toBe(false);
91
+
92
+ // Then set as mixed array
93
+ storage.set("value", [builder, "mixed"] as unknown as string);
94
+ expect(storage.getValueType("value")).toBe("mixed-array");
95
+ expect(storage.getValues().value).toBeUndefined();
96
+ });
97
+ });
98
+
99
+ describe("ValueStorage - containsBuilder() Detection", () => {
100
+ let storage: ValueStorage<TestType>;
101
+
102
+ beforeEach(() => {
103
+ storage = new ValueStorage<TestType>();
104
+ });
105
+
106
+ test("detects direct FluentBuilder", () => {
107
+ const builder = createMockBuilder({ id: "test" });
108
+
109
+ storage.set("builder", builder);
110
+
111
+ expect(storage.getValueType("builder")).toBe("builder");
112
+ });
113
+
114
+ test("detects FluentBuilder in nested object", () => {
115
+ const builder = createMockBuilder({ id: "test" });
116
+ const nested = {
117
+ level1: {
118
+ level2: {
119
+ builder,
120
+ },
121
+ },
122
+ };
123
+
124
+ storage.set("nested", nested as unknown as { deep: string });
125
+
126
+ expect(storage.getValueType("nested")).toBe("builder");
127
+ });
128
+
129
+ test("handles circular references without infinite loop", () => {
130
+ const obj: Record<string, unknown> = { value: "test" };
131
+ obj.circular = obj;
132
+
133
+ // Should not throw or hang
134
+ expect(() => {
135
+ storage.set("nested", obj as unknown as { deep: string });
136
+ }).not.toThrow();
137
+ });
138
+
139
+ test("returns false for plain objects", () => {
140
+ const plainObj = { a: 1, b: "string", c: [1, 2, 3] };
141
+
142
+ storage.set("nested", plainObj as unknown as { deep: string });
143
+
144
+ expect(storage.getValueType("nested")).toBe("static");
145
+ });
146
+
147
+ test("ignores objects with custom prototypes", () => {
148
+ class CustomClass {
149
+ value = "test";
150
+ }
151
+ const customInstance = new CustomClass();
152
+
153
+ // Objects with custom prototypes are treated as static
154
+ // (they won't be checked for nested builders)
155
+ storage.set("nested", customInstance as unknown as { deep: string });
156
+
157
+ expect(storage.getValueType("nested")).toBe("static");
158
+ });
159
+ });
160
+
161
+ describe("ValueStorage - Utility Methods", () => {
162
+ let storage: ValueStorage<TestType>;
163
+
164
+ beforeEach(() => {
165
+ storage = new ValueStorage<TestType>();
166
+ });
167
+
168
+ describe("has()", () => {
169
+ test("returns true for values in static storage", () => {
170
+ storage.set("value", "test");
171
+ expect(storage.has("value")).toBe(true);
172
+ });
173
+
174
+ test("returns true for values in builders storage", () => {
175
+ const builder = createMockBuilder({ id: "test" });
176
+ storage.set("builder", builder);
177
+ expect(storage.has("builder")).toBe(true);
178
+ });
179
+
180
+ test("returns true for values in mixedArrays storage", () => {
181
+ const builder = createMockBuilder({ id: "test" });
182
+ storage.set("mixedArray", [builder, "static"]);
183
+ expect(storage.has("mixedArray")).toBe(true);
184
+ });
185
+
186
+ test("returns false for unset values", () => {
187
+ expect(storage.has("value")).toBe(false);
188
+ });
189
+ });
190
+
191
+ describe("peek()", () => {
192
+ test("returns static value", () => {
193
+ storage.set("value", "hello");
194
+ expect(storage.peek("value")).toBe("hello");
195
+ });
196
+
197
+ test("returns mixed array", () => {
198
+ const builder = createMockBuilder({ id: "test" });
199
+ storage.set("mixedArray", [builder, "static"]);
200
+ expect(storage.peek("mixedArray")).toEqual([builder, "static"]);
201
+ });
202
+
203
+ test("returns undefined for builder", () => {
204
+ const builder = createMockBuilder({ id: "test" });
205
+ storage.set("builder", builder);
206
+ expect(storage.peek("builder")).toBeUndefined();
207
+ });
208
+
209
+ test("returns undefined for unset value", () => {
210
+ expect(storage.peek("value")).toBeUndefined();
211
+ });
212
+ });
213
+
214
+ describe("peekBuilder()", () => {
215
+ test("returns builder when exists", () => {
216
+ const builder = createMockBuilder({ id: "test" });
217
+ storage.set("builder", builder);
218
+ expect(storage.peekBuilder("builder")).toBe(builder);
219
+ });
220
+
221
+ test("returns undefined when no builder", () => {
222
+ storage.set("value", "static");
223
+ expect(storage.peekBuilder("value")).toBeUndefined();
224
+ });
225
+
226
+ test("returns undefined for unset value", () => {
227
+ expect(storage.peekBuilder("builder")).toBeUndefined();
228
+ });
229
+
230
+ test("returns undefined for object containing builder (not direct builder)", () => {
231
+ const builder = createMockBuilder({ id: "test" });
232
+ storage.set("nested", { wrapper: builder } as unknown as {
233
+ deep: string;
234
+ });
235
+ // peekBuilder returns the builder only if it's a direct FluentBuilder
236
+ // If it's an object containing a builder, peekBuilder returns undefined
237
+ expect(storage.peekBuilder("nested")).toBeUndefined();
238
+ });
239
+ });
240
+
241
+ describe("getValueType()", () => {
242
+ test("returns 'static' for static values", () => {
243
+ storage.set("value", "test");
244
+ expect(storage.getValueType("value")).toBe("static");
245
+ });
246
+
247
+ test("returns 'builder' for builders", () => {
248
+ const builder = createMockBuilder({ id: "test" });
249
+ storage.set("builder", builder);
250
+ expect(storage.getValueType("builder")).toBe("builder");
251
+ });
252
+
253
+ test("returns 'mixed-array' for mixed arrays", () => {
254
+ const builder = createMockBuilder({ id: "test" });
255
+ storage.set("mixedArray", [builder, "static"]);
256
+ expect(storage.getValueType("mixedArray")).toBe("mixed-array");
257
+ });
258
+
259
+ test("returns 'unset' for unset values", () => {
260
+ expect(storage.getValueType("value")).toBe("unset");
261
+ });
262
+ });
263
+
264
+ describe("unset()", () => {
265
+ test("removes from static storage", () => {
266
+ storage.set("value", "test");
267
+ storage.unset("value");
268
+ expect(storage.has("value")).toBe(false);
269
+ });
270
+
271
+ test("removes from builders storage", () => {
272
+ const builder = createMockBuilder({ id: "test" });
273
+ storage.set("builder", builder);
274
+ storage.unset("builder");
275
+ expect(storage.has("builder")).toBe(false);
276
+ });
277
+
278
+ test("removes from mixedArrays storage", () => {
279
+ const builder = createMockBuilder({ id: "test" });
280
+ storage.set("mixedArray", [builder, "static"]);
281
+ storage.unset("mixedArray");
282
+ expect(storage.has("mixedArray")).toBe(false);
283
+ });
284
+
285
+ test("removes from correct storage type", () => {
286
+ storage.set("value", "static");
287
+ storage.unset("value");
288
+
289
+ expect(storage.getValueType("value")).toBe("unset");
290
+ expect(storage.getValues().value).toBeUndefined();
291
+ expect(storage.getBuilders().has("value")).toBe(false);
292
+ expect(storage.getMixedArrays().has("value")).toBe(false);
293
+ });
294
+ });
295
+ });
296
+
297
+ describe("ValueStorage - clone()", () => {
298
+ let storage: ValueStorage<TestType>;
299
+
300
+ beforeEach(() => {
301
+ storage = new ValueStorage<TestType>();
302
+ });
303
+
304
+ test("creates independent copy of values", () => {
305
+ storage.set("value", "original");
306
+ storage.set("count", 42);
307
+
308
+ const cloned = storage.clone();
309
+
310
+ // Modify original
311
+ storage.set("value", "modified");
312
+
313
+ // Clone should be unchanged
314
+ expect(cloned.peek("value")).toBe("original");
315
+ expect(cloned.peek("count")).toBe(42);
316
+ });
317
+
318
+ test("creates new Sets for mixedArray indices", () => {
319
+ const builder = createMockBuilder({ id: "test" });
320
+ storage.set("mixedArray", [builder, "static"]);
321
+
322
+ const cloned = storage.clone();
323
+
324
+ // Get the mixed array metadata
325
+ const originalMeta = storage.getMixedArrays().get("mixedArray");
326
+ const clonedMeta = cloned.getMixedArrays().get("mixedArray");
327
+
328
+ // Should be different Set instances
329
+ expect(originalMeta?.builderIndices).not.toBe(clonedMeta?.builderIndices);
330
+ expect(originalMeta?.objectIndices).not.toBe(clonedMeta?.objectIndices);
331
+
332
+ // But should have same values
333
+ expect([...originalMeta!.builderIndices]).toEqual([
334
+ ...clonedMeta!.builderIndices,
335
+ ]);
336
+ });
337
+
338
+ test("modifications to clone do not affect original", () => {
339
+ storage.set("value", "original");
340
+ const builder = createMockBuilder({ id: "test" });
341
+ storage.set("builder", builder);
342
+
343
+ const cloned = storage.clone();
344
+
345
+ // Modify cloned
346
+ cloned.set("value", "cloned-value");
347
+ cloned.unset("builder");
348
+
349
+ // Original should be unchanged
350
+ expect(storage.peek("value")).toBe("original");
351
+ expect(storage.has("builder")).toBe(true);
352
+
353
+ // Clone should have new values
354
+ expect(cloned.peek("value")).toBe("cloned-value");
355
+ expect(cloned.has("builder")).toBe(false);
356
+ });
357
+ });
358
+
359
+ describe("ValueStorage - clear()", () => {
360
+ let storage: ValueStorage<TestType>;
361
+
362
+ beforeEach(() => {
363
+ storage = new ValueStorage<TestType>();
364
+ });
365
+
366
+ test("clears all storage types", () => {
367
+ const builder = createMockBuilder({ id: "test" });
368
+
369
+ storage.set("value", "static");
370
+ storage.set("builder", builder);
371
+ storage.set("mixedArray", [builder, "static"]);
372
+
373
+ storage.clear();
374
+
375
+ expect(storage.has("value")).toBe(false);
376
+ expect(storage.has("builder")).toBe(false);
377
+ expect(storage.has("mixedArray")).toBe(false);
378
+
379
+ expect(storage.getValues()).toEqual({});
380
+ expect(storage.getBuilders().size).toBe(0);
381
+ expect(storage.getMixedArrays().size).toBe(0);
382
+ });
383
+ });
384
+
385
+ describe("ValueStorage - Constructor", () => {
386
+ test("initializes with empty storage", () => {
387
+ const storage = new ValueStorage<TestType>();
388
+
389
+ expect(storage.getValues()).toEqual({});
390
+ expect(storage.getBuilders().size).toBe(0);
391
+ expect(storage.getMixedArrays().size).toBe(0);
392
+ });
393
+
394
+ test("accepts initial values", () => {
395
+ const initial: Partial<TestType> = {
396
+ value: "initial",
397
+ count: 10,
398
+ };
399
+
400
+ const storage = new ValueStorage<TestType>(initial);
401
+
402
+ expect(storage.peek("value")).toBe("initial");
403
+ expect(storage.peek("count")).toBe(10);
404
+ });
405
+
406
+ test("initial values are copied not referenced", () => {
407
+ const initial: Partial<TestType> = {
408
+ value: "initial",
409
+ };
410
+
411
+ const storage = new ValueStorage<TestType>(initial);
412
+
413
+ // Modify initial object
414
+ initial.value = "modified";
415
+
416
+ // Storage should be unchanged
417
+ expect(storage.peek("value")).toBe("initial");
418
+ });
419
+ });
420
+
421
+ describe("ValueStorage - Array Handling", () => {
422
+ let storage: ValueStorage<TestType>;
423
+
424
+ beforeEach(() => {
425
+ storage = new ValueStorage<TestType>();
426
+ });
427
+
428
+ test("static arrays go to values storage", () => {
429
+ storage.set("items", ["a", "b", "c"]);
430
+
431
+ expect(storage.getValueType("items")).toBe("static");
432
+ expect(storage.peek("items")).toEqual(["a", "b", "c"]);
433
+ });
434
+
435
+ test("arrays with only builders go to mixedArrays", () => {
436
+ const builder1 = createMockBuilder({ id: "1" });
437
+ const builder2 = createMockBuilder({ id: "2" });
438
+
439
+ storage.set("mixedArray", [builder1, builder2]);
440
+
441
+ expect(storage.getValueType("mixedArray")).toBe("mixed-array");
442
+
443
+ const meta = storage.getMixedArrays().get("mixedArray");
444
+ expect(meta?.builderIndices.has(0)).toBe(true);
445
+ expect(meta?.builderIndices.has(1)).toBe(true);
446
+ });
447
+
448
+ test("arrays with nested builders go to mixedArrays", () => {
449
+ const builder = createMockBuilder({ id: "test" });
450
+ const objWithBuilder = { wrapper: builder };
451
+
452
+ storage.set("mixedArray", [objWithBuilder, "static"]);
453
+
454
+ expect(storage.getValueType("mixedArray")).toBe("mixed-array");
455
+
456
+ const meta = storage.getMixedArrays().get("mixedArray");
457
+ expect(meta?.objectIndices.has(0)).toBe(true);
458
+ });
459
+ });