@player-tools/fluent 0.13.0--canary.221.5662
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.
- package/dist/cjs/index.cjs +2257 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.legacy-esm.js +2143 -0
- package/dist/index.mjs +2143 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +36 -0
- package/src/core/base-builder/__tests__/fluent-builder-base.test.ts +2257 -0
- package/src/core/base-builder/__tests__/id-generator.test.ts +658 -0
- package/src/core/base-builder/__tests__/registry.test.ts +411 -0
- package/src/core/base-builder/__tests__/switch.test.ts +501 -0
- package/src/core/base-builder/__tests__/template.test.ts +449 -0
- package/src/core/base-builder/__tests__/value-extraction.test.ts +200 -0
- package/src/core/base-builder/conditional/index.ts +64 -0
- package/src/core/base-builder/context.ts +151 -0
- package/src/core/base-builder/fluent-builder-base.ts +261 -0
- package/src/core/base-builder/guards.ts +137 -0
- package/src/core/base-builder/id/generator.ts +286 -0
- package/src/core/base-builder/id/registry.ts +152 -0
- package/src/core/base-builder/index.ts +60 -0
- package/src/core/base-builder/resolution/path-resolver.ts +108 -0
- package/src/core/base-builder/resolution/pipeline.ts +96 -0
- package/src/core/base-builder/resolution/steps/asset-id.ts +77 -0
- package/src/core/base-builder/resolution/steps/asset-wrappers.ts +64 -0
- package/src/core/base-builder/resolution/steps/builders.ts +85 -0
- package/src/core/base-builder/resolution/steps/mixed-arrays.ts +117 -0
- package/src/core/base-builder/resolution/steps/static-values.ts +35 -0
- package/src/core/base-builder/resolution/steps/switches.ts +63 -0
- package/src/core/base-builder/resolution/steps/templates.ts +30 -0
- package/src/core/base-builder/resolution/value-resolver.ts +308 -0
- package/src/core/base-builder/storage/auxiliary-storage.ts +82 -0
- package/src/core/base-builder/storage/value-storage.ts +280 -0
- package/src/core/base-builder/types.ts +184 -0
- package/src/core/base-builder/utils.ts +10 -0
- package/src/core/flow/__tests__/index.test.ts +292 -0
- package/src/core/flow/index.ts +141 -0
- package/src/core/index.ts +8 -0
- package/src/core/mocks/generated/action.builder.ts +109 -0
- package/src/core/mocks/generated/choice.builder.ts +161 -0
- package/src/core/mocks/generated/choiceItem.builder.ts +133 -0
- package/src/core/mocks/generated/collection.builder.ts +117 -0
- package/src/core/mocks/generated/index.ts +7 -0
- package/src/core/mocks/generated/info.builder.ts +80 -0
- package/src/core/mocks/generated/input.builder.ts +75 -0
- package/src/core/mocks/generated/text.builder.ts +63 -0
- package/src/core/mocks/index.ts +1 -0
- package/src/core/mocks/types/action.ts +92 -0
- package/src/core/mocks/types/choice.ts +129 -0
- package/src/core/mocks/types/collection.ts +140 -0
- package/src/core/mocks/types/info.ts +7 -0
- package/src/core/mocks/types/input.ts +7 -0
- package/src/core/mocks/types/text.ts +5 -0
- package/src/core/schema/__tests__/index.test.ts +127 -0
- package/src/core/schema/index.ts +195 -0
- package/src/core/schema/types.ts +7 -0
- package/src/core/switch/__tests__/index.test.ts +156 -0
- package/src/core/switch/index.ts +76 -0
- package/src/core/tagged-template/README.md +448 -0
- package/src/core/tagged-template/__tests__/extract-bindings-from-schema.test.ts +207 -0
- package/src/core/tagged-template/__tests__/index.test.ts +190 -0
- package/src/core/tagged-template/__tests__/schema-std-integration.test.ts +580 -0
- package/src/core/tagged-template/binding.ts +95 -0
- package/src/core/tagged-template/expression.ts +92 -0
- package/src/core/tagged-template/extract-bindings-from-schema.ts +120 -0
- package/src/core/tagged-template/index.ts +5 -0
- package/src/core/tagged-template/std.ts +472 -0
- package/src/core/tagged-template/types.ts +123 -0
- package/src/core/template/__tests__/index.test.ts +380 -0
- package/src/core/template/index.ts +191 -0
- package/src/core/utils/index.ts +160 -0
- package/src/fp/README.md +411 -0
- package/src/fp/__tests__/index.test.ts +1178 -0
- package/src/fp/index.ts +386 -0
- package/src/gen/common.ts +2 -0
- package/src/gen/plugin.mjs +315 -0
- package/src/index.ts +5 -0
- package/src/types.ts +203 -0
- package/types/core/base-builder/conditional/index.d.ts +21 -0
- package/types/core/base-builder/context.d.ts +39 -0
- package/types/core/base-builder/fluent-builder-base.d.ts +132 -0
- package/types/core/base-builder/guards.d.ts +58 -0
- package/types/core/base-builder/id/generator.d.ts +69 -0
- package/types/core/base-builder/id/registry.d.ts +93 -0
- package/types/core/base-builder/index.d.ts +8 -0
- package/types/core/base-builder/resolution/path-resolver.d.ts +15 -0
- package/types/core/base-builder/resolution/pipeline.d.ts +25 -0
- package/types/core/base-builder/resolution/steps/asset-id.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/asset-wrappers.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/builders.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/mixed-arrays.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/static-values.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/switches.d.ts +15 -0
- package/types/core/base-builder/resolution/steps/templates.d.ts +14 -0
- package/types/core/base-builder/resolution/value-resolver.d.ts +37 -0
- package/types/core/base-builder/storage/auxiliary-storage.d.ts +50 -0
- package/types/core/base-builder/storage/value-storage.d.ts +82 -0
- package/types/core/base-builder/types.d.ts +141 -0
- package/types/core/base-builder/utils.d.ts +2 -0
- package/types/core/flow/index.d.ts +23 -0
- package/types/core/index.d.ts +8 -0
- package/types/core/mocks/index.d.ts +2 -0
- package/types/core/mocks/types/action.d.ts +58 -0
- package/types/core/mocks/types/choice.d.ts +95 -0
- package/types/core/mocks/types/collection.d.ts +102 -0
- package/types/core/mocks/types/info.d.ts +7 -0
- package/types/core/mocks/types/input.d.ts +7 -0
- package/types/core/mocks/types/text.d.ts +5 -0
- package/types/core/schema/index.d.ts +34 -0
- package/types/core/schema/types.d.ts +5 -0
- package/types/core/switch/index.d.ts +21 -0
- package/types/core/tagged-template/binding.d.ts +19 -0
- package/types/core/tagged-template/expression.d.ts +11 -0
- package/types/core/tagged-template/extract-bindings-from-schema.d.ts +7 -0
- package/types/core/tagged-template/index.d.ts +6 -0
- package/types/core/tagged-template/std.d.ts +174 -0
- package/types/core/tagged-template/types.d.ts +69 -0
- package/types/core/template/index.d.ts +97 -0
- package/types/core/utils/index.d.ts +47 -0
- package/types/fp/index.d.ts +149 -0
- package/types/gen/common.d.ts +3 -0
- package/types/index.d.ts +3 -0
- package/types/types.d.ts +163 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from "vitest";
|
|
2
|
+
import type { Template, Asset } from "@player-ui/types";
|
|
3
|
+
import { template, isTemplate, TEMPLATE_MARKER } from "../../template";
|
|
4
|
+
import {
|
|
5
|
+
type BaseBuildContext,
|
|
6
|
+
type SlotBranch,
|
|
7
|
+
resetGlobalIdSet,
|
|
8
|
+
} from "../index";
|
|
9
|
+
import { text } from "../../mocks";
|
|
10
|
+
import { binding as b } from "../../tagged-template";
|
|
11
|
+
|
|
12
|
+
// Mock BaseBuildContext
|
|
13
|
+
const mockParentCtx: BaseBuildContext = {
|
|
14
|
+
parentId: "parent-1",
|
|
15
|
+
branch: {
|
|
16
|
+
type: "slot",
|
|
17
|
+
name: "test",
|
|
18
|
+
} as SlotBranch,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("template integration with base-builder", () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
resetGlobalIdSet();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("create a basic template configuration", () => {
|
|
27
|
+
const result = template({
|
|
28
|
+
data: "list.of.names",
|
|
29
|
+
output: "values",
|
|
30
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
31
|
+
})(mockParentCtx);
|
|
32
|
+
|
|
33
|
+
const expected: Template<{ asset: Asset<"text"> }> = {
|
|
34
|
+
data: "list.of.names",
|
|
35
|
+
output: "values",
|
|
36
|
+
value: {
|
|
37
|
+
asset: {
|
|
38
|
+
id: "parent-1-test-_index_-text",
|
|
39
|
+
type: "text",
|
|
40
|
+
value: "{{list.of.names._index_}}",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
expect(result).toEqual(expected);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("create a template with tagged template binding", () => {
|
|
49
|
+
const result = template({
|
|
50
|
+
data: b`list.of.names`,
|
|
51
|
+
output: "values",
|
|
52
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
53
|
+
})(mockParentCtx);
|
|
54
|
+
|
|
55
|
+
const expected: Template<{ asset: Asset<"text"> }> = {
|
|
56
|
+
data: "{{list.of.names}}",
|
|
57
|
+
output: "values",
|
|
58
|
+
value: {
|
|
59
|
+
asset: {
|
|
60
|
+
id: "parent-1-test-_index_-text",
|
|
61
|
+
type: "text",
|
|
62
|
+
value: "{{list.of.names._index_}}",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
expect(result).toEqual(expected);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("include dynamic flag when specified", () => {
|
|
71
|
+
const result = template({
|
|
72
|
+
data: "list.of.names",
|
|
73
|
+
output: "values",
|
|
74
|
+
dynamic: true,
|
|
75
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
76
|
+
})(mockParentCtx);
|
|
77
|
+
|
|
78
|
+
const expected: Template<{ asset: Asset<"text"> }> = {
|
|
79
|
+
data: "list.of.names",
|
|
80
|
+
output: "values",
|
|
81
|
+
dynamic: true,
|
|
82
|
+
value: {
|
|
83
|
+
asset: {
|
|
84
|
+
id: "parent-1-test-_index_-text",
|
|
85
|
+
type: "text",
|
|
86
|
+
value: "{{list.of.names._index_}}",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
expect(result).toEqual(expected);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("not include dynamic flag when it is false", () => {
|
|
95
|
+
const result = template({
|
|
96
|
+
data: "list.of.names",
|
|
97
|
+
output: "values",
|
|
98
|
+
dynamic: false,
|
|
99
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
100
|
+
})(mockParentCtx);
|
|
101
|
+
|
|
102
|
+
const expected: Template<{ asset: Asset<"text"> }> = {
|
|
103
|
+
data: "list.of.names",
|
|
104
|
+
output: "values",
|
|
105
|
+
value: {
|
|
106
|
+
asset: {
|
|
107
|
+
id: "parent-1-test-_index_-text",
|
|
108
|
+
type: "text",
|
|
109
|
+
value: "{{list.of.names._index_}}",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
expect(result).toEqual(expected);
|
|
115
|
+
expect(result).not.toHaveProperty("dynamic");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("pass the correct parent context to the value function", () => {
|
|
119
|
+
let capturedParentCtx: BaseBuildContext | null = null;
|
|
120
|
+
|
|
121
|
+
const valueWithCapture = (parentCtx: BaseBuildContext): Asset<"text"> => {
|
|
122
|
+
capturedParentCtx = parentCtx;
|
|
123
|
+
return {
|
|
124
|
+
id: "test",
|
|
125
|
+
type: "text",
|
|
126
|
+
value: "test",
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
template({
|
|
131
|
+
data: "list.of.names",
|
|
132
|
+
output: "values",
|
|
133
|
+
value: valueWithCapture,
|
|
134
|
+
})(mockParentCtx);
|
|
135
|
+
|
|
136
|
+
expect(capturedParentCtx).toEqual({
|
|
137
|
+
parentId: "parent-1-test",
|
|
138
|
+
branch: {
|
|
139
|
+
type: "template",
|
|
140
|
+
depth: 0,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Simulation of a multiple templates scenario (we test the output structure, not runtime behavior)
|
|
146
|
+
test("support the structure for multiple templates with the same output property", () => {
|
|
147
|
+
const template1 = template({
|
|
148
|
+
data: "list.of.names",
|
|
149
|
+
output: "values",
|
|
150
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
151
|
+
})(mockParentCtx);
|
|
152
|
+
|
|
153
|
+
const template2 = template({
|
|
154
|
+
data: "list.of.other-names",
|
|
155
|
+
output: "values",
|
|
156
|
+
value: text().withValue(b`list.of.other-names._index_`),
|
|
157
|
+
})(mockParentCtx);
|
|
158
|
+
|
|
159
|
+
expect(template1.output).toEqual(template2.output);
|
|
160
|
+
expect(template1.output).toBe("values");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Test complex nested asset structures
|
|
164
|
+
test("handle complex nested asset structures", () => {
|
|
165
|
+
interface CollectionAsset extends Asset<"collection"> {
|
|
166
|
+
items: Array<{ asset: Asset<"text"> }>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const collectionAsset = (): CollectionAsset => ({
|
|
170
|
+
id: "collection-_index_",
|
|
171
|
+
type: "collection",
|
|
172
|
+
items: [
|
|
173
|
+
{
|
|
174
|
+
asset: {
|
|
175
|
+
id: "item-_index_-0",
|
|
176
|
+
type: "text",
|
|
177
|
+
value: "{{list.of.names._index_.first}}",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
asset: {
|
|
182
|
+
id: "item-_index_-1",
|
|
183
|
+
type: "text",
|
|
184
|
+
value: "{{list.of.names._index_.last}}",
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const result = template({
|
|
191
|
+
data: "list.of.names",
|
|
192
|
+
output: "collections",
|
|
193
|
+
value: collectionAsset,
|
|
194
|
+
})(mockParentCtx);
|
|
195
|
+
|
|
196
|
+
const expected: Template<{ asset: CollectionAsset }> = {
|
|
197
|
+
data: "list.of.names",
|
|
198
|
+
output: "collections",
|
|
199
|
+
value: {
|
|
200
|
+
asset: {
|
|
201
|
+
id: "collection-_index_",
|
|
202
|
+
type: "collection",
|
|
203
|
+
items: [
|
|
204
|
+
{
|
|
205
|
+
asset: {
|
|
206
|
+
id: "item-_index_-0",
|
|
207
|
+
type: "text",
|
|
208
|
+
value: "{{list.of.names._index_.first}}",
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
asset: {
|
|
213
|
+
id: "item-_index_-1",
|
|
214
|
+
type: "text",
|
|
215
|
+
value: "{{list.of.names._index_.last}}",
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
expect(result).toEqual(expected);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Test the structure for dynamic template functionality
|
|
227
|
+
test("create a dynamic template that updates when data changes", () => {
|
|
228
|
+
const result = template({
|
|
229
|
+
data: "list.of.names",
|
|
230
|
+
output: "values",
|
|
231
|
+
dynamic: true,
|
|
232
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
233
|
+
})(mockParentCtx);
|
|
234
|
+
|
|
235
|
+
expect(result.dynamic).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("isTemplate type guard", () => {
|
|
240
|
+
beforeEach(() => {
|
|
241
|
+
resetGlobalIdSet();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("should return true for template functions", () => {
|
|
245
|
+
const templateFn = template({
|
|
246
|
+
data: "list.of.names",
|
|
247
|
+
output: "values",
|
|
248
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(isTemplate(templateFn)).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("should return false for non-template functions", () => {
|
|
255
|
+
const regularFunction = () => ({});
|
|
256
|
+
const arrowFunction = () => "test";
|
|
257
|
+
const objectWithFunction = { fn: () => {} };
|
|
258
|
+
|
|
259
|
+
expect(isTemplate(regularFunction)).toBe(false);
|
|
260
|
+
expect(isTemplate(arrowFunction)).toBe(false);
|
|
261
|
+
expect(isTemplate(objectWithFunction.fn)).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("should return false for non-functions", () => {
|
|
265
|
+
const string = "not a function";
|
|
266
|
+
const number = 42;
|
|
267
|
+
const object = { type: "test" };
|
|
268
|
+
const nullValue = null;
|
|
269
|
+
const undefinedValue = undefined;
|
|
270
|
+
|
|
271
|
+
expect(isTemplate(string)).toBe(false);
|
|
272
|
+
expect(isTemplate(number)).toBe(false);
|
|
273
|
+
expect(isTemplate(object)).toBe(false);
|
|
274
|
+
expect(isTemplate(nullValue)).toBe(false);
|
|
275
|
+
expect(isTemplate(undefinedValue)).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("should have TEMPLATE_MARKER symbol on template functions", () => {
|
|
279
|
+
const templateFn = template({
|
|
280
|
+
data: "list.of.names",
|
|
281
|
+
output: "values",
|
|
282
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(TEMPLATE_MARKER in templateFn).toBe(true);
|
|
286
|
+
expect(
|
|
287
|
+
(templateFn as unknown as { [TEMPLATE_MARKER]: unknown })[
|
|
288
|
+
TEMPLATE_MARKER
|
|
289
|
+
],
|
|
290
|
+
).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("should not have TEMPLATE_MARKER symbol on regular functions", () => {
|
|
294
|
+
const regularFunction = () => ({});
|
|
295
|
+
|
|
296
|
+
expect(TEMPLATE_MARKER in regularFunction).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("should work with template functions that have been called", () => {
|
|
300
|
+
const templateFn = template({
|
|
301
|
+
data: "list.of.names",
|
|
302
|
+
output: "values",
|
|
303
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Call the template function
|
|
307
|
+
templateFn(mockParentCtx);
|
|
308
|
+
|
|
309
|
+
// The original function should still be identifiable as a template
|
|
310
|
+
expect(isTemplate(templateFn)).toBe(true);
|
|
311
|
+
expect(TEMPLATE_MARKER in templateFn).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("template with optional output", () => {
|
|
316
|
+
beforeEach(() => {
|
|
317
|
+
resetGlobalIdSet();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("should infer output from slot context", () => {
|
|
321
|
+
const contextWithSlot: BaseBuildContext = {
|
|
322
|
+
parentId: "parent-1",
|
|
323
|
+
branch: {
|
|
324
|
+
type: "slot",
|
|
325
|
+
name: "values-0", // This should infer "values" as the output
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const templateFn = template({
|
|
330
|
+
data: b`list.of.names`,
|
|
331
|
+
// No output provided - should be inferred
|
|
332
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const result = templateFn(contextWithSlot);
|
|
336
|
+
|
|
337
|
+
expect(result.output).toBe("values");
|
|
338
|
+
expect(result.data).toBe("{{list.of.names}}");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("should throw error when output cannot be inferred", () => {
|
|
342
|
+
const contextWithoutSlot: BaseBuildContext = {
|
|
343
|
+
parentId: "parent-1",
|
|
344
|
+
branch: {
|
|
345
|
+
type: "template",
|
|
346
|
+
depth: 0,
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const templateFn = template({
|
|
351
|
+
data: b`list.of.names`,
|
|
352
|
+
// No output provided and context doesn't allow inference
|
|
353
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(() => templateFn(contextWithoutSlot)).toThrow(
|
|
357
|
+
"Template output must be provided or inferrable from context",
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("should use explicit output when provided", () => {
|
|
362
|
+
const contextWithSlot: BaseBuildContext = {
|
|
363
|
+
parentId: "parent-1",
|
|
364
|
+
branch: {
|
|
365
|
+
type: "slot",
|
|
366
|
+
name: "values-0",
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const templateFn = template({
|
|
371
|
+
data: b`list.of.names`,
|
|
372
|
+
output: "customOutput", // Explicit output should override inference
|
|
373
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const result = templateFn(contextWithSlot);
|
|
377
|
+
|
|
378
|
+
expect(result.output).toBe("customOutput");
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe("template context creation", () => {
|
|
383
|
+
beforeEach(() => {
|
|
384
|
+
resetGlobalIdSet();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("should create template context with depth 0", () => {
|
|
388
|
+
const context: BaseBuildContext = {
|
|
389
|
+
parentId: "parent",
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const templateFn = template({
|
|
393
|
+
data: "items",
|
|
394
|
+
output: "values",
|
|
395
|
+
value: (ctx: BaseBuildContext) => {
|
|
396
|
+
// Verify the context has template branch
|
|
397
|
+
expect(ctx.branch).toEqual({ type: "template", depth: 0 });
|
|
398
|
+
expect(ctx.parentId).toBe("parent");
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
id: "test",
|
|
402
|
+
type: "text",
|
|
403
|
+
value: "test",
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
templateFn(context);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("should generate proper template IDs with _index_ placeholder", () => {
|
|
412
|
+
const context: BaseBuildContext = {
|
|
413
|
+
parentId: "parent",
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const result = template({
|
|
417
|
+
data: "items",
|
|
418
|
+
output: "values",
|
|
419
|
+
value: text({ value: "test" }),
|
|
420
|
+
})(context);
|
|
421
|
+
|
|
422
|
+
expect(result.value.asset.id).toBe("parent-_index_-text");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("should support nested template depth tracking", () => {
|
|
426
|
+
const context: BaseBuildContext = {
|
|
427
|
+
parentId: "parent",
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// First level template
|
|
431
|
+
const level1Result = template({
|
|
432
|
+
data: "items",
|
|
433
|
+
output: "values",
|
|
434
|
+
value: text({ value: "test" }),
|
|
435
|
+
})(context);
|
|
436
|
+
|
|
437
|
+
expect(level1Result.value.asset.id).toBe("parent-_index_-text");
|
|
438
|
+
|
|
439
|
+
// Simulate nested template (depth 1)
|
|
440
|
+
const nestedContext: BaseBuildContext = {
|
|
441
|
+
parentId: "parent-item",
|
|
442
|
+
branch: { type: "template", depth: 1 },
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const nestedAsset = text({ value: "nested" }).build(nestedContext);
|
|
446
|
+
|
|
447
|
+
expect(nestedAsset.id).toBe("parent-item-_index1_-text");
|
|
448
|
+
});
|
|
449
|
+
});
|
|
@@ -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,64 @@
|
|
|
1
|
+
import type { Asset } from "@player-ui/types";
|
|
2
|
+
import type { FluentBuilder, BaseBuildContext } from "../types";
|
|
3
|
+
import { isFluentBuilder, isAsset, isAssetWrapperValue } from "../guards";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a value or function to its final value
|
|
7
|
+
*
|
|
8
|
+
* Generic helper that unwraps functions to their return values.
|
|
9
|
+
* Handles both simple functions and ConditionalValue types.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveValueOrFunction<V>(value: V | (() => V)): V {
|
|
12
|
+
if (typeof value === "function" && !isFluentBuilder(value)) {
|
|
13
|
+
// SAFETY: We've checked it's a function and not a FluentBuilder,
|
|
14
|
+
// so it must be a function returning V
|
|
15
|
+
return (value as () => V)();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return value as V;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Type guard to check if a value should be wrapped in AssetWrapper format
|
|
23
|
+
*/
|
|
24
|
+
export function shouldWrapInAssetWrapper(
|
|
25
|
+
value: unknown,
|
|
26
|
+
): value is
|
|
27
|
+
| FluentBuilder<unknown, BaseBuildContext>
|
|
28
|
+
| Asset
|
|
29
|
+
| Array<FluentBuilder<unknown, BaseBuildContext> | Asset> {
|
|
30
|
+
// Don't wrap if already wrapped (has 'asset' property but not 'type')
|
|
31
|
+
if (isAssetWrapperValue(value)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Wrap FluentBuilders
|
|
36
|
+
if (isFluentBuilder(value)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Wrap Assets (objects with 'type' property)
|
|
41
|
+
if (isAsset(value)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Wrap arrays of builders/assets
|
|
46
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
47
|
+
const firstItem = value[0];
|
|
48
|
+
return isFluentBuilder(firstItem) || isAsset(firstItem);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Wraps a value in AssetWrapper format if needed
|
|
56
|
+
* This enables if() and ifElse() to work with unwrapped asset builders
|
|
57
|
+
*/
|
|
58
|
+
export function maybeWrapAsset<V>(value: V): V | { asset: V } {
|
|
59
|
+
if (shouldWrapInAssetWrapper(value)) {
|
|
60
|
+
return { asset: value };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return value;
|
|
64
|
+
}
|