@player-ui/player 0.8.0--canary.307.9621 → 0.8.0-next.0

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 (214) hide show
  1. package/dist/Player.native.js +11630 -0
  2. package/dist/Player.native.js.map +1 -0
  3. package/dist/cjs/index.cjs +5626 -0
  4. package/dist/cjs/index.cjs.map +1 -0
  5. package/dist/{index.esm.js → index.legacy-esm.js} +2044 -1667
  6. package/dist/{index.cjs.js → index.mjs} +2052 -1761
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +29 -63
  9. package/src/__tests__/data.test.ts +498 -0
  10. package/src/__tests__/flow.test.ts +312 -0
  11. package/src/__tests__/helpers/action-exp.plugin.ts +22 -0
  12. package/src/__tests__/helpers/actions.flow.ts +67 -0
  13. package/src/__tests__/helpers/binding.plugin.ts +125 -0
  14. package/src/__tests__/helpers/expression.plugin.ts +88 -0
  15. package/src/__tests__/helpers/transform-plugin.ts +19 -0
  16. package/src/__tests__/helpers/validation.flow.ts +56 -0
  17. package/src/__tests__/player.test.ts +597 -0
  18. package/src/__tests__/string-resolver.test.ts +186 -0
  19. package/src/__tests__/validation.test.ts +3555 -0
  20. package/src/__tests__/view.test.ts +715 -0
  21. package/src/binding/__tests__/binding.test.ts +113 -0
  22. package/src/binding/__tests__/index.test.ts +208 -0
  23. package/src/binding/__tests__/resolver.test.ts +83 -0
  24. package/src/binding/binding.ts +6 -6
  25. package/src/binding/index.ts +34 -34
  26. package/src/binding/resolver.ts +19 -19
  27. package/src/binding/utils.ts +7 -7
  28. package/src/binding-grammar/__tests__/parser.test.ts +64 -0
  29. package/src/binding-grammar/__tests__/test-utils/ast-cases.ts +198 -0
  30. package/src/binding-grammar/__tests__/test-utils/perf-test.ts +66 -0
  31. package/src/binding-grammar/ast.ts +11 -11
  32. package/src/binding-grammar/custom/index.ts +19 -22
  33. package/src/binding-grammar/ebnf/index.ts +20 -21
  34. package/src/binding-grammar/ebnf/types.ts +13 -13
  35. package/src/binding-grammar/index.ts +4 -4
  36. package/src/binding-grammar/parsimmon/index.ts +14 -14
  37. package/src/controllers/constants/__tests__/index.test.ts +106 -0
  38. package/src/controllers/constants/index.ts +3 -3
  39. package/src/controllers/constants/utils.ts +4 -4
  40. package/src/controllers/data/controller.ts +22 -22
  41. package/src/controllers/data/index.ts +1 -1
  42. package/src/controllers/data/utils.ts +7 -7
  43. package/src/controllers/flow/__tests__/controller.test.ts +195 -0
  44. package/src/controllers/flow/__tests__/flow.test.ts +381 -0
  45. package/src/controllers/flow/controller.ts +13 -13
  46. package/src/controllers/flow/flow.ts +23 -23
  47. package/src/controllers/flow/index.ts +2 -2
  48. package/src/controllers/index.ts +5 -5
  49. package/src/controllers/validation/binding-tracker.ts +71 -59
  50. package/src/controllers/validation/controller.ts +104 -104
  51. package/src/controllers/validation/index.ts +2 -2
  52. package/src/controllers/view/asset-transform.ts +20 -20
  53. package/src/controllers/view/controller.ts +27 -27
  54. package/src/controllers/view/index.ts +4 -4
  55. package/src/controllers/view/store.ts +3 -3
  56. package/src/controllers/view/types.ts +7 -7
  57. package/src/data/__tests__/__snapshots__/dependency-tracker.test.ts.snap +64 -0
  58. package/src/data/__tests__/dependency-tracker.test.ts +146 -0
  59. package/src/data/__tests__/local-model.test.ts +46 -0
  60. package/src/data/__tests__/model.test.ts +78 -0
  61. package/src/data/dependency-tracker.ts +16 -16
  62. package/src/data/index.ts +4 -4
  63. package/src/data/local-model.ts +6 -6
  64. package/src/data/model.ts +17 -17
  65. package/src/data/noop-model.ts +1 -1
  66. package/src/expressions/__tests__/__snapshots__/parser.test.ts.snap +854 -0
  67. package/src/expressions/__tests__/evaluator-functions.test.ts +47 -0
  68. package/src/expressions/__tests__/evaluator.test.ts +410 -0
  69. package/src/expressions/__tests__/parser.test.ts +115 -0
  70. package/src/expressions/__tests__/utils.test.ts +44 -0
  71. package/src/expressions/evaluator-functions.ts +6 -6
  72. package/src/expressions/evaluator.ts +71 -67
  73. package/src/expressions/index.ts +4 -4
  74. package/src/expressions/parser.ts +102 -105
  75. package/src/expressions/types.ts +29 -21
  76. package/src/expressions/utils.ts +32 -21
  77. package/src/index.ts +13 -13
  78. package/src/logger/__tests__/consoleLogger.test.ts +46 -0
  79. package/src/logger/__tests__/noopLogger.test.ts +13 -0
  80. package/src/logger/__tests__/proxyLogger.test.ts +31 -0
  81. package/src/logger/__tests__/tapableLogger.test.ts +41 -0
  82. package/src/logger/consoleLogger.ts +9 -9
  83. package/src/logger/index.ts +5 -5
  84. package/src/logger/noopLogger.ts +1 -1
  85. package/src/logger/proxyLogger.ts +6 -6
  86. package/src/logger/tapableLogger.ts +7 -7
  87. package/src/logger/types.ts +2 -2
  88. package/src/player.ts +60 -58
  89. package/src/plugins/default-exp-plugin.ts +10 -10
  90. package/src/plugins/default-view-plugin.ts +29 -0
  91. package/src/plugins/flow-exp-plugin.ts +6 -6
  92. package/src/schema/__tests__/schema.test.ts +243 -0
  93. package/src/schema/index.ts +2 -2
  94. package/src/schema/schema.ts +24 -24
  95. package/src/schema/types.ts +4 -4
  96. package/src/string-resolver/__tests__/index.test.ts +361 -0
  97. package/src/string-resolver/index.ts +17 -17
  98. package/src/types.ts +17 -17
  99. package/src/utils/__tests__/replaceParams.test.ts +33 -0
  100. package/src/utils/index.ts +1 -1
  101. package/src/utils/replaceParams.ts +1 -1
  102. package/src/validator/__tests__/binding-map-splice.test.ts +53 -0
  103. package/src/validator/__tests__/validation-middleware.test.ts +127 -0
  104. package/src/validator/binding-map-splice.ts +5 -5
  105. package/src/validator/index.ts +4 -4
  106. package/src/validator/registry.ts +1 -1
  107. package/src/validator/types.ts +13 -13
  108. package/src/validator/validation-middleware.ts +15 -15
  109. package/src/view/__tests__/view.immutable.test.ts +269 -0
  110. package/src/view/__tests__/view.test.ts +959 -0
  111. package/src/view/builder/index.test.ts +69 -0
  112. package/src/view/builder/index.ts +3 -3
  113. package/src/view/index.ts +5 -5
  114. package/src/view/parser/__tests__/__snapshots__/parser.test.ts.snap +394 -0
  115. package/src/view/parser/__tests__/parser.test.ts +264 -0
  116. package/src/view/parser/index.ts +43 -33
  117. package/src/view/parser/types.ts +11 -11
  118. package/src/view/parser/utils.ts +5 -5
  119. package/src/view/plugins/__tests__/__snapshots__/template.test.ts.snap +278 -0
  120. package/src/view/plugins/__tests__/applicability.test.ts +265 -0
  121. package/src/view/plugins/__tests__/string.test.ts +122 -0
  122. package/src/view/plugins/__tests__/template.test.ts +724 -0
  123. package/src/view/plugins/applicability.ts +19 -19
  124. package/src/view/plugins/index.ts +4 -5
  125. package/src/view/plugins/options.ts +1 -1
  126. package/src/view/plugins/string-resolver.ts +22 -22
  127. package/src/view/plugins/switch.ts +22 -23
  128. package/src/view/plugins/template-plugin.ts +26 -27
  129. package/src/view/resolver/__tests__/dependencies.test.ts +321 -0
  130. package/src/view/resolver/__tests__/edgecases.test.ts +626 -0
  131. package/src/view/resolver/index.ts +42 -42
  132. package/src/view/resolver/types.ts +21 -20
  133. package/src/view/resolver/utils.ts +9 -9
  134. package/src/view/view.ts +32 -22
  135. package/types/binding/binding.d.ts +50 -0
  136. package/types/binding/index.d.ts +29 -0
  137. package/types/binding/resolver.d.ts +26 -0
  138. package/types/binding/utils.d.ts +12 -0
  139. package/types/binding-grammar/ast.d.ts +67 -0
  140. package/types/binding-grammar/custom/index.d.ts +4 -0
  141. package/types/binding-grammar/ebnf/index.d.ts +4 -0
  142. package/types/binding-grammar/ebnf/types.d.ts +75 -0
  143. package/types/binding-grammar/index.d.ts +5 -0
  144. package/types/binding-grammar/parsimmon/index.d.ts +4 -0
  145. package/types/controllers/constants/index.d.ts +45 -0
  146. package/types/controllers/constants/utils.d.ts +6 -0
  147. package/types/controllers/data/controller.d.ts +45 -0
  148. package/types/controllers/data/index.d.ts +2 -0
  149. package/types/controllers/data/utils.d.ts +14 -0
  150. package/types/controllers/flow/controller.d.ts +25 -0
  151. package/types/controllers/flow/flow.d.ts +50 -0
  152. package/types/controllers/flow/index.d.ts +3 -0
  153. package/types/controllers/index.d.ts +6 -0
  154. package/types/controllers/validation/binding-tracker.d.ts +32 -0
  155. package/types/controllers/validation/controller.d.ts +151 -0
  156. package/types/controllers/validation/index.d.ts +3 -0
  157. package/types/controllers/view/asset-transform.d.ts +19 -0
  158. package/types/controllers/view/controller.d.ts +37 -0
  159. package/types/controllers/view/index.d.ts +5 -0
  160. package/types/controllers/view/store.d.ts +20 -0
  161. package/types/controllers/view/types.d.ts +16 -0
  162. package/types/data/dependency-tracker.d.ts +49 -0
  163. package/types/data/index.d.ts +5 -0
  164. package/types/data/local-model.d.ts +16 -0
  165. package/types/data/model.d.ts +86 -0
  166. package/types/data/noop-model.d.ts +13 -0
  167. package/types/expressions/evaluator-functions.d.ts +15 -0
  168. package/types/expressions/evaluator.d.ts +52 -0
  169. package/types/expressions/index.d.ts +5 -0
  170. package/types/expressions/parser.d.ts +10 -0
  171. package/types/expressions/types.d.ts +144 -0
  172. package/types/expressions/utils.d.ts +12 -0
  173. package/types/index.d.ts +14 -0
  174. package/types/logger/consoleLogger.d.ts +17 -0
  175. package/types/logger/index.d.ts +6 -0
  176. package/types/logger/noopLogger.d.ts +10 -0
  177. package/types/logger/proxyLogger.d.ts +15 -0
  178. package/types/logger/tapableLogger.d.ts +23 -0
  179. package/types/logger/types.d.ts +6 -0
  180. package/types/player.d.ts +101 -0
  181. package/types/plugins/default-exp-plugin.d.ts +9 -0
  182. package/types/plugins/default-view-plugin.d.ts +9 -0
  183. package/types/plugins/flow-exp-plugin.d.ts +11 -0
  184. package/types/schema/index.d.ts +3 -0
  185. package/types/schema/schema.d.ts +36 -0
  186. package/types/schema/types.d.ts +38 -0
  187. package/types/string-resolver/index.d.ts +30 -0
  188. package/types/types.d.ts +73 -0
  189. package/types/utils/index.d.ts +2 -0
  190. package/types/utils/replaceParams.d.ts +9 -0
  191. package/types/validator/binding-map-splice.d.ts +10 -0
  192. package/types/validator/index.d.ts +5 -0
  193. package/types/validator/registry.d.ts +11 -0
  194. package/types/validator/types.d.ts +53 -0
  195. package/types/validator/validation-middleware.d.ts +36 -0
  196. package/types/view/builder/index.d.ts +35 -0
  197. package/types/view/index.d.ts +6 -0
  198. package/types/view/parser/index.d.ts +52 -0
  199. package/types/view/parser/types.d.ts +109 -0
  200. package/types/view/parser/utils.d.ts +6 -0
  201. package/types/view/plugins/applicability.d.ts +10 -0
  202. package/types/view/plugins/index.d.ts +5 -0
  203. package/types/view/plugins/options.d.ts +4 -0
  204. package/types/view/plugins/string-resolver.d.ts +13 -0
  205. package/types/view/plugins/switch.d.ts +14 -0
  206. package/types/view/plugins/template-plugin.d.ts +33 -0
  207. package/types/view/resolver/index.d.ts +73 -0
  208. package/types/view/resolver/types.d.ts +129 -0
  209. package/types/view/resolver/utils.d.ts +11 -0
  210. package/types/view/view.d.ts +37 -0
  211. package/dist/index.d.ts +0 -1814
  212. package/dist/player.dev.js +0 -11472
  213. package/dist/player.prod.js +0 -2
  214. package/src/view/plugins/plugin.ts +0 -21
@@ -0,0 +1,626 @@
1
+ import { describe, it, expect, vitest } from "vitest";
2
+ import { replaceAt, set, omit } from "timm";
3
+ import { BindingParser } from "../../../binding";
4
+ import { ExpressionEvaluator } from "../../../expressions";
5
+ import { LocalModel, withParser } from "../../../data";
6
+ import { SchemaController } from "../../../schema";
7
+ import type { Logger } from "../../../logger";
8
+ import { TapableLogger } from "../../../logger";
9
+ import { Resolver } from "..";
10
+ import type { Node } from "../../parser";
11
+ import { NodeType, Parser } from "../../parser";
12
+ import { StringResolverPlugin } from "../../plugins";
13
+
14
+ describe("Dynamic AST Transforms", () => {
15
+ const content = {
16
+ id: "main-view",
17
+ type: "questionAnswer",
18
+ title: [
19
+ {
20
+ asset: {
21
+ id: "title",
22
+ type: "text",
23
+ value: "Cool Page",
24
+ },
25
+ },
26
+ ],
27
+ primaryInfo: [
28
+ {
29
+ asset: {
30
+ id: "subtitle",
31
+ type: "text",
32
+ value: "{{year}}",
33
+ },
34
+ },
35
+ ],
36
+ };
37
+
38
+ it("Dynamically added Nodes are properly resolved/cached on rerender", () => {
39
+ const model = new LocalModel({
40
+ year: "2021",
41
+ });
42
+ const parser = new Parser();
43
+ const bindingParser = new BindingParser();
44
+ const inputBinding = bindingParser.parse("year");
45
+ const rootNode = parser.parseObject(content);
46
+
47
+ const resolver = new Resolver(rootNode!, {
48
+ model,
49
+ parseBinding: bindingParser.parse.bind(bindingParser),
50
+ parseNode: parser.parseObject.bind(parser),
51
+ evaluator: new ExpressionEvaluator({
52
+ model: withParser(model, bindingParser.parse),
53
+ }),
54
+ schema: new SchemaController(),
55
+ });
56
+
57
+ // basic transform to change the asset
58
+ resolver.hooks.beforeResolve.tap("test-plugin", (node) => {
59
+ if (
60
+ node?.type === NodeType.Asset ||
61
+ node?.type === NodeType.View ||
62
+ node?.type === NodeType.Value
63
+ ) {
64
+ let newNode = node;
65
+
66
+ newNode.children?.forEach((child, i) => {
67
+ if (child.path.length === 1) {
68
+ // We have a child for this key
69
+ // Check if it's an array and shouldn't be
70
+ const { value: childNode } = child;
71
+ if (childNode.type === "multi-node") {
72
+ if (childNode.values.length === 1) {
73
+ // If there's only 1 node, no need for a collection, just up-level the asset that's there
74
+ const firstChild = childNode.values[0];
75
+ newNode = set(
76
+ newNode,
77
+ "children",
78
+ replaceAt(newNode.children ?? [], i, {
79
+ path: child.path,
80
+ value: {
81
+ ...firstChild,
82
+ },
83
+ }),
84
+ );
85
+ }
86
+ }
87
+ }
88
+ });
89
+ if (newNode !== node) {
90
+ // We updated something, set the children of the newNode to have the correct parent
91
+ newNode.children?.forEach((child) => {
92
+ // Don't worry about mutating here any new children are ones we created above
93
+ // eslint-disable-next-line no-param-reassign
94
+ child.value.parent = newNode;
95
+ });
96
+ }
97
+
98
+ return newNode;
99
+ }
100
+
101
+ return node;
102
+ });
103
+
104
+ new StringResolverPlugin().applyResolver(resolver);
105
+
106
+ const firstUpdate = resolver.update();
107
+ expect(firstUpdate).toStrictEqual({
108
+ id: "main-view",
109
+ type: "questionAnswer",
110
+ title: {
111
+ asset: {
112
+ id: "title",
113
+ type: "text",
114
+ value: "Cool Page",
115
+ },
116
+ },
117
+ primaryInfo: {
118
+ asset: {
119
+ id: "subtitle",
120
+ type: "text",
121
+ value: "2021",
122
+ },
123
+ },
124
+ });
125
+
126
+ model.set([[inputBinding, "2022"]]);
127
+ const secondUpdate = resolver.update(new Set([inputBinding]));
128
+ expect(secondUpdate).toStrictEqual({
129
+ id: "main-view",
130
+ type: "questionAnswer",
131
+ title: {
132
+ asset: {
133
+ id: "title",
134
+ type: "text",
135
+ value: "Cool Page",
136
+ },
137
+ },
138
+ primaryInfo: {
139
+ asset: {
140
+ id: "subtitle",
141
+ type: "text",
142
+ value: "2022",
143
+ },
144
+ },
145
+ });
146
+ });
147
+
148
+ it("Nodes are properly cached on rerender", () => {
149
+ const model = new LocalModel({
150
+ year: "2021",
151
+ });
152
+ const parser = new Parser();
153
+ const bindingParser = new BindingParser();
154
+ const inputBinding = bindingParser.parse("year");
155
+ const rootNode = parser.parseObject(content);
156
+
157
+ const resolver = new Resolver(rootNode!, {
158
+ model,
159
+ parseBinding: bindingParser.parse.bind(bindingParser),
160
+ parseNode: parser.parseObject.bind(parser),
161
+ evaluator: new ExpressionEvaluator({
162
+ model: withParser(model, bindingParser.parse),
163
+ }),
164
+ schema: new SchemaController(),
165
+ });
166
+
167
+ resolver.update();
168
+
169
+ const resolveCache = resolver.getResolveCache();
170
+
171
+ resolver.update(new Set([inputBinding]));
172
+
173
+ const newResolveCache = resolver.getResolveCache();
174
+
175
+ expect(resolveCache.size).toBe(newResolveCache.size);
176
+
177
+ // The cached items between each re-render should stay the same
178
+ for (const [k, v] of resolveCache) {
179
+ const excludingUpdated = omit(v, "updated");
180
+
181
+ expect(newResolveCache.has(k)).toBe(true);
182
+ expect(newResolveCache.get(k)).toMatchObject(excludingUpdated);
183
+ }
184
+ });
185
+
186
+ it("Cached node points to the correct parent node", () => {
187
+ const view = {
188
+ id: "main-view",
189
+ type: "questionAnswer",
190
+ title: [
191
+ {
192
+ asset: {
193
+ id: "title",
194
+ type: "text",
195
+ value: "Cool Page",
196
+ },
197
+ },
198
+ ],
199
+ primaryInfo: [
200
+ {
201
+ asset: {
202
+ id: "input",
203
+ type: "input",
204
+ value: "{{year}}",
205
+ label: {
206
+ asset: {
207
+ id: "label",
208
+ type: "text",
209
+ value: "label",
210
+ },
211
+ },
212
+ },
213
+ },
214
+ ],
215
+ };
216
+
217
+ const model = new LocalModel({
218
+ year: "2021",
219
+ });
220
+ const parser = new Parser();
221
+ const bindingParser = new BindingParser();
222
+ const inputBinding = bindingParser.parse("year");
223
+ const rootNode = parser.parseObject(view);
224
+
225
+ const resolver = new Resolver(rootNode!, {
226
+ model,
227
+ parseBinding: bindingParser.parse.bind(bindingParser),
228
+ parseNode: parser.parseObject.bind(parser),
229
+ evaluator: new ExpressionEvaluator({
230
+ model: withParser(model, bindingParser.parse),
231
+ }),
232
+ schema: new SchemaController(),
233
+ });
234
+
235
+ let inputNode: Node.Node | undefined;
236
+ let labelNode: Node.Node | undefined;
237
+
238
+ resolver.hooks.beforeResolve.tap("test", (node, options) => {
239
+ if (node?.type === "asset" && node.value.id === "input") {
240
+ // Add to dependencies
241
+ options.data.model.get(inputBinding);
242
+ }
243
+
244
+ return node;
245
+ });
246
+
247
+ resolver.hooks.afterResolve.tap("test", (value, node) => {
248
+ if (node.type === "asset") {
249
+ const { id } = node.value;
250
+
251
+ if (id === "input") inputNode = node;
252
+
253
+ if (id === "label") labelNode = node;
254
+ }
255
+
256
+ return value;
257
+ });
258
+
259
+ resolver.update();
260
+
261
+ model.set([[inputBinding, "2022"]]);
262
+
263
+ resolver.update(new Set([inputBinding]));
264
+
265
+ // Check that label (which is cached) still points to the correct parent node.
266
+ expect(labelNode?.parent).toBe(inputNode ?? {});
267
+ });
268
+
269
+ it("Fixes parent references when beforeResolve taps make changes", () => {
270
+ const model = new LocalModel({
271
+ year: "2021",
272
+ });
273
+ const parser = new Parser();
274
+ const bindingParser = new BindingParser();
275
+ const rootNode = parser.parseObject(content);
276
+
277
+ const resolver = new Resolver(rootNode!, {
278
+ model,
279
+ parseBinding: bindingParser.parse.bind(bindingParser),
280
+ parseNode: parser.parseObject.bind(parser),
281
+ evaluator: new ExpressionEvaluator({
282
+ model: withParser(model, bindingParser.parse),
283
+ }),
284
+ schema: new SchemaController(),
285
+ });
286
+
287
+ let parent;
288
+ resolver.hooks.beforeResolve.tap("test", (node) => {
289
+ if (node?.type !== NodeType.Asset || node.value.id !== "subtitle") {
290
+ return node;
291
+ }
292
+
293
+ parent = node.parent;
294
+ return {
295
+ ...node,
296
+ parent: undefined,
297
+ };
298
+ });
299
+
300
+ let resolvedNode: Node.Node | undefined;
301
+ resolver.hooks.afterResolve.tap("test", (resolvedValue, node) => {
302
+ if (node?.type === NodeType.Asset && node.value.id === "subtitle") {
303
+ resolvedNode = node;
304
+ }
305
+
306
+ return resolvedValue;
307
+ });
308
+
309
+ resolver.update();
310
+
311
+ expect(parent).not.toBeUndefined();
312
+ expect(resolvedNode).not.toBeUndefined();
313
+ expect(resolvedNode?.parent).toBe(parent);
314
+ });
315
+ });
316
+
317
+ describe("Duplicate IDs", () => {
318
+ it("Throws an error if two assets have the same id", () => {
319
+ const content = {
320
+ id: "action",
321
+ type: "collection",
322
+ values: [
323
+ {
324
+ asset: {
325
+ id: "action-1",
326
+ type: "action",
327
+ label: {
328
+ asset: {
329
+ id: "action-label-1",
330
+ type: "text",
331
+ value: "Clicked {{count1}} times",
332
+ },
333
+ },
334
+ },
335
+ },
336
+ {
337
+ asset: {
338
+ id: "action-1",
339
+ type: "action",
340
+ label: {
341
+ asset: {
342
+ id: "action-label-2",
343
+ type: "text",
344
+ value: "Clicked {{count2}} times",
345
+ },
346
+ },
347
+ },
348
+ },
349
+ ],
350
+ };
351
+
352
+ const model = new LocalModel({
353
+ count1: 0,
354
+ count2: 0,
355
+ });
356
+ const parser = new Parser();
357
+ const bindingParser = new BindingParser();
358
+ const rootNode = parser.parseObject(content, NodeType.View);
359
+
360
+ const logger = new TapableLogger();
361
+
362
+ const testLogger: Logger = {
363
+ trace: vitest.fn(),
364
+ debug: vitest.fn(),
365
+ info: vitest.fn(),
366
+ warn: vitest.fn(),
367
+ error: vitest.fn(),
368
+ };
369
+
370
+ logger.addHandler(testLogger);
371
+
372
+ const resolver = new Resolver(rootNode!, {
373
+ model,
374
+ parseBinding: bindingParser.parse.bind(bindingParser),
375
+ parseNode: parser.parseObject.bind(parser),
376
+ evaluator: new ExpressionEvaluator({
377
+ model: withParser(model, bindingParser.parse),
378
+ }),
379
+ schema: new SchemaController(),
380
+ logger,
381
+ });
382
+
383
+ new StringResolverPlugin().applyResolver(resolver);
384
+
385
+ const firstUpdate = resolver.update();
386
+
387
+ expect(testLogger.error).toBeCalledTimes(1);
388
+ expect(testLogger.error).toBeCalledWith(
389
+ "Cache conflict: Found Asset/View nodes that have conflicting ids: action-1, may cause cache issues.",
390
+ );
391
+ (testLogger.error as jest.Mock).mockClear();
392
+
393
+ expect(firstUpdate).toStrictEqual({
394
+ id: "action",
395
+ type: "collection",
396
+ values: [
397
+ {
398
+ asset: {
399
+ id: "action-1",
400
+ type: "action",
401
+ label: {
402
+ asset: {
403
+ id: "action-label-1",
404
+ type: "text",
405
+ value: "Clicked 0 times",
406
+ },
407
+ },
408
+ },
409
+ },
410
+ {
411
+ asset: {
412
+ id: "action-1",
413
+ type: "action",
414
+ label: {
415
+ asset: {
416
+ id: "action-label-2",
417
+ type: "text",
418
+ value: "Clicked 0 times",
419
+ },
420
+ },
421
+ },
422
+ },
423
+ ],
424
+ });
425
+
426
+ resolver.update();
427
+
428
+ expect(testLogger.error).not.toBeCalled();
429
+ });
430
+
431
+ it("Throws a warning if two views have the same id", () => {
432
+ const content = {
433
+ id: "action",
434
+ type: "collection",
435
+ values: [
436
+ {
437
+ id: "value-1",
438
+ binding: "count1",
439
+ },
440
+ {
441
+ id: "value-1",
442
+ binding: "count2",
443
+ },
444
+ ],
445
+ };
446
+
447
+ const model = new LocalModel({
448
+ count1: 0,
449
+ count2: 0,
450
+ });
451
+ const parser = new Parser();
452
+ const bindingParser = new BindingParser();
453
+ const rootNode = parser.parseObject(content, NodeType.View);
454
+
455
+ const logger = new TapableLogger();
456
+
457
+ const testLogger: Logger = {
458
+ trace: vitest.fn(),
459
+ debug: vitest.fn(),
460
+ info: vitest.fn(),
461
+ warn: vitest.fn(),
462
+ error: vitest.fn(),
463
+ };
464
+
465
+ logger.addHandler(testLogger);
466
+
467
+ const resolver = new Resolver(rootNode!, {
468
+ model,
469
+ parseBinding: bindingParser.parse.bind(bindingParser),
470
+ parseNode: parser.parseObject.bind(parser),
471
+ evaluator: new ExpressionEvaluator({
472
+ model: withParser(model, bindingParser.parse),
473
+ }),
474
+ schema: new SchemaController(),
475
+ logger,
476
+ });
477
+
478
+ new StringResolverPlugin().applyResolver(resolver);
479
+
480
+ const firstUpdate = resolver.update();
481
+
482
+ expect(testLogger.info).toBeCalledTimes(1);
483
+ expect(testLogger.info).toBeCalledWith(
484
+ "Cache conflict: Found Value nodes that have conflicting ids: value-1, may cause cache issues. To improve performance make value node IDs globally unique.",
485
+ );
486
+ (testLogger.info as jest.Mock).mockClear();
487
+ expect(firstUpdate).toStrictEqual(content);
488
+
489
+ resolver.update();
490
+
491
+ expect(testLogger.info).not.toHaveBeenCalled();
492
+ });
493
+ });
494
+
495
+ describe("AST caching", () => {
496
+ it("skipping resolution of nodes should still repopulate AST map for itself and children", () => {
497
+ const content = {
498
+ id: "collection",
499
+ type: "collection",
500
+ values: [
501
+ {
502
+ id: "value-1",
503
+ type: "collection",
504
+ values: [
505
+ {
506
+ id: "value-1-1",
507
+ },
508
+ ],
509
+ },
510
+ ],
511
+ };
512
+
513
+ const model = new LocalModel();
514
+ const parser = new Parser();
515
+ const bindingParser = new BindingParser();
516
+ const rootNode = parser.parseObject(content, NodeType.View);
517
+ const resolver = new Resolver(rootNode!, {
518
+ model,
519
+ parseBinding: bindingParser.parse.bind(bindingParser),
520
+ parseNode: parser.parseObject.bind(parser),
521
+ evaluator: new ExpressionEvaluator({
522
+ model: withParser(model, bindingParser.parse),
523
+ }),
524
+ schema: new SchemaController(),
525
+ });
526
+
527
+ const resolvedNodes: any[] = [];
528
+ resolver.hooks.afterResolve.tap("afterResolve", (value, node) => {
529
+ resolvedNodes.push(node);
530
+ return value;
531
+ });
532
+
533
+ resolver.hooks.skipResolve.tap(
534
+ "skipResolve",
535
+ () => resolvedNodes.length >= 5,
536
+ );
537
+
538
+ new StringResolverPlugin().applyResolver(resolver);
539
+
540
+ expect(resolvedNodes).toHaveLength(0);
541
+
542
+ resolver.update();
543
+
544
+ const frozenResolvedNodes = [...resolvedNodes];
545
+ expect(frozenResolvedNodes).toHaveLength(5);
546
+
547
+ const sourceNodes = frozenResolvedNodes.map((node) => {
548
+ const sourceNode = resolver.getSourceNode(node);
549
+ expect(sourceNode).toBeDefined();
550
+ return sourceNode;
551
+ });
552
+
553
+ resolver.update();
554
+
555
+ frozenResolvedNodes.forEach((node, index) => {
556
+ const sourceNode = resolver.getSourceNode(node);
557
+ expect(sourceNode).toBeDefined();
558
+ expect(sourceNode).toStrictEqual(sourceNodes[index]!);
559
+ });
560
+ });
561
+ });
562
+
563
+ describe("Root AST Immutability", () => {
564
+ it("modifying nodes in beforeResolve should not impact the original tree", () => {
565
+ const content = {
566
+ id: "action",
567
+ type: "collection",
568
+ values: [
569
+ {
570
+ id: "value-1",
571
+ binding: "count1",
572
+ },
573
+ {
574
+ id: "value-1",
575
+ binding: "count2",
576
+ },
577
+ ],
578
+ };
579
+
580
+ const model = new LocalModel();
581
+ const parser = new Parser();
582
+ const bindingParser = new BindingParser();
583
+ const rootNode = parser.parseObject(content, NodeType.View);
584
+ const resolver = new Resolver(rootNode!, {
585
+ model,
586
+ parseBinding: bindingParser.parse.bind(bindingParser),
587
+ parseNode: parser.parseObject.bind(parser),
588
+ evaluator: new ExpressionEvaluator({
589
+ model: withParser(model, bindingParser.parse),
590
+ }),
591
+ schema: new SchemaController(),
592
+ });
593
+ let finalNode;
594
+
595
+ resolver.hooks.beforeResolve.tap("beforeResolve", (node) => {
596
+ if (node?.type !== NodeType.View) return node;
597
+
598
+ // eslint-disable-next-line no-param-reassign
599
+ node.value.type = "not-collection";
600
+ return node;
601
+ });
602
+
603
+ resolver.hooks.afterResolve.tap("afterResolve", (value, node) => {
604
+ if (node?.type === NodeType.View) {
605
+ finalNode = node;
606
+ }
607
+
608
+ return value;
609
+ });
610
+
611
+ resolver.update();
612
+
613
+ expect(rootNode).toBe(resolver.root);
614
+ expect(rootNode).not.toBe(finalNode);
615
+ expect(finalNode).toMatchObject({
616
+ value: {
617
+ type: "not-collection",
618
+ },
619
+ });
620
+ expect(rootNode).toMatchObject({
621
+ value: {
622
+ type: "collection",
623
+ },
624
+ });
625
+ });
626
+ });