@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,597 @@
1
+ import { vitest, test, expect, describe, it } from "vitest";
2
+ import { makeFlow } from "@player-ui/make-flow";
3
+
4
+ import type { ViewInstance } from "../view";
5
+ import { NodeType } from "../view";
6
+ import { Player } from "..";
7
+ import type { ViewController } from "..";
8
+ import actionsFlow from "./helpers/actions.flow";
9
+ import type { InProgressState } from "../types";
10
+ import { ActionExpPlugin } from "./helpers/action-exp.plugin";
11
+
12
+ const minimal = {
13
+ id: "minimal-player-content-response-format",
14
+ topic: "MOCK",
15
+ schema: {},
16
+ data: {},
17
+ views: [
18
+ {
19
+ actions: [
20
+ {
21
+ asset: {
22
+ id: "action-1",
23
+ type: "action",
24
+ value: "Next",
25
+ label: {
26
+ asset: {
27
+ id: "Action-Label-Next",
28
+ type: "text",
29
+ value: "Continue",
30
+ },
31
+ },
32
+ },
33
+ },
34
+ ],
35
+ id: "KitchenSink-View1",
36
+ title: {
37
+ asset: {
38
+ id: "KitchenSink-View1-Title",
39
+ type: "text",
40
+ value: "Minimal JSON Example",
41
+ },
42
+ },
43
+ type: "questionAnswer",
44
+ },
45
+ ],
46
+ navigation: {
47
+ BEGIN: "KitchenSinkFlow",
48
+ KitchenSinkFlow: {
49
+ END_Done: {
50
+ outcome: "doneWithTopic",
51
+ state_type: "END",
52
+ },
53
+ VIEW_KitchenSink_1: {
54
+ ref: "KitchenSink-View1",
55
+ state_type: "VIEW",
56
+ transitions: {
57
+ "*": "END_Done",
58
+ },
59
+ },
60
+ startState: "VIEW_KitchenSink_1",
61
+ },
62
+ },
63
+ };
64
+
65
+ test("it loads a flow", async () => {
66
+ return new Promise<void>((resolve) => {
67
+ const player = new Player();
68
+ player.hooks.viewController.tap("test", (vc: ViewController) => {
69
+ vc.hooks.view.tap("test", (view: ViewInstance) => {
70
+ view.hooks.onUpdate.tap("test", (v: any) => {
71
+ expect(v.id).toBe("KitchenSink-View1");
72
+ resolve();
73
+ });
74
+ });
75
+ });
76
+ player.start(minimal as any);
77
+ });
78
+ });
79
+
80
+ test("multiple data change only update view once", async () => {
81
+ const player = new Player();
82
+
83
+ const onUpdateCall = vitest.fn();
84
+ player.hooks.viewController.tap("test", (vc: ViewController) => {
85
+ vc.hooks.view.tap("test", (view: ViewInstance) => {
86
+ view.hooks.onUpdate.tap("test", onUpdateCall);
87
+ });
88
+ });
89
+
90
+ player.start({
91
+ id: "test-flow",
92
+ views: [
93
+ {
94
+ id: "t1",
95
+ type: "text",
96
+ value: "count is at {{count}}",
97
+ },
98
+ ],
99
+ data: {
100
+ count: 0,
101
+ },
102
+ navigation: {
103
+ BEGIN: "FLOW_1",
104
+ FLOW_1: {
105
+ startState: "VIEW_1",
106
+ VIEW_1: {
107
+ state_type: "VIEW",
108
+ ref: "t1",
109
+ transitions: {
110
+ "*": "END_Done",
111
+ },
112
+ },
113
+ END_Done: {
114
+ state_type: "END",
115
+ outcome: "done",
116
+ },
117
+ },
118
+ },
119
+ } as any);
120
+
121
+ const state = player.getState() as InProgressState;
122
+
123
+ expect(onUpdateCall).toBeCalledTimes(1);
124
+
125
+ state.controllers.data.set([["count", 1]]);
126
+ state.controllers.data.set([["count", 2]]);
127
+
128
+ await vitest.waitFor(() => expect(onUpdateCall).toBeCalledTimes(2));
129
+ });
130
+
131
+ test("it handles multiple resolutions", async () => {
132
+ const player = new Player({
133
+ plugins: [new ActionExpPlugin()],
134
+ });
135
+
136
+ player.hooks.viewController.tap("action", (vc) => {
137
+ vc.hooks.view.tap("action", (view) => {
138
+ view.hooks.resolver.tap("action", (resolver) => {
139
+ resolver.hooks.resolve.tap("action", (action, node, options) => {
140
+ if (node.type !== NodeType.Asset || node.value.type !== "action") {
141
+ return action;
142
+ }
143
+
144
+ return {
145
+ ...action,
146
+ run() {
147
+ options.evaluate(action.exp);
148
+ },
149
+ };
150
+ });
151
+ });
152
+ });
153
+ });
154
+
155
+ player.start(actionsFlow as any);
156
+
157
+ /** A helper to get the current view */
158
+ const getView = () =>
159
+ (player.getState() as InProgressState).controllers.view.currentView
160
+ ?.lastUpdate;
161
+
162
+ const state = player.getState() as InProgressState;
163
+ let lastUpdate = state.controllers.view.currentView?.lastUpdate;
164
+ lastUpdate?.values?.[1].asset.run();
165
+ await vitest.waitFor(() => {
166
+ lastUpdate = getView();
167
+
168
+ expect(lastUpdate?.values[1].asset.label.asset.value).toBe(
169
+ "Clicked 1 times",
170
+ );
171
+ expect(lastUpdate?.values[2].asset.label.asset.value).toBe(
172
+ "Clicked 0 times",
173
+ );
174
+ });
175
+
176
+ lastUpdate = state.controllers.view.currentView?.lastUpdate;
177
+
178
+ lastUpdate?.values[2].asset.run();
179
+
180
+ await vitest.waitFor(() => {
181
+ lastUpdate = getView();
182
+
183
+ expect(lastUpdate?.values[1].asset.label.asset.value).toBe(
184
+ "Clicked 1 times",
185
+ );
186
+ expect(lastUpdate?.values[2].asset.label.asset.value).toBe(
187
+ "Clicked 1 times",
188
+ );
189
+ });
190
+ });
191
+
192
+ test("it inserts data into the view", async () => {
193
+ const player = new Player({
194
+ plugins: [new ActionExpPlugin()],
195
+ });
196
+ player.start({
197
+ id: "action-with-expression",
198
+ views: [
199
+ {
200
+ id: "action",
201
+ type: "action",
202
+ exp: "{{count}} = {{count}} + 1",
203
+ label: {
204
+ asset: {
205
+ id: "action-label",
206
+ type: "text",
207
+ value: "Clicked {{count}} times",
208
+ },
209
+ },
210
+ },
211
+ ],
212
+ data: {
213
+ count: 0,
214
+ },
215
+ navigation: {
216
+ BEGIN: "FLOW_1",
217
+ FLOW_1: {
218
+ startState: "VIEW_1",
219
+ VIEW_1: {
220
+ state_type: "VIEW",
221
+ ref: "action",
222
+ transitions: {
223
+ "*": "END_Done",
224
+ },
225
+ },
226
+ END_Done: {
227
+ state_type: "END",
228
+ outcome: "done",
229
+ },
230
+ },
231
+ },
232
+ } as any);
233
+
234
+ const started = player.getState() as InProgressState;
235
+
236
+ expect(started.controllers.view.currentView?.lastUpdate).toStrictEqual({
237
+ id: "action",
238
+ type: "action",
239
+ exp: "{{count}} = {{count}} + 1",
240
+ label: {
241
+ asset: {
242
+ id: "action-label",
243
+ type: "text",
244
+ value: "Clicked 0 times",
245
+ },
246
+ },
247
+ });
248
+
249
+ started.controllers.expression.evaluate(
250
+ started.controllers.view.currentView?.lastUpdate?.exp,
251
+ );
252
+
253
+ await vitest.waitFor(() =>
254
+ expect(started.controllers.view.currentView?.lastUpdate).toStrictEqual({
255
+ id: "action",
256
+ type: "action",
257
+ exp: "{{count}} = {{count}} + 1",
258
+ label: {
259
+ asset: {
260
+ id: "action-label",
261
+ type: "text",
262
+ value: "Clicked 1 times",
263
+ },
264
+ },
265
+ }),
266
+ );
267
+ });
268
+
269
+ test("handles non-present data", async () => {
270
+ const simpleFlow = makeFlow({
271
+ id: "text",
272
+ type: "text",
273
+ value: "{{some.data}}",
274
+ });
275
+
276
+ const player = new Player();
277
+ player.start(simpleFlow);
278
+
279
+ expect(
280
+ (player.getState() as InProgressState).controllers.view.currentView
281
+ ?.lastUpdate,
282
+ ).toStrictEqual({
283
+ id: "text",
284
+ type: "text",
285
+ value: undefined,
286
+ });
287
+
288
+ (player.getState() as InProgressState).controllers.data.set([
289
+ ["some.other.binding", "Updated"],
290
+ ]);
291
+
292
+ expect(
293
+ (player.getState() as InProgressState).controllers.view.currentView
294
+ ?.lastUpdate,
295
+ ).toStrictEqual({
296
+ id: "text",
297
+ type: "text",
298
+ value: undefined,
299
+ });
300
+
301
+ (player.getState() as InProgressState).controllers.data.set([
302
+ ["some.data", "Updated!"],
303
+ ]);
304
+
305
+ await vitest.waitFor(() =>
306
+ expect(
307
+ (player.getState() as InProgressState).controllers.view.currentView
308
+ ?.lastUpdate,
309
+ ).toStrictEqual({
310
+ id: "text",
311
+ type: "text",
312
+ value: "Updated!",
313
+ }),
314
+ );
315
+ });
316
+
317
+ test("allows for content mutation before processing", () => {
318
+ const player = new Player();
319
+
320
+ player.hooks.resolveFlowContent.tap("test", () => {
321
+ return makeFlow({ id: "new-flow", type: "new" });
322
+ });
323
+
324
+ player.start(makeFlow({ id: "old-flow", type: "old" }));
325
+
326
+ const state = player.getState() as InProgressState;
327
+
328
+ expect(state.controllers.view.currentView?.lastUpdate).toStrictEqual({
329
+ id: "new-flow",
330
+ type: "new",
331
+ });
332
+ });
333
+
334
+ describe("expressions", () => {
335
+ it("works outside", async () => {
336
+ const simpleFlow = makeFlow({
337
+ id: "text",
338
+ type: "text",
339
+ value: "{{data.count1}} - {{data.count2}}",
340
+ });
341
+
342
+ simpleFlow.data = {
343
+ data: {
344
+ count1: 0,
345
+ count2: 1,
346
+ },
347
+ };
348
+
349
+ const player = new Player();
350
+ player.start(simpleFlow);
351
+ const state = player.getState() as InProgressState;
352
+ expect(state.controllers.view.currentView?.lastUpdate?.value).toBe("0 - 1");
353
+
354
+ state.controllers.expression.evaluate([
355
+ "{{data.count1}} = 5",
356
+ "{{data.count2}} = 10",
357
+ ]);
358
+ await vitest.waitFor(() =>
359
+ expect(state.controllers.view.currentView?.lastUpdate?.value).toBe(
360
+ "5 - 10",
361
+ ),
362
+ );
363
+ });
364
+
365
+ it("works with local expressions", async () => {
366
+ const simpleFlow = makeFlow({
367
+ id: "text",
368
+ type: "text",
369
+ value: "{{label1}} @[ label2 ]@",
370
+ });
371
+
372
+ const player = new Player();
373
+ player.start(simpleFlow);
374
+ const state = player.getState() as InProgressState;
375
+
376
+ state.controllers.expression.evaluate(["label2 = 5", "{{label1}} = 10"]);
377
+ await vitest.waitFor(() =>
378
+ expect(state.controllers.view.currentView?.lastUpdate?.value).toBe(
379
+ "10 5",
380
+ ),
381
+ );
382
+
383
+ state.controllers.expression.evaluate(["{{label1}} = 20"]);
384
+ await vitest.waitFor(() =>
385
+ expect(state.controllers.view.currentView?.lastUpdate?.value).toBe(
386
+ "20 5",
387
+ ),
388
+ );
389
+ });
390
+
391
+ it("works inside transform", async () => {
392
+ const simpleFlow = makeFlow({
393
+ id: "action",
394
+ type: "action",
395
+ exp: ["{{data.count1}} = 5", "{{data.count2}} = 10"],
396
+ value: "{{data.count1}} - {{data.count2}}",
397
+ });
398
+
399
+ simpleFlow.data = {
400
+ data: {
401
+ count1: 0,
402
+ count2: 1,
403
+ },
404
+ };
405
+
406
+ const player = new Player({ plugins: [new ActionExpPlugin()] });
407
+
408
+ player.hooks.view.tap("test", (view) => {
409
+ view.hooks.resolver.tap("test", (resolver) => {
410
+ resolver.hooks.afterResolve.tap("test", (val, node, options) => {
411
+ if (node.type === NodeType.View && val.type === "action") {
412
+ return {
413
+ ...val,
414
+ run: () => options.evaluate(val.exp),
415
+ };
416
+ }
417
+
418
+ return val;
419
+ });
420
+ });
421
+ });
422
+
423
+ player.start(simpleFlow);
424
+ const state = player.getState() as InProgressState;
425
+ expect(state.controllers.view.currentView?.lastUpdate?.value).toBe("0 - 1");
426
+ expect(state.controllers.view.currentView?.lastUpdate?.exp).toStrictEqual([
427
+ "{{data.count1}} = 5",
428
+ "{{data.count2}} = 10",
429
+ ]);
430
+ state.controllers.view.currentView?.lastUpdate?.run();
431
+
432
+ await vitest.waitFor(() =>
433
+ expect(state.controllers.view.currentView?.lastUpdate?.value).toBe(
434
+ "5 - 10",
435
+ ),
436
+ );
437
+ });
438
+
439
+ it("recognizes inline format expressions", () => {
440
+ const simpleFlow = makeFlow({
441
+ id: "inline-format-text",
442
+ type: "text",
443
+ value: "this should be upper case: @[ format('aaaa', 'CAPS') ]@",
444
+ });
445
+
446
+ const player = new Player();
447
+
448
+ player.hooks.schema.tap("test", (schema) => {
449
+ schema.addFormatters([
450
+ {
451
+ name: "CAPS",
452
+ format: (value: string) => value.toUpperCase(),
453
+ },
454
+ ]);
455
+ });
456
+
457
+ player.start(simpleFlow);
458
+ const state = player.getState() as InProgressState;
459
+ const lastUpdate = state.controllers.view.currentView?.lastUpdate as any;
460
+ expect(lastUpdate.value).toBe("this should be upper case: AAAA");
461
+ });
462
+ });
463
+
464
+ describe("formatting", () => {
465
+ it("formats by reference", () => {
466
+ const player = new Player();
467
+ player.hooks.schema.tap("test", (schema) => {
468
+ schema.addFormatters([
469
+ {
470
+ name: "CAPS",
471
+ format: (value: string) => value.toUpperCase(),
472
+ },
473
+ ]);
474
+ });
475
+
476
+ player.hooks.viewController.tap("test", (vc) => {
477
+ vc.hooks.view.tap("test", (v) => {
478
+ v.hooks.resolver.tap("test", (resolver) => {
479
+ resolver.hooks.resolve.tap("test", (value, node, options) => {
480
+ if (value && value.type === "text") {
481
+ return {
482
+ ...value,
483
+ value: options.data.formatValue({ type: "CAPS" }, value.value),
484
+ };
485
+ }
486
+
487
+ return value;
488
+ });
489
+ });
490
+ });
491
+ });
492
+
493
+ player.start(minimal as any);
494
+
495
+ const state = player.getState() as InProgressState;
496
+
497
+ expect(
498
+ state.controllers.view.currentView?.lastUpdate?.title.asset.value,
499
+ ).toBe("MINIMAL JSON EXAMPLE");
500
+ });
501
+ });
502
+
503
+ describe("failure cases", () => {
504
+ it("handles non-existant views", async () => {
505
+ const player = new Player();
506
+
507
+ const flow = makeFlow({ id: "view-1", type: "text", value: "Title" });
508
+ flow.views![0].id = "not-view-1";
509
+
510
+ await expect(player.start(flow)).rejects.toThrowError(
511
+ "No view with id view-1",
512
+ );
513
+ });
514
+
515
+ it("handles plugins throwing errors", async () => {
516
+ const player = new Player({
517
+ plugins: [
518
+ {
519
+ name: "test",
520
+ apply(p) {
521
+ p.hooks.bindingParser.tap("error", (b) => {
522
+ (b as any).notThere();
523
+ });
524
+ },
525
+ },
526
+ ],
527
+ });
528
+
529
+ const flow = makeFlow({ id: "view-1", type: "text", value: "Title" });
530
+
531
+ await expect(player.start(flow)).rejects.toThrowError(
532
+ "b.notThere is not a function",
533
+ );
534
+ });
535
+
536
+ it("handles non-existent view", async () => {
537
+ const player = new Player();
538
+ const flow = makeFlow({ id: "view-1", type: "text", value: "Title" });
539
+ flow.views![0].id = "other-id";
540
+
541
+ await expect(player.start(flow)).rejects.toThrowError(
542
+ `No view with id view-1`,
543
+ );
544
+ });
545
+
546
+ it("fails gracefully when states after an ACTION state have failures", async () => {
547
+ const player = new Player();
548
+
549
+ const payload = {
550
+ id: "test",
551
+ views: [
552
+ {
553
+ id: "view",
554
+ type: "text",
555
+ value: "Some text",
556
+ },
557
+ ],
558
+ data: {},
559
+ navigation: {
560
+ BEGIN: "Flow",
561
+ Flow: {
562
+ startState: "ActionState",
563
+ ActionState: {
564
+ state_type: "ACTION",
565
+ transitions: {
566
+ "*": "ViewState",
567
+ },
568
+ },
569
+ ViewState: {
570
+ state_type: "VIEW",
571
+ ref: "non-existing-view",
572
+ },
573
+ },
574
+ },
575
+ };
576
+
577
+ const response = player.start(makeFlow(payload));
578
+
579
+ await expect(response).rejects.toThrowError(
580
+ "No view with id non-existing-view",
581
+ );
582
+ });
583
+
584
+ it("can be failed from other places", async () => {
585
+ const player = new Player();
586
+
587
+ const response = player.start(
588
+ makeFlow({ type: "text", id: "text", value: "View" }),
589
+ );
590
+
591
+ const state = player.getState() as InProgressState;
592
+
593
+ state.fail(new Error("Custom Error"));
594
+
595
+ await expect(response).rejects.toThrowError("Custom Error");
596
+ });
597
+ });