@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,715 @@
1
+ import { test, vitest, describe, beforeEach, expect } from "vitest";
2
+ import type { Flow, NavigationFlowViewState } from "@player-ui/types";
3
+ import type { FlowController } from "../controllers";
4
+ import TrackBindingPlugin from "./helpers/binding.plugin";
5
+ import type { InProgressState } from "../types";
6
+ import { Player } from "..";
7
+ import { ActionExpPlugin } from "./helpers/action-exp.plugin";
8
+
9
+ const minimal: Flow = {
10
+ id: "minimal-flow",
11
+ views: [
12
+ {
13
+ id: "view-1",
14
+ type: "view",
15
+ label: {
16
+ asset: {
17
+ id: "action-label",
18
+ type: "text",
19
+ value: "Clicked {{count}} times",
20
+ },
21
+ },
22
+ },
23
+ {
24
+ id: "action",
25
+ type: "action",
26
+ exp: "{{count}} = {{count}} + 1",
27
+ label: {
28
+ asset: {
29
+ id: "action-label",
30
+ type: "text",
31
+ value: "Clicked {{count}} times",
32
+ },
33
+ },
34
+ },
35
+ ],
36
+ data: {
37
+ count: 0,
38
+ },
39
+ navigation: {
40
+ BEGIN: "FLOW_1",
41
+ FLOW_1: {
42
+ startState: "VIEW_1",
43
+ VIEW_1: {
44
+ state_type: "VIEW",
45
+ ref: "view-1",
46
+ transitions: {
47
+ Next: "VIEW_2",
48
+ },
49
+ },
50
+ VIEW_2: {
51
+ state_type: "VIEW",
52
+ onStart: "{{count}} = {{count}} + 1",
53
+ ref: "action",
54
+ transitions: {
55
+ "*": "END_Done",
56
+ },
57
+ },
58
+ END: {
59
+ state_type: "END",
60
+ outcome: "done",
61
+ },
62
+ },
63
+ },
64
+ };
65
+
66
+ describe("state node expression tests", () => {
67
+ let player: Player;
68
+ let flowController: FlowController | undefined;
69
+
70
+ beforeEach(() => {
71
+ player = new Player({
72
+ plugins: [new ActionExpPlugin()],
73
+ });
74
+ player.hooks.flowController.tap("test", (fc) => {
75
+ flowController = fc;
76
+ });
77
+ });
78
+
79
+ // helpers
80
+ const state = () => player.getState() as InProgressState;
81
+ const getView = () => state().controllers.view.currentView?.lastUpdate;
82
+
83
+ test("evaluates onStart expression", async () => {
84
+ player.start(minimal as any);
85
+
86
+ expect(getView()).toStrictEqual({
87
+ id: "view-1",
88
+ type: "view",
89
+ label: {
90
+ asset: {
91
+ id: "action-label",
92
+ type: "text",
93
+ value: "Clicked 0 times",
94
+ },
95
+ },
96
+ });
97
+
98
+ flowController?.transition("Next");
99
+
100
+ expect(getView()).toStrictEqual({
101
+ id: "action",
102
+ type: "action",
103
+ exp: "{{count}} = {{count}} + 1",
104
+ label: {
105
+ asset: {
106
+ id: "action-label",
107
+ type: "text",
108
+ value: "Clicked 1 times",
109
+ },
110
+ },
111
+ });
112
+
113
+ state().controllers.expression.evaluate(getView()?.exp);
114
+
115
+ await vitest.waitFor(() =>
116
+ expect(getView()).toStrictEqual({
117
+ id: "action",
118
+ type: "action",
119
+ exp: "{{count}} = {{count}} + 1",
120
+ label: {
121
+ asset: {
122
+ id: "action-label",
123
+ type: "text",
124
+ value: "Clicked 2 times",
125
+ },
126
+ },
127
+ }),
128
+ );
129
+ });
130
+
131
+ test("evaluates exp for action nodes", async () => {
132
+ player.start({
133
+ ...minimal,
134
+ views: [
135
+ {
136
+ id: "view-1",
137
+ type: "action",
138
+ label: {
139
+ asset: {
140
+ id: "action-label",
141
+ type: "text",
142
+ value: "Clicked {{count}} times",
143
+ },
144
+ },
145
+ },
146
+ {
147
+ id: "view-2",
148
+ type: "view",
149
+ label: {
150
+ asset: {
151
+ id: "action-label",
152
+ type: "text",
153
+ value: "yay",
154
+ },
155
+ },
156
+ },
157
+ ],
158
+ data: {
159
+ ...minimal.data,
160
+ viewRef: "initial-view",
161
+ },
162
+ navigation: {
163
+ BEGIN: "FLOW_1",
164
+ FLOW_1: {
165
+ startState: "VIEW_1",
166
+ VIEW_1: {
167
+ state_type: "ACTION",
168
+ exp: "{{viewref}} = 'VIEW_2'",
169
+ transitions: {
170
+ "*": "{{viewref}}",
171
+ },
172
+ },
173
+ VIEW_2: {
174
+ state_type: "VIEW",
175
+ ref: "view-2",
176
+ transitions: {
177
+ "*": "END_Done",
178
+ },
179
+ },
180
+ END: {
181
+ state_type: "END",
182
+ outcome: "done",
183
+ },
184
+ },
185
+ },
186
+ });
187
+
188
+ await vitest.waitFor(() => {
189
+ const currentFlowState = state().controllers.flow.current?.currentState
190
+ ?.value as NavigationFlowViewState;
191
+ expect(currentFlowState.ref).toBe("view-2");
192
+ });
193
+ });
194
+
195
+ test("evaluates onEnd expression", () => {
196
+ const updatedContent = {
197
+ ...minimal,
198
+ navigation: {
199
+ BEGIN: "FLOW_1",
200
+ FLOW_1: {
201
+ startState: "VIEW_1",
202
+ VIEW_1: {
203
+ state_type: "VIEW",
204
+ onEnd: "{{count}} = {{count}} + 1",
205
+ ref: "view-1",
206
+ transitions: {
207
+ Next: "VIEW_2",
208
+ },
209
+ },
210
+ VIEW_2: {
211
+ state_type: "VIEW",
212
+ ref: "action",
213
+ transitions: {
214
+ "*": "END_Done",
215
+ },
216
+ },
217
+ END: {
218
+ state_type: "END",
219
+ outcome: "done",
220
+ },
221
+ },
222
+ },
223
+ };
224
+
225
+ player.start(updatedContent as any);
226
+
227
+ expect(getView()).toStrictEqual({
228
+ id: "view-1",
229
+ type: "view",
230
+ label: {
231
+ asset: {
232
+ id: "action-label",
233
+ type: "text",
234
+ value: "Clicked 0 times",
235
+ },
236
+ },
237
+ });
238
+
239
+ flowController?.transition("Next");
240
+
241
+ expect(getView()).toStrictEqual({
242
+ id: "action",
243
+ type: "action",
244
+ exp: "{{count}} = {{count}} + 1",
245
+ label: {
246
+ asset: {
247
+ id: "action-label",
248
+ type: "text",
249
+ value: "Clicked 1 times",
250
+ },
251
+ },
252
+ });
253
+ });
254
+
255
+ test("evaluates onStart/onEnd expressions for action nodes", async () => {
256
+ player.start({
257
+ ...minimal,
258
+ navigation: {
259
+ BEGIN: "FLOW_1",
260
+ FLOW_1: {
261
+ startState: "ACTION_1",
262
+ ACTION_1: {
263
+ state_type: "ACTION",
264
+ exp: "{{count}} = 99",
265
+ onStart: "{{count}} = {{count}} + 1",
266
+ onEnd: "{{count}} = {{count}} + 1",
267
+ transitions: {
268
+ "*": "VIEW_1",
269
+ },
270
+ },
271
+ VIEW_1: {
272
+ state_type: "VIEW",
273
+ ref: "view-1",
274
+ transitions: {
275
+ "*": "EXTERNAL_1",
276
+ },
277
+ },
278
+ EXTERNAL_1: {
279
+ state_type: "EXTERNAL",
280
+ ref: "external-1",
281
+ transitions: {},
282
+ },
283
+ },
284
+ },
285
+ });
286
+
287
+ await vitest.waitFor(() =>
288
+ expect(state().controllers.flow.current?.currentState?.name).toBe(
289
+ "VIEW_1",
290
+ ),
291
+ );
292
+
293
+ /**
294
+ * Expected eval order:
295
+ * 1. onStart
296
+ * 2. exp
297
+ * 3. onEnd
298
+ */
299
+ await vitest.waitFor(() =>
300
+ expect(getView()).toStrictEqual({
301
+ id: "view-1",
302
+ type: "view",
303
+ label: {
304
+ asset: {
305
+ id: "action-label",
306
+ type: "text",
307
+ value: "Clicked 100 times",
308
+ },
309
+ },
310
+ }),
311
+ );
312
+ });
313
+
314
+ test("evaluates onEnd expressions last", async () => {
315
+ player.start({
316
+ ...minimal,
317
+ data: {
318
+ ...minimal.data,
319
+ viewRef: "initial-view",
320
+ },
321
+ navigation: {
322
+ BEGIN: "FLOW_1",
323
+ FLOW_1: {
324
+ startState: "ACTION_1",
325
+ ACTION_1: {
326
+ state_type: "ACTION",
327
+ exp: "{{viewRef}} = 'view-exp'",
328
+ onStart: "{{viewRef}} = 'view-onStart'",
329
+ onEnd: "{{viewRef}} = 'view-1'",
330
+ transitions: {
331
+ "*": "VIEW_1",
332
+ },
333
+ },
334
+ VIEW_1: {
335
+ state_type: "VIEW",
336
+ ref: "view-1",
337
+ transitions: {
338
+ "*": "EXTERNAL_1",
339
+ },
340
+ },
341
+ EXTERNAL_1: {
342
+ state_type: "EXTERNAL",
343
+ ref: "external-1",
344
+ transitions: {},
345
+ },
346
+ },
347
+ },
348
+ });
349
+
350
+ await vitest.waitFor(() =>
351
+ expect(state().controllers.flow.current?.currentState?.name).toBe(
352
+ "VIEW_1",
353
+ ),
354
+ );
355
+
356
+ /**
357
+ * Expected eval order:
358
+ * 1. onStart
359
+ * 2. exp
360
+ * 3. onEnd
361
+ */
362
+ expect(getView()).toStrictEqual({
363
+ id: "view-1",
364
+ type: "view",
365
+ label: {
366
+ asset: {
367
+ id: "action-label",
368
+ type: "text",
369
+ value: "Clicked 0 times",
370
+ },
371
+ },
372
+ });
373
+ });
374
+
375
+ test("evaluates onEnd before transition", () => {
376
+ player.start({
377
+ ...minimal,
378
+ data: {
379
+ ...minimal.data,
380
+ viewRef: "initial-view",
381
+ },
382
+ navigation: {
383
+ BEGIN: "FLOW_1",
384
+ FLOW_1: {
385
+ startState: "ACTION_1",
386
+ ACTION_1: {
387
+ state_type: "ACTION",
388
+ exp: "{{viewRef}} = 'view-exp'",
389
+ onStart: "{{viewRef}} = 'view-onStart'",
390
+ onEnd: "{{viewRef}} = 'VIEW_1'",
391
+ transitions: {
392
+ Next: "{{viewRef}}",
393
+ },
394
+ },
395
+ VIEW_1: {
396
+ state_type: "VIEW",
397
+ ref: "view-1",
398
+ transitions: {},
399
+ },
400
+ },
401
+ },
402
+ });
403
+
404
+ flowController?.transition("Next");
405
+
406
+ /**
407
+ * Expected eval order:
408
+ * 1. onStart
409
+ * 2. exp
410
+ * 3. onEnd
411
+ */
412
+ expect(getView()).toStrictEqual({
413
+ id: "view-1",
414
+ type: "view",
415
+ label: {
416
+ asset: {
417
+ id: "action-label",
418
+ type: "text",
419
+ value: "Clicked 0 times",
420
+ },
421
+ },
422
+ });
423
+ });
424
+
425
+ test("triggers onStart before resolving view IDs", () => {
426
+ player.start({
427
+ id: "resolve-view-flow",
428
+ views: [
429
+ {
430
+ id: "view-1",
431
+ type: "view",
432
+ },
433
+ {
434
+ id: "view-2",
435
+ type: "view",
436
+ },
437
+ ],
438
+ data: {
439
+ viewRef: "view-1",
440
+ },
441
+ navigation: {
442
+ BEGIN: "FLOW_1",
443
+ FLOW_1: {
444
+ startState: "VIEW_1",
445
+ VIEW_1: {
446
+ state_type: "VIEW",
447
+ onStart: "{{viewRef}} = 'view-2'",
448
+ ref: "{{viewRef}}",
449
+ transitions: {
450
+ Next: "END",
451
+ },
452
+ },
453
+ END: {
454
+ state_type: "END",
455
+ outcome: "done",
456
+ },
457
+ },
458
+ },
459
+ });
460
+ const currentFlowState = state().controllers.flow.current?.currentState
461
+ ?.value as NavigationFlowViewState;
462
+ expect(currentFlowState.ref).toBe("view-2");
463
+ });
464
+
465
+ const validationFlow: Flow = {
466
+ id: "validation-flow",
467
+ views: [
468
+ {
469
+ id: "view-1",
470
+ type: "view",
471
+ label: {
472
+ asset: {
473
+ id: "action-label",
474
+ type: "text",
475
+ value: "Clicked {{count}} times",
476
+ },
477
+ },
478
+ alreadyInvalidData: {
479
+ asset: {
480
+ type: "invalid",
481
+ id: "thing4",
482
+ binding: "data.thing4",
483
+ },
484
+ },
485
+ },
486
+ {
487
+ id: "action",
488
+ type: "action",
489
+ exp: "{{count}} = {{count}} + 1",
490
+ label: {
491
+ asset: {
492
+ id: "action-label",
493
+ type: "text",
494
+ value: "Clicked {{count}} times",
495
+ },
496
+ },
497
+ },
498
+ ],
499
+ data: {
500
+ count: 0,
501
+ data: {
502
+ thing4: "frodo",
503
+ },
504
+ },
505
+ schema: {
506
+ ROOT: {
507
+ data: {
508
+ type: "DataType",
509
+ },
510
+ },
511
+ DataType: {
512
+ thing4: {
513
+ type: "CatType",
514
+ validation: [
515
+ {
516
+ type: "names",
517
+ names: ["sam"],
518
+ },
519
+ ],
520
+ },
521
+ },
522
+ },
523
+ navigation: {
524
+ BEGIN: "FLOW_1",
525
+ FLOW_1: {
526
+ startState: "VIEW_1",
527
+ VIEW_1: {
528
+ state_type: "VIEW",
529
+ onStart: "{{count}} = {{count}} + 1",
530
+ onEnd: "{{count}} = {{count}} + 1",
531
+ ref: "view-1",
532
+ transitions: {
533
+ "*": "VIEW_2",
534
+ },
535
+ },
536
+ VIEW_2: {
537
+ state_type: "VIEW",
538
+ onStart: "{{count}} = {{count}} + 1",
539
+ ref: "action",
540
+ transitions: {
541
+ "*": "END_1",
542
+ },
543
+ },
544
+ END_1: {
545
+ state_type: "END",
546
+ outcome: "test",
547
+ },
548
+ },
549
+ },
550
+ };
551
+
552
+ test("prevents expression evaluation on unsuccessful validation", () => {
553
+ player = new Player({
554
+ plugins: [new TrackBindingPlugin()],
555
+ });
556
+
557
+ player.start(validationFlow);
558
+
559
+ // Starts out with nothing
560
+ expect(getView()?.alreadyInvalidData.asset.validation).toBe(undefined);
561
+
562
+ // Evals initial onStart
563
+ expect(getView()).toStrictEqual({
564
+ id: "view-1",
565
+ type: "view",
566
+ label: {
567
+ asset: {
568
+ id: "action-label",
569
+ type: "text",
570
+ value: "Clicked 1 times",
571
+ },
572
+ },
573
+ alreadyInvalidData: {
574
+ asset: {
575
+ type: "invalid",
576
+ id: "thing4",
577
+ binding: "data.thing4",
578
+ },
579
+ },
580
+ });
581
+
582
+ // Try to transition
583
+ state().controllers.flow.transition("foo");
584
+
585
+ // Stays on the same view
586
+ expect(state().controllers.flow.current?.currentState?.name).toBe("VIEW_1");
587
+ expect(getView()?.label.asset).toStrictEqual({
588
+ id: "action-label",
589
+ type: "text",
590
+ value: "Clicked 1 times",
591
+ });
592
+
593
+ // Fix the error.
594
+ state().controllers.data.set([["data.thing4", "sam"]]);
595
+
596
+ // Try to transition again
597
+ state().controllers.flow.transition("foo");
598
+ // Should work now that there's no error
599
+ expect(state().controllers.flow.current?.currentState?.name).toBe("VIEW_2");
600
+ // Evals previous onEnd and next onStart
601
+ expect(getView()?.label.asset).toStrictEqual({
602
+ id: "action-label",
603
+ type: "text",
604
+ value: "Clicked 3 times",
605
+ });
606
+ });
607
+
608
+ test("only evals exp prop for object", () => {
609
+ const flowWithObjExp = {
610
+ ...minimal,
611
+ navigation: {
612
+ ...minimal.navigation,
613
+ FLOW_1: {
614
+ ...(minimal.navigation as any).FLOW_1,
615
+ onStart: {
616
+ _comment: "this should not fail",
617
+ exp: "{{count}} = 11",
618
+ },
619
+ },
620
+ },
621
+ };
622
+
623
+ player.start(flowWithObjExp);
624
+
625
+ expect(state().controllers.data.get("count")).toBe(11);
626
+ });
627
+ });
628
+
629
+ describe("view update scheduling", () => {
630
+ test("schedules view updates", async () => {
631
+ const player = new Player();
632
+ player.start(minimal as any);
633
+
634
+ const view = (player.getState() as InProgressState).controllers.view
635
+ .currentView?.lastUpdate;
636
+
637
+ expect(view).toStrictEqual({
638
+ id: "view-1",
639
+ type: "view",
640
+ label: {
641
+ asset: {
642
+ id: "action-label",
643
+ type: "text",
644
+ value: "Clicked 0 times",
645
+ },
646
+ },
647
+ });
648
+
649
+ (player.getState() as InProgressState).controllers.data.set([["count", 1]]);
650
+
651
+ await vitest.waitFor(() => {
652
+ expect(
653
+ (player.getState() as InProgressState).controllers.view.currentView
654
+ ?.lastUpdate,
655
+ ).toStrictEqual({
656
+ id: "view-1",
657
+ type: "view",
658
+ label: {
659
+ asset: {
660
+ id: "action-label",
661
+ type: "text",
662
+ value: "Clicked 1 times",
663
+ },
664
+ },
665
+ });
666
+ });
667
+
668
+ (player.getState() as InProgressState).controllers.data.set(
669
+ [["count", 2]],
670
+ { silent: true },
671
+ );
672
+
673
+ // Add a delay here to flush any queued updates
674
+ await new Promise((resolve) => {
675
+ setTimeout(resolve, 10);
676
+ });
677
+
678
+ expect(
679
+ (player.getState() as InProgressState).controllers.view.currentView
680
+ ?.lastUpdate,
681
+ ).toStrictEqual({
682
+ id: "view-1",
683
+ type: "view",
684
+ label: {
685
+ asset: {
686
+ id: "action-label",
687
+ type: "text",
688
+ value: "Clicked 1 times",
689
+ },
690
+ },
691
+ });
692
+
693
+ // non-silent update an unrelated field, should trigger an update to the original
694
+ (player.getState() as InProgressState).controllers.data.set([
695
+ ["not-count", 1],
696
+ ]);
697
+
698
+ await vitest.waitFor(() => {
699
+ expect(
700
+ (player.getState() as InProgressState).controllers.view.currentView
701
+ ?.lastUpdate,
702
+ ).toStrictEqual({
703
+ id: "view-1",
704
+ type: "view",
705
+ label: {
706
+ asset: {
707
+ id: "action-label",
708
+ type: "text",
709
+ value: "Clicked 2 times",
710
+ },
711
+ },
712
+ });
713
+ });
714
+ });
715
+ });