@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.
- package/dist/Player.native.js +11630 -0
- package/dist/Player.native.js.map +1 -0
- package/dist/cjs/index.cjs +5626 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/{index.esm.js → index.legacy-esm.js} +2044 -1667
- package/dist/{index.cjs.js → index.mjs} +2052 -1761
- package/dist/index.mjs.map +1 -0
- package/package.json +29 -63
- package/src/__tests__/data.test.ts +498 -0
- package/src/__tests__/flow.test.ts +312 -0
- package/src/__tests__/helpers/action-exp.plugin.ts +22 -0
- package/src/__tests__/helpers/actions.flow.ts +67 -0
- package/src/__tests__/helpers/binding.plugin.ts +125 -0
- package/src/__tests__/helpers/expression.plugin.ts +88 -0
- package/src/__tests__/helpers/transform-plugin.ts +19 -0
- package/src/__tests__/helpers/validation.flow.ts +56 -0
- package/src/__tests__/player.test.ts +597 -0
- package/src/__tests__/string-resolver.test.ts +186 -0
- package/src/__tests__/validation.test.ts +3555 -0
- package/src/__tests__/view.test.ts +715 -0
- package/src/binding/__tests__/binding.test.ts +113 -0
- package/src/binding/__tests__/index.test.ts +208 -0
- package/src/binding/__tests__/resolver.test.ts +83 -0
- package/src/binding/binding.ts +6 -6
- package/src/binding/index.ts +34 -34
- package/src/binding/resolver.ts +19 -19
- package/src/binding/utils.ts +7 -7
- package/src/binding-grammar/__tests__/parser.test.ts +64 -0
- package/src/binding-grammar/__tests__/test-utils/ast-cases.ts +198 -0
- package/src/binding-grammar/__tests__/test-utils/perf-test.ts +66 -0
- package/src/binding-grammar/ast.ts +11 -11
- package/src/binding-grammar/custom/index.ts +19 -22
- package/src/binding-grammar/ebnf/index.ts +20 -21
- package/src/binding-grammar/ebnf/types.ts +13 -13
- package/src/binding-grammar/index.ts +4 -4
- package/src/binding-grammar/parsimmon/index.ts +14 -14
- package/src/controllers/constants/__tests__/index.test.ts +106 -0
- package/src/controllers/constants/index.ts +3 -3
- package/src/controllers/constants/utils.ts +4 -4
- package/src/controllers/data/controller.ts +22 -22
- package/src/controllers/data/index.ts +1 -1
- package/src/controllers/data/utils.ts +7 -7
- package/src/controllers/flow/__tests__/controller.test.ts +195 -0
- package/src/controllers/flow/__tests__/flow.test.ts +381 -0
- package/src/controllers/flow/controller.ts +13 -13
- package/src/controllers/flow/flow.ts +23 -23
- package/src/controllers/flow/index.ts +2 -2
- package/src/controllers/index.ts +5 -5
- package/src/controllers/validation/binding-tracker.ts +71 -59
- package/src/controllers/validation/controller.ts +104 -104
- package/src/controllers/validation/index.ts +2 -2
- package/src/controllers/view/asset-transform.ts +20 -20
- package/src/controllers/view/controller.ts +27 -27
- package/src/controllers/view/index.ts +4 -4
- package/src/controllers/view/store.ts +3 -3
- package/src/controllers/view/types.ts +7 -7
- package/src/data/__tests__/__snapshots__/dependency-tracker.test.ts.snap +64 -0
- package/src/data/__tests__/dependency-tracker.test.ts +146 -0
- package/src/data/__tests__/local-model.test.ts +46 -0
- package/src/data/__tests__/model.test.ts +78 -0
- package/src/data/dependency-tracker.ts +16 -16
- package/src/data/index.ts +4 -4
- package/src/data/local-model.ts +6 -6
- package/src/data/model.ts +17 -17
- package/src/data/noop-model.ts +1 -1
- package/src/expressions/__tests__/__snapshots__/parser.test.ts.snap +854 -0
- package/src/expressions/__tests__/evaluator-functions.test.ts +47 -0
- package/src/expressions/__tests__/evaluator.test.ts +410 -0
- package/src/expressions/__tests__/parser.test.ts +115 -0
- package/src/expressions/__tests__/utils.test.ts +44 -0
- package/src/expressions/evaluator-functions.ts +6 -6
- package/src/expressions/evaluator.ts +71 -67
- package/src/expressions/index.ts +4 -4
- package/src/expressions/parser.ts +102 -105
- package/src/expressions/types.ts +29 -21
- package/src/expressions/utils.ts +32 -21
- package/src/index.ts +13 -13
- package/src/logger/__tests__/consoleLogger.test.ts +46 -0
- package/src/logger/__tests__/noopLogger.test.ts +13 -0
- package/src/logger/__tests__/proxyLogger.test.ts +31 -0
- package/src/logger/__tests__/tapableLogger.test.ts +41 -0
- package/src/logger/consoleLogger.ts +9 -9
- package/src/logger/index.ts +5 -5
- package/src/logger/noopLogger.ts +1 -1
- package/src/logger/proxyLogger.ts +6 -6
- package/src/logger/tapableLogger.ts +7 -7
- package/src/logger/types.ts +2 -2
- package/src/player.ts +60 -58
- package/src/plugins/default-exp-plugin.ts +10 -10
- package/src/plugins/default-view-plugin.ts +29 -0
- package/src/plugins/flow-exp-plugin.ts +6 -6
- package/src/schema/__tests__/schema.test.ts +243 -0
- package/src/schema/index.ts +2 -2
- package/src/schema/schema.ts +24 -24
- package/src/schema/types.ts +4 -4
- package/src/string-resolver/__tests__/index.test.ts +361 -0
- package/src/string-resolver/index.ts +17 -17
- package/src/types.ts +17 -17
- package/src/utils/__tests__/replaceParams.test.ts +33 -0
- package/src/utils/index.ts +1 -1
- package/src/utils/replaceParams.ts +1 -1
- package/src/validator/__tests__/binding-map-splice.test.ts +53 -0
- package/src/validator/__tests__/validation-middleware.test.ts +127 -0
- package/src/validator/binding-map-splice.ts +5 -5
- package/src/validator/index.ts +4 -4
- package/src/validator/registry.ts +1 -1
- package/src/validator/types.ts +13 -13
- package/src/validator/validation-middleware.ts +15 -15
- package/src/view/__tests__/view.immutable.test.ts +269 -0
- package/src/view/__tests__/view.test.ts +959 -0
- package/src/view/builder/index.test.ts +69 -0
- package/src/view/builder/index.ts +3 -3
- package/src/view/index.ts +5 -5
- package/src/view/parser/__tests__/__snapshots__/parser.test.ts.snap +394 -0
- package/src/view/parser/__tests__/parser.test.ts +264 -0
- package/src/view/parser/index.ts +43 -33
- package/src/view/parser/types.ts +11 -11
- package/src/view/parser/utils.ts +5 -5
- package/src/view/plugins/__tests__/__snapshots__/template.test.ts.snap +278 -0
- package/src/view/plugins/__tests__/applicability.test.ts +265 -0
- package/src/view/plugins/__tests__/string.test.ts +122 -0
- package/src/view/plugins/__tests__/template.test.ts +724 -0
- package/src/view/plugins/applicability.ts +19 -19
- package/src/view/plugins/index.ts +4 -5
- package/src/view/plugins/options.ts +1 -1
- package/src/view/plugins/string-resolver.ts +22 -22
- package/src/view/plugins/switch.ts +22 -23
- package/src/view/plugins/template-plugin.ts +26 -27
- package/src/view/resolver/__tests__/dependencies.test.ts +321 -0
- package/src/view/resolver/__tests__/edgecases.test.ts +626 -0
- package/src/view/resolver/index.ts +42 -42
- package/src/view/resolver/types.ts +21 -20
- package/src/view/resolver/utils.ts +9 -9
- package/src/view/view.ts +32 -22
- package/types/binding/binding.d.ts +50 -0
- package/types/binding/index.d.ts +29 -0
- package/types/binding/resolver.d.ts +26 -0
- package/types/binding/utils.d.ts +12 -0
- package/types/binding-grammar/ast.d.ts +67 -0
- package/types/binding-grammar/custom/index.d.ts +4 -0
- package/types/binding-grammar/ebnf/index.d.ts +4 -0
- package/types/binding-grammar/ebnf/types.d.ts +75 -0
- package/types/binding-grammar/index.d.ts +5 -0
- package/types/binding-grammar/parsimmon/index.d.ts +4 -0
- package/types/controllers/constants/index.d.ts +45 -0
- package/types/controllers/constants/utils.d.ts +6 -0
- package/types/controllers/data/controller.d.ts +45 -0
- package/types/controllers/data/index.d.ts +2 -0
- package/types/controllers/data/utils.d.ts +14 -0
- package/types/controllers/flow/controller.d.ts +25 -0
- package/types/controllers/flow/flow.d.ts +50 -0
- package/types/controllers/flow/index.d.ts +3 -0
- package/types/controllers/index.d.ts +6 -0
- package/types/controllers/validation/binding-tracker.d.ts +32 -0
- package/types/controllers/validation/controller.d.ts +151 -0
- package/types/controllers/validation/index.d.ts +3 -0
- package/types/controllers/view/asset-transform.d.ts +19 -0
- package/types/controllers/view/controller.d.ts +37 -0
- package/types/controllers/view/index.d.ts +5 -0
- package/types/controllers/view/store.d.ts +20 -0
- package/types/controllers/view/types.d.ts +16 -0
- package/types/data/dependency-tracker.d.ts +49 -0
- package/types/data/index.d.ts +5 -0
- package/types/data/local-model.d.ts +16 -0
- package/types/data/model.d.ts +86 -0
- package/types/data/noop-model.d.ts +13 -0
- package/types/expressions/evaluator-functions.d.ts +15 -0
- package/types/expressions/evaluator.d.ts +52 -0
- package/types/expressions/index.d.ts +5 -0
- package/types/expressions/parser.d.ts +10 -0
- package/types/expressions/types.d.ts +144 -0
- package/types/expressions/utils.d.ts +12 -0
- package/types/index.d.ts +14 -0
- package/types/logger/consoleLogger.d.ts +17 -0
- package/types/logger/index.d.ts +6 -0
- package/types/logger/noopLogger.d.ts +10 -0
- package/types/logger/proxyLogger.d.ts +15 -0
- package/types/logger/tapableLogger.d.ts +23 -0
- package/types/logger/types.d.ts +6 -0
- package/types/player.d.ts +101 -0
- package/types/plugins/default-exp-plugin.d.ts +9 -0
- package/types/plugins/default-view-plugin.d.ts +9 -0
- package/types/plugins/flow-exp-plugin.d.ts +11 -0
- package/types/schema/index.d.ts +3 -0
- package/types/schema/schema.d.ts +36 -0
- package/types/schema/types.d.ts +38 -0
- package/types/string-resolver/index.d.ts +30 -0
- package/types/types.d.ts +73 -0
- package/types/utils/index.d.ts +2 -0
- package/types/utils/replaceParams.d.ts +9 -0
- package/types/validator/binding-map-splice.d.ts +10 -0
- package/types/validator/index.d.ts +5 -0
- package/types/validator/registry.d.ts +11 -0
- package/types/validator/types.d.ts +53 -0
- package/types/validator/validation-middleware.d.ts +36 -0
- package/types/view/builder/index.d.ts +35 -0
- package/types/view/index.d.ts +6 -0
- package/types/view/parser/index.d.ts +52 -0
- package/types/view/parser/types.d.ts +109 -0
- package/types/view/parser/utils.d.ts +6 -0
- package/types/view/plugins/applicability.d.ts +10 -0
- package/types/view/plugins/index.d.ts +5 -0
- package/types/view/plugins/options.d.ts +4 -0
- package/types/view/plugins/string-resolver.d.ts +13 -0
- package/types/view/plugins/switch.d.ts +14 -0
- package/types/view/plugins/template-plugin.d.ts +33 -0
- package/types/view/resolver/index.d.ts +73 -0
- package/types/view/resolver/types.d.ts +129 -0
- package/types/view/resolver/utils.d.ts +11 -0
- package/types/view/view.d.ts +37 -0
- package/dist/index.d.ts +0 -1814
- package/dist/player.dev.js +0 -11472
- package/dist/player.prod.js +0 -2
- package/src/view/plugins/plugin.ts +0 -21
|
@@ -0,0 +1,3555 @@
|
|
|
1
|
+
import { test, expect, describe, it, beforeEach } from "vitest";
|
|
2
|
+
import { omit } from "timm";
|
|
3
|
+
import { makeFlow } from "@player-ui/make-flow";
|
|
4
|
+
import { vitest } from "vitest";
|
|
5
|
+
import type { Flow } from "@player-ui/types";
|
|
6
|
+
import type { SchemaController } from "../schema";
|
|
7
|
+
import type { BindingParser } from "../binding";
|
|
8
|
+
import TrackBindingPlugin, { addValidator } from "./helpers/binding.plugin";
|
|
9
|
+
import { Player } from "..";
|
|
10
|
+
import { VALIDATION_PROVIDER_NAME_SYMBOL } from "../controllers/validation";
|
|
11
|
+
import type { ValidationController } from "../controllers/validation";
|
|
12
|
+
import type { InProgressState } from "../types";
|
|
13
|
+
import TestExpressionPlugin, {
|
|
14
|
+
RequiredIfValidationProviderPlugin,
|
|
15
|
+
} from "./helpers/expression.plugin";
|
|
16
|
+
|
|
17
|
+
const simpleFlow: Flow = {
|
|
18
|
+
id: "test-flow",
|
|
19
|
+
views: [
|
|
20
|
+
{
|
|
21
|
+
id: "view-1",
|
|
22
|
+
type: "view",
|
|
23
|
+
thing1: {
|
|
24
|
+
asset: {
|
|
25
|
+
type: "whatevs",
|
|
26
|
+
id: "thing1",
|
|
27
|
+
binding: "data.thing1",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
thing2: {
|
|
31
|
+
asset: {
|
|
32
|
+
type: "whatevs",
|
|
33
|
+
id: "thing2",
|
|
34
|
+
binding: "data.thing2",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
data: {},
|
|
40
|
+
schema: {
|
|
41
|
+
ROOT: {
|
|
42
|
+
data: {
|
|
43
|
+
type: "DataType",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
DataType: {
|
|
47
|
+
thing1: {
|
|
48
|
+
type: "CatType",
|
|
49
|
+
validation: [
|
|
50
|
+
{
|
|
51
|
+
type: "names",
|
|
52
|
+
names: ["frodo", "sam"],
|
|
53
|
+
trigger: "navigation",
|
|
54
|
+
severity: "warning",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
thing2: {
|
|
59
|
+
type: "CatType",
|
|
60
|
+
validation: [
|
|
61
|
+
{
|
|
62
|
+
type: "names",
|
|
63
|
+
trigger: "navigation",
|
|
64
|
+
names: ["frodo", "sam"],
|
|
65
|
+
severity: "warning",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
navigation: {
|
|
72
|
+
BEGIN: "FLOW_1",
|
|
73
|
+
FLOW_1: {
|
|
74
|
+
startState: "VIEW_1",
|
|
75
|
+
VIEW_1: {
|
|
76
|
+
state_type: "VIEW",
|
|
77
|
+
ref: "view-1",
|
|
78
|
+
transitions: {
|
|
79
|
+
"*": "END_1",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
END_1: {
|
|
83
|
+
state_type: "END",
|
|
84
|
+
outcome: "test",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const simpleExpressionFlow: Flow = {
|
|
91
|
+
id: "test-flow",
|
|
92
|
+
views: [
|
|
93
|
+
{
|
|
94
|
+
id: "view-1",
|
|
95
|
+
type: "view",
|
|
96
|
+
foo: {
|
|
97
|
+
asset: {
|
|
98
|
+
type: "whatevs",
|
|
99
|
+
id: "foo",
|
|
100
|
+
binding: "data.foo",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
foo2: {
|
|
104
|
+
asset: {
|
|
105
|
+
type: "whatevs",
|
|
106
|
+
id: "foo2",
|
|
107
|
+
binding: "data.foo2",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
bar: {
|
|
111
|
+
asset: {
|
|
112
|
+
type: "whatevs",
|
|
113
|
+
id: "bar",
|
|
114
|
+
binding: "data.bar",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
bar2: {
|
|
118
|
+
asset: {
|
|
119
|
+
type: "whatevs",
|
|
120
|
+
id: "bar2",
|
|
121
|
+
binding: "data.bar2",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
data: {},
|
|
127
|
+
schema: {
|
|
128
|
+
ROOT: {
|
|
129
|
+
data: {
|
|
130
|
+
type: "DataType",
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
DataType: {
|
|
134
|
+
foo: {
|
|
135
|
+
type: "CatType",
|
|
136
|
+
validation: [
|
|
137
|
+
{
|
|
138
|
+
type: "expression",
|
|
139
|
+
exp: "!(isEmpty({{data.foo}}) && !isEmpty({{data.foo2}}))",
|
|
140
|
+
severity: "warning",
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
bar: {
|
|
145
|
+
type: "CatType",
|
|
146
|
+
validation: [
|
|
147
|
+
{
|
|
148
|
+
type: "expression",
|
|
149
|
+
exp: "!(isEmpty({{data.bar}}) && !isEmpty({{data.bar2}}))",
|
|
150
|
+
severity: "warning",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
navigation: {
|
|
157
|
+
BEGIN: "FLOW_1",
|
|
158
|
+
FLOW_1: {
|
|
159
|
+
startState: "VIEW_1",
|
|
160
|
+
VIEW_1: {
|
|
161
|
+
state_type: "VIEW",
|
|
162
|
+
ref: "view-1",
|
|
163
|
+
transitions: {
|
|
164
|
+
"*": "END_1",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
END_1: {
|
|
168
|
+
state_type: "END",
|
|
169
|
+
outcome: "test",
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const flowWithMultiNode: Flow = {
|
|
176
|
+
id: "test-flow",
|
|
177
|
+
views: [
|
|
178
|
+
{
|
|
179
|
+
id: "view-1",
|
|
180
|
+
type: "view",
|
|
181
|
+
multiNode: [
|
|
182
|
+
{
|
|
183
|
+
nestedMultiNode: [
|
|
184
|
+
{
|
|
185
|
+
asset: {
|
|
186
|
+
type: "asset-type",
|
|
187
|
+
id: "nested-asset",
|
|
188
|
+
binding: "data.foo",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
data: {},
|
|
197
|
+
schema: {
|
|
198
|
+
ROOT: {
|
|
199
|
+
data: {
|
|
200
|
+
type: "DataType",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
DataType: {
|
|
204
|
+
foo: {
|
|
205
|
+
type: "CatType",
|
|
206
|
+
validation: [
|
|
207
|
+
{
|
|
208
|
+
type: "names",
|
|
209
|
+
names: ["frodo", "sam"],
|
|
210
|
+
trigger: "navigation",
|
|
211
|
+
severity: "warning",
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
navigation: {
|
|
218
|
+
BEGIN: "FLOW_1",
|
|
219
|
+
FLOW_1: {
|
|
220
|
+
startState: "VIEW_1",
|
|
221
|
+
VIEW_1: {
|
|
222
|
+
state_type: "VIEW",
|
|
223
|
+
ref: "view-1",
|
|
224
|
+
transitions: {
|
|
225
|
+
"*": "END_1",
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
END_1: {
|
|
229
|
+
state_type: "END",
|
|
230
|
+
outcome: "test",
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const flowWithThings: Flow = {
|
|
237
|
+
id: "test-flow",
|
|
238
|
+
views: [
|
|
239
|
+
{
|
|
240
|
+
id: "view-1",
|
|
241
|
+
type: "view",
|
|
242
|
+
thing1: {
|
|
243
|
+
asset: {
|
|
244
|
+
type: "whatevs",
|
|
245
|
+
id: "thing1",
|
|
246
|
+
binding: "data.thing1",
|
|
247
|
+
applicability: "{{applicability.thing1}}",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
thing2: {
|
|
251
|
+
asset: {
|
|
252
|
+
type: "whatevs",
|
|
253
|
+
id: "thing2",
|
|
254
|
+
binding: "data.thing2",
|
|
255
|
+
applicability: "{{applicability.thing2}}",
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
thing3: {
|
|
259
|
+
asset: {
|
|
260
|
+
type: "whatevs",
|
|
261
|
+
id: "thing3",
|
|
262
|
+
applicability: "{{applicability.thing3}}",
|
|
263
|
+
binding: "data.thing3",
|
|
264
|
+
other: {
|
|
265
|
+
asset: {
|
|
266
|
+
type: "whatevs",
|
|
267
|
+
id: "thing3a",
|
|
268
|
+
binding: "data.thing3a",
|
|
269
|
+
applicability: "{{applicability.thing3a}}",
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
thing5: {
|
|
275
|
+
asset: {
|
|
276
|
+
type: "section",
|
|
277
|
+
id: "thing5",
|
|
278
|
+
binding: "data.thing5",
|
|
279
|
+
applicability: "{{applicability.thing5}}",
|
|
280
|
+
thing6: {
|
|
281
|
+
asset: {
|
|
282
|
+
type: "section",
|
|
283
|
+
id: "thing6",
|
|
284
|
+
binding: "data.thing6",
|
|
285
|
+
applicability: "{{applicability.thing6}}",
|
|
286
|
+
thing7: {
|
|
287
|
+
asset: {
|
|
288
|
+
type: "whatevs",
|
|
289
|
+
id: "thing7",
|
|
290
|
+
binding: "data.thing7",
|
|
291
|
+
applicability: "{{applicability.thing7}}",
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
alreadyInvalidData: {
|
|
299
|
+
asset: {
|
|
300
|
+
type: "invalid",
|
|
301
|
+
id: "thing4",
|
|
302
|
+
binding: "data.thing4",
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
data: {
|
|
308
|
+
applicability: {
|
|
309
|
+
thing1: true,
|
|
310
|
+
thing2: true,
|
|
311
|
+
thing3: true,
|
|
312
|
+
thing3a: true,
|
|
313
|
+
thing5: true,
|
|
314
|
+
thing6: true,
|
|
315
|
+
thing7: true,
|
|
316
|
+
},
|
|
317
|
+
data: {
|
|
318
|
+
thing2: "frodo",
|
|
319
|
+
thing4: "frodo",
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
schema: {
|
|
323
|
+
ROOT: {
|
|
324
|
+
data: {
|
|
325
|
+
type: "DataType",
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
DataType: {
|
|
329
|
+
thing2: {
|
|
330
|
+
type: "CatType",
|
|
331
|
+
validation: [
|
|
332
|
+
{
|
|
333
|
+
type: "names",
|
|
334
|
+
names: ["frodo", "sam"],
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
},
|
|
338
|
+
thing4: {
|
|
339
|
+
type: "CatType",
|
|
340
|
+
validation: [
|
|
341
|
+
{
|
|
342
|
+
type: "names",
|
|
343
|
+
names: ["sam"],
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
},
|
|
347
|
+
thing5: {
|
|
348
|
+
type: "CatType",
|
|
349
|
+
validation: [
|
|
350
|
+
{
|
|
351
|
+
type: "names",
|
|
352
|
+
names: ["frodo"],
|
|
353
|
+
displayTarget: "page",
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
thing6: {
|
|
358
|
+
type: "CatType",
|
|
359
|
+
validation: [
|
|
360
|
+
{
|
|
361
|
+
type: "names",
|
|
362
|
+
names: ["sam"],
|
|
363
|
+
displayTarget: "section",
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
thing7: {
|
|
368
|
+
type: "CatType",
|
|
369
|
+
validation: [
|
|
370
|
+
{
|
|
371
|
+
type: "names",
|
|
372
|
+
names: ["bilbo"],
|
|
373
|
+
displayTarget: "section",
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
navigation: {
|
|
380
|
+
BEGIN: "FLOW_1",
|
|
381
|
+
FLOW_1: {
|
|
382
|
+
startState: "VIEW_1",
|
|
383
|
+
VIEW_1: {
|
|
384
|
+
state_type: "VIEW",
|
|
385
|
+
ref: "view-1",
|
|
386
|
+
transitions: {
|
|
387
|
+
"*": "END_1",
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
END_1: {
|
|
391
|
+
state_type: "END",
|
|
392
|
+
outcome: "test",
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const flowWithApplicability: Flow = {
|
|
399
|
+
id: "test-flow",
|
|
400
|
+
views: [
|
|
401
|
+
{
|
|
402
|
+
id: "view-1",
|
|
403
|
+
type: "view",
|
|
404
|
+
thing1: {
|
|
405
|
+
asset: {
|
|
406
|
+
type: "whatevs",
|
|
407
|
+
id: "thing1",
|
|
408
|
+
binding: "dependentBinding",
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
thing2: {
|
|
412
|
+
asset: {
|
|
413
|
+
type: "whatevs",
|
|
414
|
+
id: "thing2",
|
|
415
|
+
binding: "independentBinding",
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
thing3: {
|
|
419
|
+
asset: {
|
|
420
|
+
type: "whatevs",
|
|
421
|
+
id: "thing3",
|
|
422
|
+
applicability: "{{independentBinding}} == true",
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
validation: [
|
|
426
|
+
{
|
|
427
|
+
type: "requiredIf",
|
|
428
|
+
ref: "dependentBinding",
|
|
429
|
+
trigger: "load",
|
|
430
|
+
param: "{{independentBinding}}",
|
|
431
|
+
message: "required based on independent value",
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
data: {},
|
|
437
|
+
navigation: {
|
|
438
|
+
BEGIN: "FLOW_1",
|
|
439
|
+
FLOW_1: {
|
|
440
|
+
startState: "VIEW_1",
|
|
441
|
+
VIEW_1: {
|
|
442
|
+
state_type: "VIEW",
|
|
443
|
+
ref: "view-1",
|
|
444
|
+
transitions: {
|
|
445
|
+
"*": "END_1",
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
END_1: {
|
|
449
|
+
state_type: "END",
|
|
450
|
+
outcome: "test",
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const flowWithItemsInArray: Flow = {
|
|
457
|
+
id: "test-flow",
|
|
458
|
+
views: [
|
|
459
|
+
{
|
|
460
|
+
id: "view-1",
|
|
461
|
+
type: "view",
|
|
462
|
+
pets: [
|
|
463
|
+
{
|
|
464
|
+
asset: {
|
|
465
|
+
type: "whatevs",
|
|
466
|
+
id: "thing1",
|
|
467
|
+
binding: "pets.0.name",
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
asset: {
|
|
472
|
+
type: "whatevs",
|
|
473
|
+
id: "thing2",
|
|
474
|
+
binding: "pets.1.name",
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
asset: {
|
|
479
|
+
type: "whatevs",
|
|
480
|
+
id: "thing2",
|
|
481
|
+
binding: "pets.2.name",
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
data: {
|
|
488
|
+
pets: [],
|
|
489
|
+
},
|
|
490
|
+
schema: {
|
|
491
|
+
ROOT: {
|
|
492
|
+
pets: {
|
|
493
|
+
type: "PetType",
|
|
494
|
+
isArray: true,
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
PetType: {
|
|
498
|
+
name: {
|
|
499
|
+
type: "string",
|
|
500
|
+
validation: [
|
|
501
|
+
{
|
|
502
|
+
type: "required",
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
navigation: {
|
|
509
|
+
BEGIN: "FLOW_1",
|
|
510
|
+
FLOW_1: {
|
|
511
|
+
startState: "VIEW_1",
|
|
512
|
+
VIEW_1: {
|
|
513
|
+
state_type: "VIEW",
|
|
514
|
+
ref: "view-1",
|
|
515
|
+
transitions: {
|
|
516
|
+
"*": "END_1",
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
END_1: {
|
|
520
|
+
state_type: "END",
|
|
521
|
+
outcome: "test",
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const multipleWarningsFlow: Flow = {
|
|
528
|
+
id: "input-validation-flow",
|
|
529
|
+
views: [
|
|
530
|
+
{
|
|
531
|
+
type: "view",
|
|
532
|
+
id: "view",
|
|
533
|
+
loadWarning: {
|
|
534
|
+
asset: {
|
|
535
|
+
id: "load-warning",
|
|
536
|
+
type: "warning-asset",
|
|
537
|
+
binding: "foo.load",
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
navigationWarning: {
|
|
541
|
+
asset: {
|
|
542
|
+
id: "required-warning",
|
|
543
|
+
type: "warning-asset",
|
|
544
|
+
binding: "foo.navigation",
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
schema: {
|
|
550
|
+
ROOT: {
|
|
551
|
+
foo: {
|
|
552
|
+
type: "FooType",
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
FooType: {
|
|
556
|
+
navigation: {
|
|
557
|
+
type: "String",
|
|
558
|
+
validation: [
|
|
559
|
+
{
|
|
560
|
+
type: "required",
|
|
561
|
+
severity: "warning",
|
|
562
|
+
blocking: "once",
|
|
563
|
+
trigger: "navigation",
|
|
564
|
+
},
|
|
565
|
+
],
|
|
566
|
+
},
|
|
567
|
+
load: {
|
|
568
|
+
type: "String",
|
|
569
|
+
validation: [
|
|
570
|
+
{
|
|
571
|
+
type: "required",
|
|
572
|
+
severity: "warning",
|
|
573
|
+
blocking: "once",
|
|
574
|
+
trigger: "load",
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
data: {},
|
|
581
|
+
navigation: {
|
|
582
|
+
BEGIN: "FLOW_1",
|
|
583
|
+
FLOW_1: {
|
|
584
|
+
startState: "VIEW_1",
|
|
585
|
+
VIEW_1: {
|
|
586
|
+
state_type: "VIEW",
|
|
587
|
+
ref: "view",
|
|
588
|
+
transitions: {
|
|
589
|
+
"*": "END_Done",
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
END_Done: {
|
|
593
|
+
state_type: "END",
|
|
594
|
+
outcome: "done",
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const simpleFlowWithViewValidation: Flow = {
|
|
601
|
+
id: "test-flow",
|
|
602
|
+
views: [
|
|
603
|
+
{
|
|
604
|
+
id: "view-1",
|
|
605
|
+
type: "view",
|
|
606
|
+
thing1: {
|
|
607
|
+
asset: {
|
|
608
|
+
type: "whatevs",
|
|
609
|
+
id: "thing1",
|
|
610
|
+
binding: "data.thing1",
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
validation: [
|
|
614
|
+
{
|
|
615
|
+
ref: "data.thing1",
|
|
616
|
+
type: "expression",
|
|
617
|
+
exp: "{{data.thing1}} > 50",
|
|
618
|
+
trigger: "navigation",
|
|
619
|
+
message: "Must be greater than 50",
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
},
|
|
623
|
+
],
|
|
624
|
+
data: {},
|
|
625
|
+
schema: {
|
|
626
|
+
ROOT: {
|
|
627
|
+
data: {
|
|
628
|
+
type: "DataType",
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
DataType: {
|
|
632
|
+
thing1: {
|
|
633
|
+
type: "IntegerType",
|
|
634
|
+
validation: [
|
|
635
|
+
{
|
|
636
|
+
type: "required",
|
|
637
|
+
},
|
|
638
|
+
],
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
navigation: {
|
|
643
|
+
BEGIN: "FLOW_1",
|
|
644
|
+
FLOW_1: {
|
|
645
|
+
startState: "VIEW_1",
|
|
646
|
+
VIEW_1: {
|
|
647
|
+
state_type: "VIEW",
|
|
648
|
+
ref: "view-1",
|
|
649
|
+
transitions: {
|
|
650
|
+
"*": "END_1",
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
END_1: {
|
|
654
|
+
state_type: "END",
|
|
655
|
+
outcome: "test",
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
test("alt APIs", async () => {
|
|
662
|
+
const player = new Player();
|
|
663
|
+
|
|
664
|
+
player.hooks.validationController.tap("test", (validationProvider) => {
|
|
665
|
+
addValidator(validationProvider);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
player.hooks.viewController.tap("test", (vc) => {
|
|
669
|
+
vc.hooks.view.tap("test", (view) => {
|
|
670
|
+
view.hooks.resolver.tap("test", (resolver) => {
|
|
671
|
+
resolver.hooks.resolve.tap("test", (val, node, options) => {
|
|
672
|
+
if (val.type === "section") {
|
|
673
|
+
options.validation?.register({ type: "section" });
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (val?.binding) {
|
|
677
|
+
return {
|
|
678
|
+
...val,
|
|
679
|
+
validation: options.validation?.get(val.binding, { track: true }),
|
|
680
|
+
childValidations: options.validation?.getChildren,
|
|
681
|
+
sectionValidations: options.validation?.getValidationsForSection,
|
|
682
|
+
allValidations: options.validation?.getAll(),
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return {
|
|
687
|
+
...val,
|
|
688
|
+
childValidations: options.validation?.getChildren,
|
|
689
|
+
groupValidations: options.validation?.getValidationsForSection,
|
|
690
|
+
allValidations: options.validation?.getAll(),
|
|
691
|
+
};
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
player.start(flowWithThings);
|
|
697
|
+
|
|
698
|
+
const state = player.getState() as InProgressState;
|
|
699
|
+
|
|
700
|
+
// Starts out with nothing
|
|
701
|
+
expect(
|
|
702
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation,
|
|
703
|
+
).toBe(undefined);
|
|
704
|
+
|
|
705
|
+
// Updates when data is updated to throw an error
|
|
706
|
+
state.controllers.data.set([["data.thing2", "ginger"]]);
|
|
707
|
+
await vitest.waitFor(() =>
|
|
708
|
+
expect(
|
|
709
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation,
|
|
710
|
+
).toMatchObject({
|
|
711
|
+
severity: "error",
|
|
712
|
+
message: `Names just be in: frodo,sam`,
|
|
713
|
+
displayTarget: "field",
|
|
714
|
+
}),
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
expect(
|
|
718
|
+
Array.from(
|
|
719
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset.allValidations.values(),
|
|
720
|
+
),
|
|
721
|
+
).toMatchObject([
|
|
722
|
+
{
|
|
723
|
+
severity: "error",
|
|
724
|
+
message: `Names just be in: frodo,sam`,
|
|
725
|
+
displayTarget: "field",
|
|
726
|
+
},
|
|
727
|
+
]);
|
|
728
|
+
|
|
729
|
+
// check that the childValidations and sectionValidation computation works and
|
|
730
|
+
state.controllers.data.set([["data.thing5", "sam"]]);
|
|
731
|
+
state.controllers.data.set([["data.thing6", "frodo"]]);
|
|
732
|
+
state.controllers.data.set([["data.thing7", "golumn"]]);
|
|
733
|
+
|
|
734
|
+
// Gets all page errors for all children
|
|
735
|
+
await vitest.waitFor(() =>
|
|
736
|
+
expect(
|
|
737
|
+
Array.from(
|
|
738
|
+
state.controllers.view.currentView?.lastUpdate
|
|
739
|
+
?.childValidations("page")
|
|
740
|
+
.values(),
|
|
741
|
+
),
|
|
742
|
+
).toMatchObject([
|
|
743
|
+
{
|
|
744
|
+
severity: "error",
|
|
745
|
+
message: `Names just be in: frodo`,
|
|
746
|
+
displayTarget: "page",
|
|
747
|
+
},
|
|
748
|
+
]),
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
// Gets all section errors for all children
|
|
752
|
+
expect(
|
|
753
|
+
Array.from(
|
|
754
|
+
state.controllers.view.currentView?.lastUpdate
|
|
755
|
+
?.childValidations("section")
|
|
756
|
+
.values(),
|
|
757
|
+
),
|
|
758
|
+
).toMatchObject([
|
|
759
|
+
{
|
|
760
|
+
severity: "error",
|
|
761
|
+
message: `Names just be in: sam`,
|
|
762
|
+
displayTarget: "section",
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
severity: "error",
|
|
766
|
+
message: `Names just be in: bilbo`,
|
|
767
|
+
displayTarget: "section",
|
|
768
|
+
},
|
|
769
|
+
]);
|
|
770
|
+
|
|
771
|
+
// Gets section error for child that is not wrapped in nested section
|
|
772
|
+
expect(
|
|
773
|
+
Array.from(
|
|
774
|
+
state.controllers.view.currentView?.lastUpdate?.thing5.asset
|
|
775
|
+
?.sectionValidations()
|
|
776
|
+
.values(),
|
|
777
|
+
),
|
|
778
|
+
).toMatchObject([
|
|
779
|
+
{
|
|
780
|
+
severity: "error",
|
|
781
|
+
message: `Names just be in: sam`,
|
|
782
|
+
displayTarget: "section",
|
|
783
|
+
},
|
|
784
|
+
]);
|
|
785
|
+
|
|
786
|
+
// Ensure that nested section still produces an error
|
|
787
|
+
expect(
|
|
788
|
+
Array.from(
|
|
789
|
+
state.controllers.view.currentView?.lastUpdate?.thing5.asset.thing6.asset
|
|
790
|
+
?.sectionValidations()
|
|
791
|
+
.values(),
|
|
792
|
+
),
|
|
793
|
+
).toMatchObject([
|
|
794
|
+
{
|
|
795
|
+
severity: "error",
|
|
796
|
+
message: `Names just be in: bilbo`,
|
|
797
|
+
displayTarget: "section",
|
|
798
|
+
},
|
|
799
|
+
]);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
describe("validation", () => {
|
|
803
|
+
let player: Player;
|
|
804
|
+
let validationController: ValidationController;
|
|
805
|
+
let schema: SchemaController;
|
|
806
|
+
let parser: BindingParser;
|
|
807
|
+
|
|
808
|
+
beforeEach(() => {
|
|
809
|
+
player = new Player({
|
|
810
|
+
plugins: [new TrackBindingPlugin()],
|
|
811
|
+
});
|
|
812
|
+
player.hooks.validationController.tap("test", (vc) => {
|
|
813
|
+
validationController = vc;
|
|
814
|
+
});
|
|
815
|
+
player.hooks.schema.tap("test", (s) => {
|
|
816
|
+
schema = s;
|
|
817
|
+
});
|
|
818
|
+
player.hooks.bindingParser.tap("test", (p) => {
|
|
819
|
+
parser = p;
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
player.start(flowWithThings);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
describe("binding tracker", () => {
|
|
826
|
+
it("tracks bindings in the view", () => {
|
|
827
|
+
expect(validationController?.getBindings().size).toStrictEqual(8);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it("preserves tracked bindings for non-updated things", () => {
|
|
831
|
+
expect(validationController?.getBindings().size).toStrictEqual(8);
|
|
832
|
+
|
|
833
|
+
(player.getState() as InProgressState).controllers.data.set([
|
|
834
|
+
["not.there", false],
|
|
835
|
+
]);
|
|
836
|
+
expect(validationController?.getBindings().size).toStrictEqual(8);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it("drops bindings for non-applicable things", async () => {
|
|
840
|
+
expect(validationController?.getBindings().size).toStrictEqual(8);
|
|
841
|
+
|
|
842
|
+
(player.getState() as InProgressState).controllers.data.set([
|
|
843
|
+
["applicability.thing3", false],
|
|
844
|
+
]);
|
|
845
|
+
|
|
846
|
+
await vitest.waitFor(() =>
|
|
847
|
+
expect(validationController?.getBindings().size).toStrictEqual(6),
|
|
848
|
+
);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it("track bindings in nested multi nodes", async () => {
|
|
852
|
+
player.start(flowWithMultiNode);
|
|
853
|
+
|
|
854
|
+
await vitest.waitFor(() =>
|
|
855
|
+
expect(validationController?.getBindings().size).toStrictEqual(1),
|
|
856
|
+
);
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
describe("schema", () => {
|
|
861
|
+
it("tests the types right", () => {
|
|
862
|
+
expect(schema.getType(parser.parse("data.thing2"))?.type).toBe("CatType");
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
describe("data model delete", () => {
|
|
867
|
+
it("deletes the validation when the data is deleted", async () => {
|
|
868
|
+
const state = player.getState() as InProgressState;
|
|
869
|
+
|
|
870
|
+
const { validation, data, binding, view } = state.controllers;
|
|
871
|
+
const thing2Binding = binding.parse("data.thing2");
|
|
872
|
+
|
|
873
|
+
expect(validation.getBindings().has(thing2Binding)).toBe(true);
|
|
874
|
+
|
|
875
|
+
await vitest.waitFor(() => {
|
|
876
|
+
expect(
|
|
877
|
+
view.currentView?.lastUpdate?.thing2.asset.validation,
|
|
878
|
+
).toBeUndefined();
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
data.set([["data.thing2", "gandalf"]]);
|
|
882
|
+
|
|
883
|
+
await vitest.waitFor(() => {
|
|
884
|
+
expect(
|
|
885
|
+
view.currentView?.lastUpdate?.thing2.asset.validation?.message,
|
|
886
|
+
).toBe("Names just be in: frodo,sam");
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
data.delete("data.thing2");
|
|
890
|
+
expect(data.get("data.thing2", { includeInvalid: true })).toBe(undefined);
|
|
891
|
+
|
|
892
|
+
await vitest.waitFor(() => {
|
|
893
|
+
expect(
|
|
894
|
+
view.currentView?.lastUpdate?.thing2.asset.validation,
|
|
895
|
+
).toBeUndefined();
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
data.set([["data.thing2", "gandalf"]]);
|
|
899
|
+
await vitest.waitFor(() => {
|
|
900
|
+
expect(
|
|
901
|
+
view.currentView?.lastUpdate?.thing2.asset.validation?.message,
|
|
902
|
+
).toBe("Names just be in: frodo,sam");
|
|
903
|
+
});
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it("handles arrays", async () => {
|
|
907
|
+
player.start(flowWithItemsInArray);
|
|
908
|
+
const state = player.getState() as InProgressState;
|
|
909
|
+
const { data, binding, view } = state.controllers;
|
|
910
|
+
|
|
911
|
+
await vitest.waitFor(() => {
|
|
912
|
+
expect(
|
|
913
|
+
view.currentView?.lastUpdate?.pets[1].asset.validation,
|
|
914
|
+
).toBeUndefined();
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
// Trigger validation for the second item
|
|
918
|
+
data.set([["pets.1.name", ""]]);
|
|
919
|
+
expect(
|
|
920
|
+
schema.getType(binding.parse("pets.1.name"))?.validation,
|
|
921
|
+
).toHaveLength(1);
|
|
922
|
+
|
|
923
|
+
await vitest.waitFor(() => {
|
|
924
|
+
expect(
|
|
925
|
+
view.currentView?.lastUpdate?.pets[1].asset.validation?.message,
|
|
926
|
+
).toBe("A value is required");
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// Delete the first item, the items should shift up and validation moves to the first item
|
|
930
|
+
data.delete("pets.0");
|
|
931
|
+
|
|
932
|
+
await vitest.waitFor(() => {
|
|
933
|
+
expect(
|
|
934
|
+
view.currentView?.lastUpdate?.pets[1].asset.validation,
|
|
935
|
+
).toBeUndefined();
|
|
936
|
+
expect(
|
|
937
|
+
view.currentView?.lastUpdate?.pets[0].asset.validation?.message,
|
|
938
|
+
).toBe("A value is required");
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
describe("state", () => {
|
|
944
|
+
it("updates when setting data", async () => {
|
|
945
|
+
const state = player.getState() as InProgressState;
|
|
946
|
+
|
|
947
|
+
// Starts out with nothing
|
|
948
|
+
expect(
|
|
949
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation,
|
|
950
|
+
).toBe(undefined);
|
|
951
|
+
|
|
952
|
+
// Updates when data is updated to throw an error
|
|
953
|
+
state.controllers.data.set([["data.thing2", "ginger"]]);
|
|
954
|
+
await vitest.waitFor(() =>
|
|
955
|
+
expect(
|
|
956
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset
|
|
957
|
+
.validation,
|
|
958
|
+
).toMatchObject({
|
|
959
|
+
severity: "error",
|
|
960
|
+
message: `Names just be in: frodo,sam`,
|
|
961
|
+
displayTarget: "field",
|
|
962
|
+
}),
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
// Back to nothing when the error is fixed
|
|
966
|
+
state.controllers.data.set([["data.thing2", "frodo"]]);
|
|
967
|
+
await vitest.waitFor(() =>
|
|
968
|
+
expect(
|
|
969
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset
|
|
970
|
+
.validation,
|
|
971
|
+
).toBe(undefined),
|
|
972
|
+
);
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
describe("validation object", () => {
|
|
977
|
+
it("returns the whole validation object", async () => {
|
|
978
|
+
const state = player.getState() as InProgressState;
|
|
979
|
+
|
|
980
|
+
// Starts out with nothing
|
|
981
|
+
expect(
|
|
982
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation,
|
|
983
|
+
).toBe(undefined);
|
|
984
|
+
|
|
985
|
+
// Updates when data is updated to throw an error
|
|
986
|
+
state.controllers.data.set([["data.thing2", "ginger"]]);
|
|
987
|
+
await vitest.waitFor(() =>
|
|
988
|
+
expect(
|
|
989
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset
|
|
990
|
+
.validation,
|
|
991
|
+
).toStrictEqual({
|
|
992
|
+
severity: "error",
|
|
993
|
+
message: `Names just be in: frodo,sam`,
|
|
994
|
+
names: ["frodo", "sam"],
|
|
995
|
+
displayTarget: "field",
|
|
996
|
+
trigger: "change",
|
|
997
|
+
type: "names",
|
|
998
|
+
blocking: true,
|
|
999
|
+
[VALIDATION_PROVIDER_NAME_SYMBOL]: "schema",
|
|
1000
|
+
}),
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
// Back to nothing when the error is fixed
|
|
1004
|
+
state.controllers.data.set([["data.thing2", "frodo"]]);
|
|
1005
|
+
await vitest.waitFor(() =>
|
|
1006
|
+
expect(
|
|
1007
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset
|
|
1008
|
+
.validation,
|
|
1009
|
+
).toBe(undefined),
|
|
1010
|
+
);
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
describe("navigation", () => {
|
|
1015
|
+
it("prevents navigation for pre-existing invalid data", async () => {
|
|
1016
|
+
const state = player.getState() as InProgressState;
|
|
1017
|
+
const { flowResult } = state;
|
|
1018
|
+
// Starts out with nothing
|
|
1019
|
+
expect(
|
|
1020
|
+
state.controllers.view.currentView?.lastUpdate?.alreadyInvalidData.asset
|
|
1021
|
+
.validation,
|
|
1022
|
+
).toBe(undefined);
|
|
1023
|
+
|
|
1024
|
+
// Try to transition
|
|
1025
|
+
state.controllers.flow.transition("foo");
|
|
1026
|
+
|
|
1027
|
+
// Stays on the same view
|
|
1028
|
+
expect(
|
|
1029
|
+
state.controllers.flow.current?.currentState?.value.state_type,
|
|
1030
|
+
).toBe("VIEW");
|
|
1031
|
+
|
|
1032
|
+
// Fix the error.
|
|
1033
|
+
state.controllers.data.set([["data.thing4", "sam"]]);
|
|
1034
|
+
state.controllers.data.set([["data.thing5", "frodo"]]);
|
|
1035
|
+
state.controllers.data.set([["data.thing6", "sam"]]);
|
|
1036
|
+
state.controllers.data.set([["data.thing7", "bilbo"]]);
|
|
1037
|
+
|
|
1038
|
+
// Try to transition again
|
|
1039
|
+
state.controllers.flow.transition("foo");
|
|
1040
|
+
|
|
1041
|
+
// Should work now that there's no error
|
|
1042
|
+
const result = await flowResult;
|
|
1043
|
+
expect(result.endState.outcome).toBe("test");
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
it("block navigation after data changes on first input, show warning on second input, then navigation succeeds", async () => {
|
|
1047
|
+
player.start(simpleFlow);
|
|
1048
|
+
const state = player.getState() as InProgressState;
|
|
1049
|
+
const { flowResult } = state;
|
|
1050
|
+
// Starts out with nothing
|
|
1051
|
+
expect(
|
|
1052
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1053
|
+
).toBe(undefined);
|
|
1054
|
+
|
|
1055
|
+
expect(
|
|
1056
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation,
|
|
1057
|
+
).toBe(undefined);
|
|
1058
|
+
|
|
1059
|
+
state.controllers.data.set([["data.thing1", "sam"]]);
|
|
1060
|
+
|
|
1061
|
+
// Try to transition
|
|
1062
|
+
state.controllers.flow.transition("foo");
|
|
1063
|
+
|
|
1064
|
+
// Stays on the same view
|
|
1065
|
+
expect(
|
|
1066
|
+
state.controllers.flow.current?.currentState?.value.state_type,
|
|
1067
|
+
).toBe("VIEW");
|
|
1068
|
+
|
|
1069
|
+
expect(
|
|
1070
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1071
|
+
).toBe(undefined);
|
|
1072
|
+
|
|
1073
|
+
expect(
|
|
1074
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation,
|
|
1075
|
+
).not.toBe(undefined);
|
|
1076
|
+
|
|
1077
|
+
state.controllers.data.set([["data.thing1", "bilbo"]]);
|
|
1078
|
+
|
|
1079
|
+
// Try to transition
|
|
1080
|
+
state.controllers.flow.transition("foo");
|
|
1081
|
+
|
|
1082
|
+
// Should transition to end since data changes already occured on first input
|
|
1083
|
+
expect(
|
|
1084
|
+
state.controllers.flow.current?.currentState?.value.state_type,
|
|
1085
|
+
).toBe("END");
|
|
1086
|
+
|
|
1087
|
+
// Should work now that there's no error
|
|
1088
|
+
const result = await flowResult;
|
|
1089
|
+
expect(result.endState.outcome).toBe("test");
|
|
1090
|
+
});
|
|
1091
|
+
it("doesnt remove existing expression warnings if a new warning is triggered", async () => {
|
|
1092
|
+
player.hooks.expressionEvaluator.tap("test", (evaluator) => {
|
|
1093
|
+
evaluator.addExpressionFunction("isEmpty", (ctx: any, val: any) => {
|
|
1094
|
+
if (val === undefined || val === null) {
|
|
1095
|
+
return true;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (typeof val === "string") {
|
|
1099
|
+
return val.length === 0;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
return false;
|
|
1103
|
+
});
|
|
1104
|
+
});
|
|
1105
|
+
player.start(simpleExpressionFlow);
|
|
1106
|
+
const state = player.getState() as InProgressState;
|
|
1107
|
+
const { flowResult } = state;
|
|
1108
|
+
// Starts out with nothing
|
|
1109
|
+
expect(
|
|
1110
|
+
state.controllers.view.currentView?.lastUpdate?.foo.asset.validation,
|
|
1111
|
+
).toBe(undefined);
|
|
1112
|
+
|
|
1113
|
+
expect(
|
|
1114
|
+
state.controllers.view.currentView?.lastUpdate?.bar.asset.validation,
|
|
1115
|
+
).toBe(undefined);
|
|
1116
|
+
|
|
1117
|
+
state.controllers.data.set([["data.foo2", "someData"]]);
|
|
1118
|
+
|
|
1119
|
+
// Try to transition
|
|
1120
|
+
state.controllers.flow.transition("foo");
|
|
1121
|
+
|
|
1122
|
+
// Stays on the same view
|
|
1123
|
+
expect(
|
|
1124
|
+
state.controllers.flow.current?.currentState?.value.state_type,
|
|
1125
|
+
).toBe("VIEW");
|
|
1126
|
+
|
|
1127
|
+
expect(
|
|
1128
|
+
state.controllers.view.currentView?.lastUpdate?.foo.asset.validation,
|
|
1129
|
+
).not.toBe(undefined);
|
|
1130
|
+
|
|
1131
|
+
expect(
|
|
1132
|
+
state.controllers.view.currentView?.lastUpdate?.foo2.asset.validation,
|
|
1133
|
+
).toBe(undefined);
|
|
1134
|
+
|
|
1135
|
+
expect(
|
|
1136
|
+
state.controllers.view.currentView?.lastUpdate?.bar.asset.validation,
|
|
1137
|
+
).toBe(undefined);
|
|
1138
|
+
|
|
1139
|
+
expect(
|
|
1140
|
+
state.controllers.view.currentView?.lastUpdate?.bar2.asset.validation,
|
|
1141
|
+
).toBe(undefined);
|
|
1142
|
+
|
|
1143
|
+
state.controllers.data.set([["data.bar2", "someData"]]);
|
|
1144
|
+
|
|
1145
|
+
// Try to transition
|
|
1146
|
+
state.controllers.flow.transition("foo");
|
|
1147
|
+
|
|
1148
|
+
// Stays on the same view
|
|
1149
|
+
expect(
|
|
1150
|
+
state.controllers.flow.current?.currentState?.value.state_type,
|
|
1151
|
+
).toBe("VIEW");
|
|
1152
|
+
|
|
1153
|
+
// existing validation
|
|
1154
|
+
// FAILS HERE
|
|
1155
|
+
expect(
|
|
1156
|
+
state.controllers.view.currentView?.lastUpdate?.foo.asset.validation,
|
|
1157
|
+
).not.toBe(undefined);
|
|
1158
|
+
|
|
1159
|
+
expect(
|
|
1160
|
+
state.controllers.view.currentView?.lastUpdate?.foo2.asset.validation,
|
|
1161
|
+
).toBe(undefined);
|
|
1162
|
+
|
|
1163
|
+
// new validation
|
|
1164
|
+
expect(
|
|
1165
|
+
state.controllers.view.currentView?.lastUpdate?.bar.asset.validation,
|
|
1166
|
+
).not.toBe(undefined);
|
|
1167
|
+
|
|
1168
|
+
expect(
|
|
1169
|
+
state.controllers.view.currentView?.lastUpdate?.bar2.asset.validation,
|
|
1170
|
+
).toBe(undefined);
|
|
1171
|
+
|
|
1172
|
+
state.controllers.data.set([["data.foo", "frodo"]]);
|
|
1173
|
+
state.controllers.data.set([["data.bar", "sam"]]);
|
|
1174
|
+
|
|
1175
|
+
// Try to transition again
|
|
1176
|
+
state.controllers.flow.transition("foo");
|
|
1177
|
+
|
|
1178
|
+
// Should work now that there's no error
|
|
1179
|
+
const result = await flowResult;
|
|
1180
|
+
expect(result.endState.outcome).toBe("test");
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
it("autodismiss if data change already took place on input with warning, manually dismiss second warning", async () => {
|
|
1184
|
+
player.start(simpleFlow);
|
|
1185
|
+
const state = player.getState() as InProgressState;
|
|
1186
|
+
const { flowResult } = state;
|
|
1187
|
+
// Starts out with nothing
|
|
1188
|
+
expect(
|
|
1189
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1190
|
+
).toBe(undefined);
|
|
1191
|
+
|
|
1192
|
+
expect(
|
|
1193
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation,
|
|
1194
|
+
).toBe(undefined);
|
|
1195
|
+
|
|
1196
|
+
state.controllers.data.set([["data.thing1", "sam"]]);
|
|
1197
|
+
|
|
1198
|
+
// Try to transition
|
|
1199
|
+
state.controllers.flow.transition("foo");
|
|
1200
|
+
|
|
1201
|
+
// Stays on the same view
|
|
1202
|
+
expect(
|
|
1203
|
+
state.controllers.flow.current?.currentState?.value.state_type,
|
|
1204
|
+
).toBe("VIEW");
|
|
1205
|
+
|
|
1206
|
+
expect(
|
|
1207
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1208
|
+
).toBe(undefined);
|
|
1209
|
+
|
|
1210
|
+
expect(
|
|
1211
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation,
|
|
1212
|
+
).not.toBe(undefined);
|
|
1213
|
+
|
|
1214
|
+
state.controllers.data.set([["data.thing1", "bilbo"]]);
|
|
1215
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation.dismiss();
|
|
1216
|
+
|
|
1217
|
+
// Try to transition
|
|
1218
|
+
state.controllers.flow.transition("foo");
|
|
1219
|
+
|
|
1220
|
+
// Since data change (setting "sam") already triggered validation next step is auto dismiss
|
|
1221
|
+
expect(
|
|
1222
|
+
state.controllers.flow.current?.currentState?.value.state_type,
|
|
1223
|
+
).toBe("END");
|
|
1224
|
+
|
|
1225
|
+
// Should work now that there's no error
|
|
1226
|
+
const result = await flowResult;
|
|
1227
|
+
expect(result.endState.outcome).toBe("test");
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
it("should auto-dismiss when dismissal is triggered", async () => {
|
|
1231
|
+
player.start(multipleWarningsFlow);
|
|
1232
|
+
const state = player.getState() as InProgressState;
|
|
1233
|
+
const { flowResult } = state;
|
|
1234
|
+
// Starts with one warning
|
|
1235
|
+
expect(
|
|
1236
|
+
state.controllers.view.currentView?.lastUpdate?.loadWarning.asset
|
|
1237
|
+
.validation,
|
|
1238
|
+
).toBeDefined();
|
|
1239
|
+
|
|
1240
|
+
expect(
|
|
1241
|
+
state.controllers.view.currentView?.lastUpdate?.navigationWarning.asset
|
|
1242
|
+
.validation,
|
|
1243
|
+
).toBeUndefined();
|
|
1244
|
+
|
|
1245
|
+
// Try to transition
|
|
1246
|
+
state.controllers.flow.transition("next");
|
|
1247
|
+
|
|
1248
|
+
// Stays on the same view
|
|
1249
|
+
expect(
|
|
1250
|
+
state.controllers.flow.current?.currentState?.value.state_type,
|
|
1251
|
+
).toBe("VIEW");
|
|
1252
|
+
|
|
1253
|
+
// new warning appears
|
|
1254
|
+
expect(
|
|
1255
|
+
state.controllers.view.currentView?.lastUpdate?.loadWarning.asset
|
|
1256
|
+
.validation,
|
|
1257
|
+
).toBeDefined();
|
|
1258
|
+
|
|
1259
|
+
expect(
|
|
1260
|
+
state.controllers.view.currentView?.lastUpdate?.navigationWarning.asset
|
|
1261
|
+
.validation,
|
|
1262
|
+
).toBeDefined();
|
|
1263
|
+
|
|
1264
|
+
// Try to transition
|
|
1265
|
+
state.controllers.flow.transition("next");
|
|
1266
|
+
|
|
1267
|
+
// Since data change (setting "sam") already triggered validation next step is auto dismiss
|
|
1268
|
+
expect(
|
|
1269
|
+
state.controllers.flow.current?.currentState?.value.state_type,
|
|
1270
|
+
).toBe("END");
|
|
1271
|
+
|
|
1272
|
+
// Should work now that there's no error
|
|
1273
|
+
const result = await flowResult;
|
|
1274
|
+
expect(result.endState.outcome).toBe("done");
|
|
1275
|
+
});
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
describe("introspection and filtering", () => {
|
|
1279
|
+
/**
|
|
1280
|
+
*
|
|
1281
|
+
*/
|
|
1282
|
+
const getAllKnownValidations = () => {
|
|
1283
|
+
const allBindings = validationController.getBindings();
|
|
1284
|
+
const allValidations = Array.from(allBindings).flatMap((b) => {
|
|
1285
|
+
const validatedBinding =
|
|
1286
|
+
validationController.getValidationForBinding(b);
|
|
1287
|
+
|
|
1288
|
+
if (!validatedBinding) {
|
|
1289
|
+
return [];
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
return validatedBinding.allValidations.map((v) => {
|
|
1293
|
+
return {
|
|
1294
|
+
binding: b,
|
|
1295
|
+
validation: v,
|
|
1296
|
+
response: validationController.validationRunner(v.value, b),
|
|
1297
|
+
};
|
|
1298
|
+
});
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
return allValidations;
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1304
|
+
it("can query all triggered validations", async () => {
|
|
1305
|
+
const state = player.getState() as InProgressState;
|
|
1306
|
+
state.controllers.data.set([["data.thing4", "not-sam"]]);
|
|
1307
|
+
|
|
1308
|
+
await vitest.waitFor(() => {
|
|
1309
|
+
expect(
|
|
1310
|
+
state.controllers.view.currentView?.lastUpdate?.alreadyInvalidData
|
|
1311
|
+
.asset.validation.message,
|
|
1312
|
+
).toBe("Names just be in: sam");
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
const currentValidations = getAllKnownValidations();
|
|
1316
|
+
|
|
1317
|
+
expect(currentValidations).toHaveLength(5);
|
|
1318
|
+
expect(
|
|
1319
|
+
currentValidations[0].validation.value[VALIDATION_PROVIDER_NAME_SYMBOL],
|
|
1320
|
+
).toBe("schema");
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
it("can compute new validations without dismissing existing ones", async () => {
|
|
1324
|
+
const updatedFlow = {
|
|
1325
|
+
...flowWithThings,
|
|
1326
|
+
views: [
|
|
1327
|
+
{
|
|
1328
|
+
...flowWithThings.views?.[0],
|
|
1329
|
+
validation: [
|
|
1330
|
+
{
|
|
1331
|
+
type: "expression",
|
|
1332
|
+
ref: "data.thing2",
|
|
1333
|
+
message: "Both need to equal 100",
|
|
1334
|
+
exp: "{{data.thing1}} + {{data.thing2}} == 100",
|
|
1335
|
+
},
|
|
1336
|
+
],
|
|
1337
|
+
},
|
|
1338
|
+
],
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
player.start(updatedFlow as any);
|
|
1342
|
+
const currentValidations = getAllKnownValidations();
|
|
1343
|
+
expect(currentValidations).toHaveLength(6);
|
|
1344
|
+
});
|
|
1345
|
+
});
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
describe("cross-field validation", () => {
|
|
1349
|
+
const crossFieldFlow = makeFlow({
|
|
1350
|
+
id: "view-1",
|
|
1351
|
+
type: "view",
|
|
1352
|
+
thing1: {
|
|
1353
|
+
asset: {
|
|
1354
|
+
id: "thing-1",
|
|
1355
|
+
binding: "foo.data.thing1",
|
|
1356
|
+
type: "input",
|
|
1357
|
+
},
|
|
1358
|
+
},
|
|
1359
|
+
thing2: {
|
|
1360
|
+
asset: {
|
|
1361
|
+
id: "thing-2",
|
|
1362
|
+
binding: "foo.data.thing2",
|
|
1363
|
+
type: "input",
|
|
1364
|
+
},
|
|
1365
|
+
},
|
|
1366
|
+
validation: [
|
|
1367
|
+
{
|
|
1368
|
+
type: "expression",
|
|
1369
|
+
ref: "foo.data.thing1",
|
|
1370
|
+
message: "Both need to equal 100",
|
|
1371
|
+
exp: "{{foo.data.thing1}} + {{foo.data.thing2}} == 100",
|
|
1372
|
+
},
|
|
1373
|
+
],
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
it("works for navigate triggers", async () => {
|
|
1377
|
+
const player = new Player({
|
|
1378
|
+
plugins: [new TrackBindingPlugin()],
|
|
1379
|
+
});
|
|
1380
|
+
player.start(crossFieldFlow);
|
|
1381
|
+
const state = player.getState() as InProgressState;
|
|
1382
|
+
|
|
1383
|
+
// Validation starts as nothing
|
|
1384
|
+
expect(
|
|
1385
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1386
|
+
).toBe(undefined);
|
|
1387
|
+
|
|
1388
|
+
// Updating a thing is still nothing (haven't navigated yet)
|
|
1389
|
+
state.controllers.data.set([["foo.data.thing1", 20]]);
|
|
1390
|
+
expect(
|
|
1391
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1392
|
+
).toBe(undefined);
|
|
1393
|
+
|
|
1394
|
+
// Try to navigate, should show the validation now
|
|
1395
|
+
state.controllers.flow.transition("next");
|
|
1396
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1397
|
+
"VIEW",
|
|
1398
|
+
);
|
|
1399
|
+
expect(
|
|
1400
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1401
|
+
).toMatchObject({
|
|
1402
|
+
severity: "error",
|
|
1403
|
+
message: "Both need to equal 100",
|
|
1404
|
+
displayTarget: "field",
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
// Updating a thing is still nothing (haven't navigated yet)
|
|
1408
|
+
state.controllers.data.set([["foo.data.thing2", 85]]);
|
|
1409
|
+
expect(
|
|
1410
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1411
|
+
).toMatchObject({
|
|
1412
|
+
severity: "error",
|
|
1413
|
+
message: "Both need to equal 100",
|
|
1414
|
+
displayTarget: "field",
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
// Set it equal to 100 and continue on
|
|
1418
|
+
state.controllers.data.set([["foo.data.thing2", 80]]);
|
|
1419
|
+
state.controllers.flow.transition("next");
|
|
1420
|
+
|
|
1421
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1422
|
+
"END",
|
|
1423
|
+
);
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
it("takes precedence over schema validation for the same binding", async () => {
|
|
1427
|
+
const player = new Player({
|
|
1428
|
+
plugins: [new TrackBindingPlugin()],
|
|
1429
|
+
});
|
|
1430
|
+
player.start(simpleFlowWithViewValidation);
|
|
1431
|
+
const state = player.getState() as InProgressState;
|
|
1432
|
+
|
|
1433
|
+
// Validation starts as nothing
|
|
1434
|
+
expect(
|
|
1435
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1436
|
+
).toBe(undefined);
|
|
1437
|
+
|
|
1438
|
+
// Try to navigate, should show the validation now
|
|
1439
|
+
state.controllers.flow.transition("next");
|
|
1440
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1441
|
+
"VIEW",
|
|
1442
|
+
);
|
|
1443
|
+
expect(
|
|
1444
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1445
|
+
).toMatchObject({
|
|
1446
|
+
severity: "error",
|
|
1447
|
+
message: "Must be greater than 50",
|
|
1448
|
+
displayTarget: "field",
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
// Updating a thing is still nothing (haven't navigated yet)
|
|
1452
|
+
state.controllers.data.set([["data.thing1", 51]]);
|
|
1453
|
+
expect(
|
|
1454
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1455
|
+
).toMatchObject({
|
|
1456
|
+
severity: "error",
|
|
1457
|
+
message: "Must be greater than 50",
|
|
1458
|
+
displayTarget: "field",
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
// Set it equal to 100 and continue on
|
|
1462
|
+
state.controllers.flow.transition("next");
|
|
1463
|
+
|
|
1464
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1465
|
+
"END",
|
|
1466
|
+
);
|
|
1467
|
+
});
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
test("shows errors on load", () => {
|
|
1471
|
+
const errFlow = makeFlow({
|
|
1472
|
+
id: "view-1",
|
|
1473
|
+
type: "view",
|
|
1474
|
+
thing1: {
|
|
1475
|
+
asset: {
|
|
1476
|
+
id: "thing-1",
|
|
1477
|
+
binding: "foo.data.thing1",
|
|
1478
|
+
type: "input",
|
|
1479
|
+
},
|
|
1480
|
+
},
|
|
1481
|
+
validation: [
|
|
1482
|
+
{
|
|
1483
|
+
type: "required",
|
|
1484
|
+
ref: "foo.data.thing1",
|
|
1485
|
+
message: "Stuffs broken",
|
|
1486
|
+
trigger: "load",
|
|
1487
|
+
severity: "error",
|
|
1488
|
+
},
|
|
1489
|
+
],
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
1493
|
+
player.start(errFlow);
|
|
1494
|
+
const state = player.getState() as InProgressState;
|
|
1495
|
+
|
|
1496
|
+
// Validation starts with a warning on load
|
|
1497
|
+
expect(
|
|
1498
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1499
|
+
).toMatchObject({
|
|
1500
|
+
message: "Stuffs broken",
|
|
1501
|
+
severity: "error",
|
|
1502
|
+
displayTarget: "field",
|
|
1503
|
+
});
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
describe("errors", () => {
|
|
1507
|
+
const errorFlow = makeFlow({
|
|
1508
|
+
id: "view-1",
|
|
1509
|
+
type: "view",
|
|
1510
|
+
thing1: {
|
|
1511
|
+
asset: {
|
|
1512
|
+
id: "thing-1",
|
|
1513
|
+
binding: "foo.data.thing1",
|
|
1514
|
+
type: "input",
|
|
1515
|
+
},
|
|
1516
|
+
},
|
|
1517
|
+
validation: [
|
|
1518
|
+
{
|
|
1519
|
+
type: "required",
|
|
1520
|
+
ref: "foo.data.thing1",
|
|
1521
|
+
trigger: "load",
|
|
1522
|
+
severity: "error",
|
|
1523
|
+
},
|
|
1524
|
+
],
|
|
1525
|
+
});
|
|
1526
|
+
const nonBlockingErrorFlow = makeFlow({
|
|
1527
|
+
id: "view-1",
|
|
1528
|
+
type: "view",
|
|
1529
|
+
thing1: {
|
|
1530
|
+
asset: {
|
|
1531
|
+
id: "thing-1",
|
|
1532
|
+
binding: "foo.data.thing1",
|
|
1533
|
+
type: "input",
|
|
1534
|
+
},
|
|
1535
|
+
},
|
|
1536
|
+
validation: [
|
|
1537
|
+
{
|
|
1538
|
+
type: "required",
|
|
1539
|
+
ref: "foo.data.thing1",
|
|
1540
|
+
trigger: "load",
|
|
1541
|
+
severity: "error",
|
|
1542
|
+
blocking: false,
|
|
1543
|
+
},
|
|
1544
|
+
],
|
|
1545
|
+
});
|
|
1546
|
+
const onceBlockingErrorFlow = makeFlow({
|
|
1547
|
+
id: "view-1",
|
|
1548
|
+
type: "view",
|
|
1549
|
+
thing1: {
|
|
1550
|
+
asset: {
|
|
1551
|
+
id: "thing-1",
|
|
1552
|
+
binding: "foo.data.thing1",
|
|
1553
|
+
type: "input",
|
|
1554
|
+
},
|
|
1555
|
+
},
|
|
1556
|
+
validation: [
|
|
1557
|
+
{
|
|
1558
|
+
type: "required",
|
|
1559
|
+
ref: "foo.data.thing1",
|
|
1560
|
+
trigger: "navigation",
|
|
1561
|
+
severity: "error",
|
|
1562
|
+
blocking: "once",
|
|
1563
|
+
},
|
|
1564
|
+
],
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1567
|
+
const oneInputWithErrorOnLoadBlockingFalseAndWarningNavigationTriggerFlow =
|
|
1568
|
+
makeFlow({
|
|
1569
|
+
id: "view-1",
|
|
1570
|
+
type: "view",
|
|
1571
|
+
thing1: {
|
|
1572
|
+
asset: {
|
|
1573
|
+
id: "thing-1",
|
|
1574
|
+
binding: "foo.data.thing1",
|
|
1575
|
+
type: "input",
|
|
1576
|
+
},
|
|
1577
|
+
},
|
|
1578
|
+
validation: [
|
|
1579
|
+
{
|
|
1580
|
+
type: "required",
|
|
1581
|
+
ref: "foo.data.thing1",
|
|
1582
|
+
severity: "error",
|
|
1583
|
+
trigger: "load",
|
|
1584
|
+
blocking: "false",
|
|
1585
|
+
},
|
|
1586
|
+
{
|
|
1587
|
+
type: "required",
|
|
1588
|
+
ref: "foo.data.thing1",
|
|
1589
|
+
trigger: "navigation",
|
|
1590
|
+
severity: "warning",
|
|
1591
|
+
},
|
|
1592
|
+
],
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
const oneInputWithErrorOnLoadBlockingFalseAndWarningChangeTriggerFlow =
|
|
1596
|
+
makeFlow({
|
|
1597
|
+
id: "view-1",
|
|
1598
|
+
type: "view",
|
|
1599
|
+
thing1: {
|
|
1600
|
+
asset: {
|
|
1601
|
+
id: "thing-1",
|
|
1602
|
+
binding: "foo.data.thing1",
|
|
1603
|
+
type: "input",
|
|
1604
|
+
},
|
|
1605
|
+
},
|
|
1606
|
+
validation: [
|
|
1607
|
+
{
|
|
1608
|
+
type: "required",
|
|
1609
|
+
ref: "foo.data.thing1",
|
|
1610
|
+
severity: "error",
|
|
1611
|
+
trigger: "load",
|
|
1612
|
+
blocking: "false",
|
|
1613
|
+
},
|
|
1614
|
+
{
|
|
1615
|
+
type: "required",
|
|
1616
|
+
ref: "foo.data.thing1",
|
|
1617
|
+
trigger: "change",
|
|
1618
|
+
severity: "warning",
|
|
1619
|
+
},
|
|
1620
|
+
],
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
it("blocks navigation by default", async () => {
|
|
1624
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
1625
|
+
player.start(errorFlow);
|
|
1626
|
+
const state = player.getState() as InProgressState;
|
|
1627
|
+
|
|
1628
|
+
// Try to navigate, should prevent the navigation and display the error
|
|
1629
|
+
state.controllers.flow.transition("next");
|
|
1630
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1631
|
+
"VIEW",
|
|
1632
|
+
);
|
|
1633
|
+
expect(
|
|
1634
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1635
|
+
).toMatchObject({
|
|
1636
|
+
message: "A value is required",
|
|
1637
|
+
severity: "error",
|
|
1638
|
+
displayTarget: "field",
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
// Try to navigate, should prevent the navigation and keep displaying the error
|
|
1642
|
+
state.controllers.flow.transition("next");
|
|
1643
|
+
// We make it to the next state
|
|
1644
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1645
|
+
"VIEW",
|
|
1646
|
+
);
|
|
1647
|
+
});
|
|
1648
|
+
it("blocking once allows navigation on second attempt", async () => {
|
|
1649
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
1650
|
+
player.start(onceBlockingErrorFlow);
|
|
1651
|
+
const state = player.getState() as InProgressState;
|
|
1652
|
+
|
|
1653
|
+
// Try to navigate, should prevent the navigation and display the error
|
|
1654
|
+
state.controllers.flow.transition("next");
|
|
1655
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1656
|
+
"VIEW",
|
|
1657
|
+
);
|
|
1658
|
+
expect(
|
|
1659
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1660
|
+
).toMatchObject({
|
|
1661
|
+
message: "A value is required",
|
|
1662
|
+
severity: "error",
|
|
1663
|
+
displayTarget: "field",
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
// Navigate _again_ this should dismiss it
|
|
1667
|
+
state.controllers.flow.transition("next");
|
|
1668
|
+
// We make it to the next state
|
|
1669
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1670
|
+
"END",
|
|
1671
|
+
);
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
it("error on load blocking false then warning with change trigger on navigation attempt", async () => {
|
|
1675
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
1676
|
+
player.start(
|
|
1677
|
+
oneInputWithErrorOnLoadBlockingFalseAndWarningChangeTriggerFlow,
|
|
1678
|
+
);
|
|
1679
|
+
const state = player.getState() as InProgressState;
|
|
1680
|
+
|
|
1681
|
+
expect(
|
|
1682
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1683
|
+
).toMatchObject({
|
|
1684
|
+
message: "A value is required",
|
|
1685
|
+
severity: "error",
|
|
1686
|
+
displayTarget: "field",
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
// Try to navigate, should prevent the navigation and display the warning
|
|
1690
|
+
state.controllers.flow.transition("next");
|
|
1691
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1692
|
+
"VIEW",
|
|
1693
|
+
);
|
|
1694
|
+
|
|
1695
|
+
expect(
|
|
1696
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1697
|
+
).toMatchObject({
|
|
1698
|
+
message: "A value is required",
|
|
1699
|
+
severity: "warning",
|
|
1700
|
+
displayTarget: "field",
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
// Navigate _again_ this should dismiss it
|
|
1704
|
+
state.controllers.flow.transition("next");
|
|
1705
|
+
// We make it to the next state
|
|
1706
|
+
|
|
1707
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1708
|
+
"END",
|
|
1709
|
+
);
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
it("error on load blocking false then warning on navigation attempt", async () => {
|
|
1713
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
1714
|
+
player.start(
|
|
1715
|
+
oneInputWithErrorOnLoadBlockingFalseAndWarningNavigationTriggerFlow,
|
|
1716
|
+
);
|
|
1717
|
+
const state = player.getState() as InProgressState;
|
|
1718
|
+
|
|
1719
|
+
expect(
|
|
1720
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1721
|
+
).toMatchObject({
|
|
1722
|
+
message: "A value is required",
|
|
1723
|
+
severity: "error",
|
|
1724
|
+
displayTarget: "field",
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
// Try to navigate, should prevent the navigation and display the warning
|
|
1728
|
+
state.controllers.flow.transition("next");
|
|
1729
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1730
|
+
"VIEW",
|
|
1731
|
+
);
|
|
1732
|
+
|
|
1733
|
+
expect(
|
|
1734
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1735
|
+
).toMatchObject({
|
|
1736
|
+
message: "A value is required",
|
|
1737
|
+
severity: "warning",
|
|
1738
|
+
displayTarget: "field",
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
// Navigate _again_ this should dismiss it
|
|
1742
|
+
state.controllers.flow.transition("next");
|
|
1743
|
+
// We make it to the next state
|
|
1744
|
+
|
|
1745
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1746
|
+
"END",
|
|
1747
|
+
);
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
it("error on load blocking false then input active then warning on navigation attempt", async () => {
|
|
1751
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
1752
|
+
player.start(
|
|
1753
|
+
oneInputWithErrorOnLoadBlockingFalseAndWarningNavigationTriggerFlow,
|
|
1754
|
+
);
|
|
1755
|
+
const state = player.getState() as InProgressState;
|
|
1756
|
+
|
|
1757
|
+
expect(
|
|
1758
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1759
|
+
).toMatchObject({
|
|
1760
|
+
message: "A value is required",
|
|
1761
|
+
severity: "error",
|
|
1762
|
+
displayTarget: "field",
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
// Type something to dismiss the error, should be empty to see the warning
|
|
1766
|
+
state.controllers.data.set([["foo.data.thing1", ""]]);
|
|
1767
|
+
|
|
1768
|
+
// Try to navigate, should prevent the navigation and display the warning
|
|
1769
|
+
state.controllers.flow.transition("next");
|
|
1770
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1771
|
+
"VIEW",
|
|
1772
|
+
);
|
|
1773
|
+
|
|
1774
|
+
expect(
|
|
1775
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1776
|
+
).toMatchObject({
|
|
1777
|
+
message: "A value is required",
|
|
1778
|
+
severity: "warning",
|
|
1779
|
+
displayTarget: "field",
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
// Navigate _again_ this should dismiss it
|
|
1783
|
+
state.controllers.flow.transition("next");
|
|
1784
|
+
// We make it to the next state
|
|
1785
|
+
|
|
1786
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1787
|
+
"END",
|
|
1788
|
+
);
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
it("blocking false allows navigation", async () => {
|
|
1792
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
1793
|
+
player.start(nonBlockingErrorFlow);
|
|
1794
|
+
const state = player.getState() as InProgressState;
|
|
1795
|
+
|
|
1796
|
+
// Try to navigate, should allow navigation because blocking is false
|
|
1797
|
+
state.controllers.flow.transition("next");
|
|
1798
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1799
|
+
"END",
|
|
1800
|
+
);
|
|
1801
|
+
});
|
|
1802
|
+
it("blocking false still shows validation", async () => {
|
|
1803
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
1804
|
+
player.start(nonBlockingErrorFlow);
|
|
1805
|
+
const state = player.getState() as InProgressState;
|
|
1806
|
+
|
|
1807
|
+
expect(
|
|
1808
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
1809
|
+
).toMatchObject({
|
|
1810
|
+
message: "A value is required",
|
|
1811
|
+
severity: "error",
|
|
1812
|
+
displayTarget: "field",
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
// Try to navigate, should allow navigation because blocking is false
|
|
1816
|
+
state.controllers.flow.transition("next");
|
|
1817
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
1818
|
+
"END",
|
|
1819
|
+
);
|
|
1820
|
+
});
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
test("validations return non-blocking errors", async () => {
|
|
1824
|
+
const flow = makeFlow({
|
|
1825
|
+
id: "view-1",
|
|
1826
|
+
type: "view",
|
|
1827
|
+
blocking: {
|
|
1828
|
+
asset: {
|
|
1829
|
+
id: "thing-1",
|
|
1830
|
+
binding: "foo.blocking",
|
|
1831
|
+
type: "input",
|
|
1832
|
+
},
|
|
1833
|
+
},
|
|
1834
|
+
nonblocking: {
|
|
1835
|
+
asset: {
|
|
1836
|
+
id: "thing-2",
|
|
1837
|
+
binding: "foo.nonblocking",
|
|
1838
|
+
type: "input",
|
|
1839
|
+
},
|
|
1840
|
+
},
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
flow.schema = {
|
|
1844
|
+
ROOT: {
|
|
1845
|
+
foo: {
|
|
1846
|
+
type: "FooType",
|
|
1847
|
+
},
|
|
1848
|
+
},
|
|
1849
|
+
FooType: {
|
|
1850
|
+
blocking: {
|
|
1851
|
+
type: "TestType",
|
|
1852
|
+
validation: [
|
|
1853
|
+
{
|
|
1854
|
+
type: "required",
|
|
1855
|
+
},
|
|
1856
|
+
],
|
|
1857
|
+
},
|
|
1858
|
+
nonblocking: {
|
|
1859
|
+
type: "TestType",
|
|
1860
|
+
validation: [
|
|
1861
|
+
{
|
|
1862
|
+
type: "required",
|
|
1863
|
+
blocking: false,
|
|
1864
|
+
},
|
|
1865
|
+
],
|
|
1866
|
+
},
|
|
1867
|
+
},
|
|
1868
|
+
};
|
|
1869
|
+
|
|
1870
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
1871
|
+
player.start(flow);
|
|
1872
|
+
|
|
1873
|
+
/**
|
|
1874
|
+
*
|
|
1875
|
+
*/
|
|
1876
|
+
const getState = () => player.getState() as InProgressState;
|
|
1877
|
+
|
|
1878
|
+
/**
|
|
1879
|
+
*
|
|
1880
|
+
*/
|
|
1881
|
+
const getCurrentView = () =>
|
|
1882
|
+
getState().controllers.view.currentView?.lastUpdate;
|
|
1883
|
+
|
|
1884
|
+
// No errors show up initially
|
|
1885
|
+
|
|
1886
|
+
await vitest.waitFor(() => {
|
|
1887
|
+
expect(getState().controllers.view.currentView?.lastUpdate?.id).toBe(
|
|
1888
|
+
"view-1",
|
|
1889
|
+
);
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
expect(getCurrentView()?.blocking.asset.validation).toBeUndefined();
|
|
1893
|
+
expect(getCurrentView()?.nonblocking.asset.validation).toBeUndefined();
|
|
1894
|
+
|
|
1895
|
+
getState().controllers.flow.transition("next");
|
|
1896
|
+
expect(
|
|
1897
|
+
getState().controllers.flow.current?.currentState?.value.state_type,
|
|
1898
|
+
).toBe("VIEW");
|
|
1899
|
+
|
|
1900
|
+
expect(player.getState().status).toBe("in-progress");
|
|
1901
|
+
|
|
1902
|
+
await vitest.waitFor(() => {
|
|
1903
|
+
expect(getCurrentView()?.blocking.asset.validation).toMatchObject({
|
|
1904
|
+
message: "A value is required",
|
|
1905
|
+
severity: "error",
|
|
1906
|
+
displayTarget: "field",
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
expect(getCurrentView()?.nonblocking.asset.validation).toMatchObject({
|
|
1910
|
+
message: "A value is required",
|
|
1911
|
+
severity: "error",
|
|
1912
|
+
displayTarget: "field",
|
|
1913
|
+
});
|
|
1914
|
+
});
|
|
1915
|
+
|
|
1916
|
+
getState().controllers.data.set([["foo.blocking", "foo"]]);
|
|
1917
|
+
|
|
1918
|
+
await vitest.waitFor(() => {
|
|
1919
|
+
expect(getCurrentView()?.blocking.asset.validation).toBeUndefined();
|
|
1920
|
+
|
|
1921
|
+
expect(getCurrentView()?.nonblocking.asset.validation).toMatchObject({
|
|
1922
|
+
message: "A value is required",
|
|
1923
|
+
severity: "error",
|
|
1924
|
+
displayTarget: "field",
|
|
1925
|
+
});
|
|
1926
|
+
});
|
|
1927
|
+
|
|
1928
|
+
getState().controllers.flow.transition("next");
|
|
1929
|
+
|
|
1930
|
+
await vitest.waitFor(() => {
|
|
1931
|
+
expect(player.getState().status).toBe("completed");
|
|
1932
|
+
});
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
describe("warnings", () => {
|
|
1936
|
+
const warningFlowOnNavigation = makeFlow({
|
|
1937
|
+
id: "view-1",
|
|
1938
|
+
type: "view",
|
|
1939
|
+
thing1: {
|
|
1940
|
+
asset: {
|
|
1941
|
+
id: "thing-1",
|
|
1942
|
+
binding: "foo.data.thing1",
|
|
1943
|
+
type: "input",
|
|
1944
|
+
},
|
|
1945
|
+
},
|
|
1946
|
+
validation: [
|
|
1947
|
+
{
|
|
1948
|
+
type: "required",
|
|
1949
|
+
ref: "foo.data.thing1",
|
|
1950
|
+
trigger: "navigation",
|
|
1951
|
+
severity: "warning",
|
|
1952
|
+
},
|
|
1953
|
+
],
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1956
|
+
const warningFlowOnLoad = makeFlow({
|
|
1957
|
+
id: "view-1",
|
|
1958
|
+
type: "view",
|
|
1959
|
+
thing1: {
|
|
1960
|
+
asset: {
|
|
1961
|
+
id: "thing-1",
|
|
1962
|
+
binding: "foo.data.thing1",
|
|
1963
|
+
type: "input",
|
|
1964
|
+
},
|
|
1965
|
+
},
|
|
1966
|
+
validation: [
|
|
1967
|
+
{
|
|
1968
|
+
type: "required",
|
|
1969
|
+
ref: "foo.data.thing1",
|
|
1970
|
+
trigger: "load",
|
|
1971
|
+
severity: "warning",
|
|
1972
|
+
},
|
|
1973
|
+
],
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
const blockingWarningFlow = makeFlow({
|
|
1977
|
+
id: "view-1",
|
|
1978
|
+
type: "view",
|
|
1979
|
+
thing1: {
|
|
1980
|
+
asset: {
|
|
1981
|
+
id: "thing-1",
|
|
1982
|
+
binding: "foo.data.thing1",
|
|
1983
|
+
type: "input",
|
|
1984
|
+
},
|
|
1985
|
+
},
|
|
1986
|
+
validation: [
|
|
1987
|
+
{
|
|
1988
|
+
type: "required",
|
|
1989
|
+
ref: "foo.data.thing1",
|
|
1990
|
+
trigger: "load",
|
|
1991
|
+
blocking: true,
|
|
1992
|
+
severity: "warning",
|
|
1993
|
+
},
|
|
1994
|
+
],
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
const onceBlockingWarningFlow = makeFlow({
|
|
1998
|
+
id: "view-1",
|
|
1999
|
+
type: "view",
|
|
2000
|
+
thing1: {
|
|
2001
|
+
asset: {
|
|
2002
|
+
id: "thing-1",
|
|
2003
|
+
binding: "foo.data.thing1",
|
|
2004
|
+
type: "input",
|
|
2005
|
+
},
|
|
2006
|
+
},
|
|
2007
|
+
validation: [
|
|
2008
|
+
{
|
|
2009
|
+
type: "required",
|
|
2010
|
+
ref: "foo.data.thing1",
|
|
2011
|
+
trigger: "navigation",
|
|
2012
|
+
blocking: "once",
|
|
2013
|
+
severity: "warning",
|
|
2014
|
+
},
|
|
2015
|
+
],
|
|
2016
|
+
});
|
|
2017
|
+
|
|
2018
|
+
const onceBlockingWarningFlowWithChangeTrigger = makeFlow({
|
|
2019
|
+
id: "view-1",
|
|
2020
|
+
type: "view",
|
|
2021
|
+
thing1: {
|
|
2022
|
+
asset: {
|
|
2023
|
+
id: "thing-1",
|
|
2024
|
+
binding: "foo.data.thing1",
|
|
2025
|
+
type: "input",
|
|
2026
|
+
},
|
|
2027
|
+
},
|
|
2028
|
+
validation: [
|
|
2029
|
+
{
|
|
2030
|
+
type: "required",
|
|
2031
|
+
ref: "foo.data.thing1",
|
|
2032
|
+
trigger: "change",
|
|
2033
|
+
blocking: "once",
|
|
2034
|
+
severity: "warning",
|
|
2035
|
+
},
|
|
2036
|
+
],
|
|
2037
|
+
});
|
|
2038
|
+
|
|
2039
|
+
it("shows warnings on load", () => {
|
|
2040
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
2041
|
+
player.start(warningFlowOnLoad);
|
|
2042
|
+
const state = player.getState() as InProgressState;
|
|
2043
|
+
|
|
2044
|
+
// Validation starts with a warning on load
|
|
2045
|
+
expect(
|
|
2046
|
+
omit(
|
|
2047
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2048
|
+
"dismiss",
|
|
2049
|
+
),
|
|
2050
|
+
).toMatchObject({
|
|
2051
|
+
message: "A value is required",
|
|
2052
|
+
severity: "warning",
|
|
2053
|
+
displayTarget: "field",
|
|
2054
|
+
});
|
|
2055
|
+
});
|
|
2056
|
+
|
|
2057
|
+
it("auto-dismiss on double-navigation", async () => {
|
|
2058
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
2059
|
+
player.start(warningFlowOnNavigation);
|
|
2060
|
+
const state = player.getState() as InProgressState;
|
|
2061
|
+
|
|
2062
|
+
// Try to navigate, should prevent the navigation and keep the warning
|
|
2063
|
+
state.controllers.flow.transition("next");
|
|
2064
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
2065
|
+
"VIEW",
|
|
2066
|
+
);
|
|
2067
|
+
expect(
|
|
2068
|
+
omit(
|
|
2069
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2070
|
+
"dismiss",
|
|
2071
|
+
),
|
|
2072
|
+
).toMatchObject({
|
|
2073
|
+
message: "A value is required",
|
|
2074
|
+
severity: "warning",
|
|
2075
|
+
displayTarget: "field",
|
|
2076
|
+
});
|
|
2077
|
+
|
|
2078
|
+
// Navigate _again_ this should dismiss it
|
|
2079
|
+
state.controllers.flow.transition("next");
|
|
2080
|
+
// We make it to the next state
|
|
2081
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
2082
|
+
"END",
|
|
2083
|
+
);
|
|
2084
|
+
});
|
|
2085
|
+
|
|
2086
|
+
it("should dismiss triggered navigation warnings on change", async () => {
|
|
2087
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
2088
|
+
player.start(warningFlowOnNavigation);
|
|
2089
|
+
const state = player.getState() as InProgressState;
|
|
2090
|
+
|
|
2091
|
+
// Try to navigate, should prevent the navigation and keep the warning
|
|
2092
|
+
state.controllers.flow.transition("next");
|
|
2093
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
2094
|
+
"VIEW",
|
|
2095
|
+
);
|
|
2096
|
+
expect(
|
|
2097
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2098
|
+
).toMatchObject(
|
|
2099
|
+
expect.objectContaining({
|
|
2100
|
+
message: "A value is required",
|
|
2101
|
+
severity: "warning",
|
|
2102
|
+
displayTarget: "field",
|
|
2103
|
+
}),
|
|
2104
|
+
);
|
|
2105
|
+
|
|
2106
|
+
state.controllers.data.set([["foo.data.thing1", "value"]]);
|
|
2107
|
+
|
|
2108
|
+
await vitest.waitFor(() => {
|
|
2109
|
+
expect(
|
|
2110
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2111
|
+
).toBeUndefined();
|
|
2112
|
+
});
|
|
2113
|
+
});
|
|
2114
|
+
|
|
2115
|
+
it("blocking warnings dont auto-dismiss on double-navigation", async () => {
|
|
2116
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
2117
|
+
player.start(blockingWarningFlow);
|
|
2118
|
+
const state = player.getState() as InProgressState;
|
|
2119
|
+
|
|
2120
|
+
// Try to navigate, should prevent the navigation and keep the warning
|
|
2121
|
+
state.controllers.flow.transition("next");
|
|
2122
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
2123
|
+
"VIEW",
|
|
2124
|
+
);
|
|
2125
|
+
expect(
|
|
2126
|
+
omit(
|
|
2127
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2128
|
+
"dismiss",
|
|
2129
|
+
),
|
|
2130
|
+
).toMatchObject({
|
|
2131
|
+
message: "A value is required",
|
|
2132
|
+
severity: "warning",
|
|
2133
|
+
displayTarget: "field",
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
// Navigate _again_ this should dismiss it
|
|
2137
|
+
state.controllers.flow.transition("next");
|
|
2138
|
+
// We make it to the next state
|
|
2139
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
2140
|
+
"VIEW",
|
|
2141
|
+
);
|
|
2142
|
+
});
|
|
2143
|
+
|
|
2144
|
+
it("warnings do not stop data saving", () => {
|
|
2145
|
+
const flow = makeFlow({
|
|
2146
|
+
asset: {
|
|
2147
|
+
id: "input-2",
|
|
2148
|
+
type: "input",
|
|
2149
|
+
binding: "person.name",
|
|
2150
|
+
label: {
|
|
2151
|
+
asset: {
|
|
2152
|
+
id: "input-2-label",
|
|
2153
|
+
type: "text",
|
|
2154
|
+
value: "Name",
|
|
2155
|
+
},
|
|
2156
|
+
},
|
|
2157
|
+
},
|
|
2158
|
+
});
|
|
2159
|
+
|
|
2160
|
+
flow.schema = {
|
|
2161
|
+
ROOT: {
|
|
2162
|
+
person: {
|
|
2163
|
+
type: "PersonType",
|
|
2164
|
+
},
|
|
2165
|
+
},
|
|
2166
|
+
PersonType: {
|
|
2167
|
+
name: {
|
|
2168
|
+
type: "StringType",
|
|
2169
|
+
validation: [
|
|
2170
|
+
{
|
|
2171
|
+
type: "names",
|
|
2172
|
+
names: ["frodo", "sam"],
|
|
2173
|
+
severity: "warning",
|
|
2174
|
+
},
|
|
2175
|
+
],
|
|
2176
|
+
},
|
|
2177
|
+
},
|
|
2178
|
+
};
|
|
2179
|
+
|
|
2180
|
+
const player = new Player({
|
|
2181
|
+
plugins: [new TrackBindingPlugin()],
|
|
2182
|
+
});
|
|
2183
|
+
player.start(flow);
|
|
2184
|
+
const state = player.getState() as InProgressState;
|
|
2185
|
+
|
|
2186
|
+
state.controllers.data.set([["person.name", "peter"]], {
|
|
2187
|
+
formatted: true,
|
|
2188
|
+
});
|
|
2189
|
+
|
|
2190
|
+
expect(
|
|
2191
|
+
state.controllers.data.get("person.name", { includeInvalid: false }),
|
|
2192
|
+
).toBe("peter");
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
it("errors still do stop data saving", () => {
|
|
2196
|
+
const flow = makeFlow({
|
|
2197
|
+
asset: {
|
|
2198
|
+
id: "input-2",
|
|
2199
|
+
type: "input",
|
|
2200
|
+
binding: "person.name",
|
|
2201
|
+
label: {
|
|
2202
|
+
asset: {
|
|
2203
|
+
id: "input-2-label",
|
|
2204
|
+
type: "text",
|
|
2205
|
+
value: "Name",
|
|
2206
|
+
},
|
|
2207
|
+
},
|
|
2208
|
+
},
|
|
2209
|
+
});
|
|
2210
|
+
|
|
2211
|
+
flow.schema = {
|
|
2212
|
+
ROOT: {
|
|
2213
|
+
person: {
|
|
2214
|
+
type: "PersonType",
|
|
2215
|
+
},
|
|
2216
|
+
},
|
|
2217
|
+
PersonType: {
|
|
2218
|
+
name: {
|
|
2219
|
+
type: "StringType",
|
|
2220
|
+
validation: [
|
|
2221
|
+
{
|
|
2222
|
+
type: "names",
|
|
2223
|
+
names: ["frodo", "sam"],
|
|
2224
|
+
},
|
|
2225
|
+
],
|
|
2226
|
+
},
|
|
2227
|
+
},
|
|
2228
|
+
};
|
|
2229
|
+
|
|
2230
|
+
const player = new Player({
|
|
2231
|
+
plugins: [new TrackBindingPlugin()],
|
|
2232
|
+
});
|
|
2233
|
+
player.start(flow);
|
|
2234
|
+
const state = player.getState() as InProgressState;
|
|
2235
|
+
|
|
2236
|
+
state.controllers.data.set([["person.name", "peter"]], {
|
|
2237
|
+
formatted: true,
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
expect(
|
|
2241
|
+
state.controllers.data.get("person.name", { includeInvalid: false }),
|
|
2242
|
+
).toBe(undefined);
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
it("once blocking warnings auto-dismiss on double-navigation", async () => {
|
|
2246
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
2247
|
+
player.start(onceBlockingWarningFlow);
|
|
2248
|
+
const state = player.getState() as InProgressState;
|
|
2249
|
+
|
|
2250
|
+
// Try to navigate, should prevent the navigation and keep the warning
|
|
2251
|
+
state.controllers.flow.transition("next");
|
|
2252
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
2253
|
+
"VIEW",
|
|
2254
|
+
);
|
|
2255
|
+
expect(
|
|
2256
|
+
omit(
|
|
2257
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2258
|
+
"dismiss",
|
|
2259
|
+
),
|
|
2260
|
+
).toMatchObject({
|
|
2261
|
+
message: "A value is required",
|
|
2262
|
+
severity: "warning",
|
|
2263
|
+
displayTarget: "field",
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
// Navigate _again_ this should dismiss it
|
|
2267
|
+
state.controllers.flow.transition("next");
|
|
2268
|
+
// We make it to the next state
|
|
2269
|
+
|
|
2270
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
2271
|
+
"END",
|
|
2272
|
+
);
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
it("once blocking warnings with change trigger auto-dismiss on double-navigation", async () => {
|
|
2276
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
2277
|
+
player.start(onceBlockingWarningFlowWithChangeTrigger);
|
|
2278
|
+
const state = player.getState() as InProgressState;
|
|
2279
|
+
|
|
2280
|
+
// Validation starts with no warnings on load
|
|
2281
|
+
expect(
|
|
2282
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2283
|
+
).toBeUndefined();
|
|
2284
|
+
|
|
2285
|
+
// Try to navigate, should prevent the navigation and show the warning
|
|
2286
|
+
state.controllers.flow.transition("next");
|
|
2287
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
2288
|
+
"VIEW",
|
|
2289
|
+
);
|
|
2290
|
+
expect(
|
|
2291
|
+
omit(
|
|
2292
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2293
|
+
"dismiss",
|
|
2294
|
+
),
|
|
2295
|
+
).toMatchObject({
|
|
2296
|
+
message: "A value is required",
|
|
2297
|
+
severity: "warning",
|
|
2298
|
+
displayTarget: "field",
|
|
2299
|
+
});
|
|
2300
|
+
|
|
2301
|
+
// Navigate _again_ this should dismiss it
|
|
2302
|
+
state.controllers.flow.transition("next");
|
|
2303
|
+
// We make it to the next state
|
|
2304
|
+
|
|
2305
|
+
await vitest.waitFor(() => {
|
|
2306
|
+
expect(
|
|
2307
|
+
state.controllers.flow.current?.currentState?.value.state_type,
|
|
2308
|
+
).toBe("END");
|
|
2309
|
+
});
|
|
2310
|
+
});
|
|
2311
|
+
|
|
2312
|
+
it("triggers re-render on dismiss call", () => {
|
|
2313
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
2314
|
+
player.start(warningFlowOnLoad);
|
|
2315
|
+
const state = player.getState() as InProgressState;
|
|
2316
|
+
|
|
2317
|
+
// Validation starts with a warning on load
|
|
2318
|
+
expect(
|
|
2319
|
+
omit(
|
|
2320
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2321
|
+
"dismiss",
|
|
2322
|
+
),
|
|
2323
|
+
).toMatchObject({
|
|
2324
|
+
message: "A value is required",
|
|
2325
|
+
severity: "warning",
|
|
2326
|
+
displayTarget: "field",
|
|
2327
|
+
});
|
|
2328
|
+
|
|
2329
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation.dismiss();
|
|
2330
|
+
expect(
|
|
2331
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2332
|
+
).toBe(undefined);
|
|
2333
|
+
|
|
2334
|
+
// Should be able to navigate w/o issues
|
|
2335
|
+
state.controllers.flow.transition("next");
|
|
2336
|
+
// We make it to the next state
|
|
2337
|
+
expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
|
|
2338
|
+
"END",
|
|
2339
|
+
);
|
|
2340
|
+
});
|
|
2341
|
+
});
|
|
2342
|
+
|
|
2343
|
+
describe("validation within arrays", () => {
|
|
2344
|
+
const arrayFlow = makeFlow({
|
|
2345
|
+
id: "view-1",
|
|
2346
|
+
type: "view",
|
|
2347
|
+
thing1: {
|
|
2348
|
+
asset: {
|
|
2349
|
+
id: "thing-1",
|
|
2350
|
+
binding: "thing.1.data.3.name",
|
|
2351
|
+
type: "input",
|
|
2352
|
+
},
|
|
2353
|
+
},
|
|
2354
|
+
thing2: {
|
|
2355
|
+
asset: {
|
|
2356
|
+
id: "thing-2",
|
|
2357
|
+
binding: "thing.2.data.0.name",
|
|
2358
|
+
type: "input",
|
|
2359
|
+
},
|
|
2360
|
+
},
|
|
2361
|
+
});
|
|
2362
|
+
|
|
2363
|
+
arrayFlow.schema = {
|
|
2364
|
+
ROOT: {
|
|
2365
|
+
thing: {
|
|
2366
|
+
type: "ThingType",
|
|
2367
|
+
isArray: true,
|
|
2368
|
+
},
|
|
2369
|
+
},
|
|
2370
|
+
ThingType: {
|
|
2371
|
+
data: {
|
|
2372
|
+
type: "DataType",
|
|
2373
|
+
isArray: true,
|
|
2374
|
+
},
|
|
2375
|
+
},
|
|
2376
|
+
DataType: {
|
|
2377
|
+
name: {
|
|
2378
|
+
type: "StringType",
|
|
2379
|
+
validation: [
|
|
2380
|
+
{
|
|
2381
|
+
type: "required",
|
|
2382
|
+
},
|
|
2383
|
+
],
|
|
2384
|
+
},
|
|
2385
|
+
},
|
|
2386
|
+
};
|
|
2387
|
+
|
|
2388
|
+
it("validates things correctly within an array", async () => {
|
|
2389
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
2390
|
+
player.start(arrayFlow);
|
|
2391
|
+
const state = player.getState() as InProgressState;
|
|
2392
|
+
|
|
2393
|
+
// Nothing initially
|
|
2394
|
+
expect(
|
|
2395
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2396
|
+
).toBe(undefined);
|
|
2397
|
+
expect(
|
|
2398
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation,
|
|
2399
|
+
).toBe(undefined);
|
|
2400
|
+
|
|
2401
|
+
// Error if set to an falsy value
|
|
2402
|
+
state.controllers.data.set([["thing.1.data.3.name", ""]]);
|
|
2403
|
+
await vitest.waitFor(() => {
|
|
2404
|
+
expect(
|
|
2405
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2406
|
+
).toMatchObject({
|
|
2407
|
+
severity: "error",
|
|
2408
|
+
message: "A value is required",
|
|
2409
|
+
displayTarget: "field",
|
|
2410
|
+
});
|
|
2411
|
+
expect(
|
|
2412
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation,
|
|
2413
|
+
).toBe(undefined);
|
|
2414
|
+
});
|
|
2415
|
+
|
|
2416
|
+
// Other one gets error if i try to navigate
|
|
2417
|
+
state.controllers.data.set([["thing.1.data.3.name", "adam"]]);
|
|
2418
|
+
state.controllers.flow.transition("anything");
|
|
2419
|
+
await vitest.waitFor(() => {
|
|
2420
|
+
expect(
|
|
2421
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2422
|
+
).toBe(undefined);
|
|
2423
|
+
expect(
|
|
2424
|
+
state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation,
|
|
2425
|
+
).toMatchObject({
|
|
2426
|
+
severity: "error",
|
|
2427
|
+
message: "A value is required",
|
|
2428
|
+
displayTarget: "field",
|
|
2429
|
+
});
|
|
2430
|
+
});
|
|
2431
|
+
});
|
|
2432
|
+
});
|
|
2433
|
+
|
|
2434
|
+
describe("models can get valid or invalid data", () => {
|
|
2435
|
+
const flow = makeFlow({
|
|
2436
|
+
asset: {
|
|
2437
|
+
id: "input-2",
|
|
2438
|
+
type: "input",
|
|
2439
|
+
binding: "person.name",
|
|
2440
|
+
label: {
|
|
2441
|
+
asset: {
|
|
2442
|
+
id: "input-2-label",
|
|
2443
|
+
type: "text",
|
|
2444
|
+
value: "Name",
|
|
2445
|
+
},
|
|
2446
|
+
},
|
|
2447
|
+
},
|
|
2448
|
+
});
|
|
2449
|
+
|
|
2450
|
+
flow.schema = {
|
|
2451
|
+
ROOT: {
|
|
2452
|
+
person: {
|
|
2453
|
+
type: "PersonType",
|
|
2454
|
+
},
|
|
2455
|
+
},
|
|
2456
|
+
PersonType: {
|
|
2457
|
+
name: {
|
|
2458
|
+
type: "StringType",
|
|
2459
|
+
validation: [
|
|
2460
|
+
{
|
|
2461
|
+
type: "names",
|
|
2462
|
+
names: ["frodo", "sam"],
|
|
2463
|
+
},
|
|
2464
|
+
],
|
|
2465
|
+
},
|
|
2466
|
+
},
|
|
2467
|
+
};
|
|
2468
|
+
|
|
2469
|
+
it("gets both", () => {
|
|
2470
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
2471
|
+
player.start(flow);
|
|
2472
|
+
const state = player.getState() as InProgressState;
|
|
2473
|
+
|
|
2474
|
+
state.controllers.data.set([["person.name", "adam"]]);
|
|
2475
|
+
|
|
2476
|
+
expect(state.controllers.data.get("person.name")).toBe(undefined);
|
|
2477
|
+
expect(
|
|
2478
|
+
state.controllers.data.get("person.name", { includeInvalid: true }),
|
|
2479
|
+
).toBe("adam");
|
|
2480
|
+
|
|
2481
|
+
state.controllers.data.set([["person.name", "sam"]]);
|
|
2482
|
+
expect(state.controllers.data.get("person.name")).toBe("sam");
|
|
2483
|
+
expect(
|
|
2484
|
+
state.controllers.data.get("person.name", { includeInvalid: true }),
|
|
2485
|
+
).toBe("sam");
|
|
2486
|
+
});
|
|
2487
|
+
});
|
|
2488
|
+
|
|
2489
|
+
test("validations can run against formatted or deformatted values", async () => {
|
|
2490
|
+
const flow = makeFlow({
|
|
2491
|
+
asset: {
|
|
2492
|
+
id: "input-2",
|
|
2493
|
+
type: "input",
|
|
2494
|
+
binding: "person.name",
|
|
2495
|
+
label: {
|
|
2496
|
+
asset: {
|
|
2497
|
+
id: "input-2-label",
|
|
2498
|
+
type: "text",
|
|
2499
|
+
value: "Name",
|
|
2500
|
+
},
|
|
2501
|
+
},
|
|
2502
|
+
},
|
|
2503
|
+
});
|
|
2504
|
+
|
|
2505
|
+
flow.schema = {
|
|
2506
|
+
ROOT: {
|
|
2507
|
+
person: {
|
|
2508
|
+
type: "PersonType",
|
|
2509
|
+
},
|
|
2510
|
+
},
|
|
2511
|
+
PersonType: {
|
|
2512
|
+
name: {
|
|
2513
|
+
type: "NumberType",
|
|
2514
|
+
format: {
|
|
2515
|
+
type: "indexOf",
|
|
2516
|
+
options: ["frodo", "sam"],
|
|
2517
|
+
},
|
|
2518
|
+
validation: [
|
|
2519
|
+
{
|
|
2520
|
+
type: "names",
|
|
2521
|
+
dataTarget: "formatted",
|
|
2522
|
+
names: ["frodo", "sam"],
|
|
2523
|
+
},
|
|
2524
|
+
],
|
|
2525
|
+
},
|
|
2526
|
+
},
|
|
2527
|
+
};
|
|
2528
|
+
|
|
2529
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
2530
|
+
|
|
2531
|
+
player.start(flow);
|
|
2532
|
+
const state = player.getState() as InProgressState;
|
|
2533
|
+
|
|
2534
|
+
state.controllers.data.set([["person.name", 0]]);
|
|
2535
|
+
expect(state.controllers.data.get("person.name")).toBe(0);
|
|
2536
|
+
expect(
|
|
2537
|
+
state.controllers.view.currentView?.lastUpdate?.validation,
|
|
2538
|
+
).toBeUndefined();
|
|
2539
|
+
|
|
2540
|
+
state.controllers.data.set([["person.name", "adam"]], { formatted: true });
|
|
2541
|
+
await vitest.waitFor(() => {
|
|
2542
|
+
expect(
|
|
2543
|
+
state.controllers.view.currentView?.lastUpdate?.validation.message,
|
|
2544
|
+
).toBe("Names just be in: frodo,sam");
|
|
2545
|
+
});
|
|
2546
|
+
|
|
2547
|
+
state.controllers.data.set([["person.name", "sam"]], { formatted: true });
|
|
2548
|
+
await vitest.waitFor(() => {
|
|
2549
|
+
expect(
|
|
2550
|
+
state.controllers.view.currentView?.lastUpdate?.validation,
|
|
2551
|
+
).toBeUndefined();
|
|
2552
|
+
});
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
test("tracking a binding commits the default value", () => {
|
|
2556
|
+
const flow = makeFlow({
|
|
2557
|
+
asset: {
|
|
2558
|
+
id: "input-2",
|
|
2559
|
+
type: "input",
|
|
2560
|
+
binding: "person.name",
|
|
2561
|
+
label: {
|
|
2562
|
+
asset: {
|
|
2563
|
+
id: "input-2-label",
|
|
2564
|
+
type: "text",
|
|
2565
|
+
value: "{{other.name}}",
|
|
2566
|
+
},
|
|
2567
|
+
},
|
|
2568
|
+
},
|
|
2569
|
+
});
|
|
2570
|
+
|
|
2571
|
+
flow.schema = {
|
|
2572
|
+
ROOT: {
|
|
2573
|
+
person: {
|
|
2574
|
+
type: "PersonType",
|
|
2575
|
+
},
|
|
2576
|
+
other: {
|
|
2577
|
+
type: "PersonType",
|
|
2578
|
+
},
|
|
2579
|
+
},
|
|
2580
|
+
PersonType: {
|
|
2581
|
+
name: {
|
|
2582
|
+
type: "StringType",
|
|
2583
|
+
default: "Adam",
|
|
2584
|
+
},
|
|
2585
|
+
},
|
|
2586
|
+
};
|
|
2587
|
+
|
|
2588
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
2589
|
+
|
|
2590
|
+
player.start(flow);
|
|
2591
|
+
const state = player.getState() as InProgressState;
|
|
2592
|
+
expect(state.controllers.data.get("person.name")).toBe("Adam");
|
|
2593
|
+
expect(state.controllers.data.get("other.name")).toBe("Adam");
|
|
2594
|
+
expect(
|
|
2595
|
+
state.controllers.view.currentView?.lastUpdate?.label.asset.value,
|
|
2596
|
+
).toBe("Adam");
|
|
2597
|
+
expect(state.controllers.data.get("")).toStrictEqual({
|
|
2598
|
+
person: { name: "Adam" },
|
|
2599
|
+
});
|
|
2600
|
+
});
|
|
2601
|
+
|
|
2602
|
+
test("does not validate on expressions outside of view", async () => {
|
|
2603
|
+
const flowWithExp: Flow = {
|
|
2604
|
+
id: "flow-with-exp",
|
|
2605
|
+
views: [
|
|
2606
|
+
{
|
|
2607
|
+
id: "view-1",
|
|
2608
|
+
type: "view",
|
|
2609
|
+
fields: {
|
|
2610
|
+
asset: {
|
|
2611
|
+
id: "input",
|
|
2612
|
+
type: "input",
|
|
2613
|
+
binding: "person.name",
|
|
2614
|
+
},
|
|
2615
|
+
},
|
|
2616
|
+
},
|
|
2617
|
+
],
|
|
2618
|
+
data: { person: { name: "frodo" } },
|
|
2619
|
+
schema: {
|
|
2620
|
+
ROOT: {
|
|
2621
|
+
person: {
|
|
2622
|
+
type: "PersonType",
|
|
2623
|
+
},
|
|
2624
|
+
},
|
|
2625
|
+
PersonType: {
|
|
2626
|
+
name: {
|
|
2627
|
+
type: "String",
|
|
2628
|
+
validation: [
|
|
2629
|
+
{
|
|
2630
|
+
type: "names",
|
|
2631
|
+
dataTarget: "formatted",
|
|
2632
|
+
names: ["frodo", "sam"],
|
|
2633
|
+
},
|
|
2634
|
+
],
|
|
2635
|
+
},
|
|
2636
|
+
},
|
|
2637
|
+
},
|
|
2638
|
+
navigation: {
|
|
2639
|
+
BEGIN: "FLOW_1",
|
|
2640
|
+
FLOW_1: {
|
|
2641
|
+
startState: "VIEW_1",
|
|
2642
|
+
VIEW_1: {
|
|
2643
|
+
state_type: "VIEW",
|
|
2644
|
+
ref: "view-1",
|
|
2645
|
+
transitions: {
|
|
2646
|
+
"*": "ACTION_1",
|
|
2647
|
+
},
|
|
2648
|
+
},
|
|
2649
|
+
ACTION_1: {
|
|
2650
|
+
state_type: "ACTION",
|
|
2651
|
+
exp: '{{person.name}} = "invalid"',
|
|
2652
|
+
transitions: {
|
|
2653
|
+
"*": "END_1",
|
|
2654
|
+
},
|
|
2655
|
+
},
|
|
2656
|
+
END_1: {
|
|
2657
|
+
state_type: "END",
|
|
2658
|
+
outcome: "done",
|
|
2659
|
+
},
|
|
2660
|
+
},
|
|
2661
|
+
},
|
|
2662
|
+
};
|
|
2663
|
+
|
|
2664
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
2665
|
+
const outcome = player.start(flowWithExp);
|
|
2666
|
+
|
|
2667
|
+
const state = player.getState() as InProgressState;
|
|
2668
|
+
state.controllers.flow.transition("Next");
|
|
2669
|
+
|
|
2670
|
+
const response = await outcome;
|
|
2671
|
+
expect(response.data).toStrictEqual({ person: { name: "invalid" } });
|
|
2672
|
+
});
|
|
2673
|
+
|
|
2674
|
+
describe("Validation applicability", () => {
|
|
2675
|
+
let player: Player;
|
|
2676
|
+
|
|
2677
|
+
beforeEach(() => {
|
|
2678
|
+
player = new Player({
|
|
2679
|
+
plugins: [
|
|
2680
|
+
new TrackBindingPlugin(),
|
|
2681
|
+
new RequiredIfValidationProviderPlugin(),
|
|
2682
|
+
],
|
|
2683
|
+
});
|
|
2684
|
+
|
|
2685
|
+
player.start(flowWithApplicability);
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2688
|
+
describe("weak validation", () => {
|
|
2689
|
+
it("weak binding updates should be allowed despite strong validation errors", async () => {
|
|
2690
|
+
const state = player.getState() as InProgressState;
|
|
2691
|
+
|
|
2692
|
+
state.controllers.data.set([["independentBinding", true]]);
|
|
2693
|
+
await vitest.waitFor(() => {
|
|
2694
|
+
expect(state.controllers.data.get("independentBinding")).toStrictEqual(
|
|
2695
|
+
true,
|
|
2696
|
+
);
|
|
2697
|
+
expect(
|
|
2698
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset
|
|
2699
|
+
.validation,
|
|
2700
|
+
).toMatchObject({
|
|
2701
|
+
severity: "error",
|
|
2702
|
+
message: `required based on independent value`,
|
|
2703
|
+
});
|
|
2704
|
+
});
|
|
2705
|
+
|
|
2706
|
+
state.controllers.data.set([["dependentBinding", "foo"]]);
|
|
2707
|
+
await vitest.waitFor(() => {
|
|
2708
|
+
expect(state.controllers.data.get("dependentBinding")).toStrictEqual(
|
|
2709
|
+
"foo",
|
|
2710
|
+
);
|
|
2711
|
+
expect(
|
|
2712
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset
|
|
2713
|
+
.validation,
|
|
2714
|
+
).toBeUndefined();
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2717
|
+
state.controllers.data.set([["dependentBinding", undefined]]);
|
|
2718
|
+
await vitest.waitFor(() => {
|
|
2719
|
+
expect(
|
|
2720
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset
|
|
2721
|
+
.validation,
|
|
2722
|
+
).toMatchObject({
|
|
2723
|
+
severity: "error",
|
|
2724
|
+
message: `required based on independent value`,
|
|
2725
|
+
});
|
|
2726
|
+
});
|
|
2727
|
+
|
|
2728
|
+
state.controllers.data.set([["independentBinding", false]]);
|
|
2729
|
+
await vitest.waitFor(() => {
|
|
2730
|
+
expect(state.controllers.data.get("independentBinding")).toStrictEqual(
|
|
2731
|
+
false,
|
|
2732
|
+
);
|
|
2733
|
+
expect(
|
|
2734
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset
|
|
2735
|
+
.validation,
|
|
2736
|
+
).toMatchObject({
|
|
2737
|
+
severity: "error",
|
|
2738
|
+
message: `required based on independent value`,
|
|
2739
|
+
});
|
|
2740
|
+
});
|
|
2741
|
+
});
|
|
2742
|
+
});
|
|
2743
|
+
});
|
|
2744
|
+
|
|
2745
|
+
test("updating a binding only updates its data and not other bindings due to weak binding connections", async () => {
|
|
2746
|
+
const flow = makeFlow({
|
|
2747
|
+
id: "view-1",
|
|
2748
|
+
type: "view",
|
|
2749
|
+
thing1: {
|
|
2750
|
+
asset: {
|
|
2751
|
+
id: "thing-1",
|
|
2752
|
+
binding: "input.text",
|
|
2753
|
+
},
|
|
2754
|
+
},
|
|
2755
|
+
thing2: {
|
|
2756
|
+
asset: {
|
|
2757
|
+
id: "thing-2",
|
|
2758
|
+
binding: "input.check",
|
|
2759
|
+
},
|
|
2760
|
+
},
|
|
2761
|
+
validation: [
|
|
2762
|
+
{
|
|
2763
|
+
type: "requiredIf",
|
|
2764
|
+
ref: "input.text",
|
|
2765
|
+
param: "input.check",
|
|
2766
|
+
},
|
|
2767
|
+
],
|
|
2768
|
+
});
|
|
2769
|
+
|
|
2770
|
+
flow.data = {
|
|
2771
|
+
someOtherParam: "notFoo",
|
|
2772
|
+
};
|
|
2773
|
+
|
|
2774
|
+
flow.schema = {
|
|
2775
|
+
ROOT: {
|
|
2776
|
+
input: {
|
|
2777
|
+
type: "InputType",
|
|
2778
|
+
},
|
|
2779
|
+
},
|
|
2780
|
+
InputType: {
|
|
2781
|
+
text: {
|
|
2782
|
+
type: "DateType",
|
|
2783
|
+
validation: [
|
|
2784
|
+
{
|
|
2785
|
+
type: "paramIsFoo",
|
|
2786
|
+
param: "someOtherParam",
|
|
2787
|
+
},
|
|
2788
|
+
],
|
|
2789
|
+
},
|
|
2790
|
+
check: {
|
|
2791
|
+
type: "BooleanType",
|
|
2792
|
+
validation: [
|
|
2793
|
+
{
|
|
2794
|
+
type: "required",
|
|
2795
|
+
},
|
|
2796
|
+
],
|
|
2797
|
+
},
|
|
2798
|
+
},
|
|
2799
|
+
};
|
|
2800
|
+
|
|
2801
|
+
const basicValidationPlugin = {
|
|
2802
|
+
name: "basic-validation",
|
|
2803
|
+
apply: (player: Player) => {
|
|
2804
|
+
player.hooks.schema.tap("basic-validation", (schema) => {
|
|
2805
|
+
schema.addDataTypes([
|
|
2806
|
+
{
|
|
2807
|
+
type: "DateType",
|
|
2808
|
+
validation: [{ type: "date" }],
|
|
2809
|
+
},
|
|
2810
|
+
{
|
|
2811
|
+
type: "BooleanType",
|
|
2812
|
+
validation: [{ type: "boolean" }],
|
|
2813
|
+
},
|
|
2814
|
+
]);
|
|
2815
|
+
});
|
|
2816
|
+
|
|
2817
|
+
player.hooks.validationController.tap("basic-validation", (vc) => {
|
|
2818
|
+
vc.hooks.createValidatorRegistry.tap("basic-validation", (registry) => {
|
|
2819
|
+
registry.register("date", (ctx, value) => {
|
|
2820
|
+
if (value === undefined) {
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
return value.match(/^\d{4}-\d{2}-\d{2}$/)
|
|
2825
|
+
? undefined
|
|
2826
|
+
: { message: "Not a date" };
|
|
2827
|
+
});
|
|
2828
|
+
registry.register("boolean", (ctx, value) => {
|
|
2829
|
+
if (value === undefined || value === true || value === false) {
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
return {
|
|
2834
|
+
message: "Not a boolean",
|
|
2835
|
+
};
|
|
2836
|
+
});
|
|
2837
|
+
|
|
2838
|
+
registry.register("required", (ctx, value) => {
|
|
2839
|
+
if (value === undefined) {
|
|
2840
|
+
return {
|
|
2841
|
+
message: "Required",
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
});
|
|
2845
|
+
|
|
2846
|
+
registry.register<any>("requiredIf", (ctx, value, { param }) => {
|
|
2847
|
+
const paramValue = ctx.model.get(param);
|
|
2848
|
+
if (paramValue === undefined) {
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
if (value === undefined) {
|
|
2853
|
+
return {
|
|
2854
|
+
message: "Required",
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2858
|
+
|
|
2859
|
+
registry.register<any>("paramIsFoo", (ctx, value, { param }) => {
|
|
2860
|
+
const paramValue = ctx.model.get(param);
|
|
2861
|
+
if (paramValue === "foo") {
|
|
2862
|
+
return;
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
if (value === undefined) {
|
|
2866
|
+
return {
|
|
2867
|
+
message: "Must be foo",
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
});
|
|
2871
|
+
});
|
|
2872
|
+
});
|
|
2873
|
+
},
|
|
2874
|
+
};
|
|
2875
|
+
|
|
2876
|
+
const player = new Player({
|
|
2877
|
+
plugins: [new TrackBindingPlugin(), basicValidationPlugin],
|
|
2878
|
+
});
|
|
2879
|
+
player.start(flow);
|
|
2880
|
+
const state = player.getState() as InProgressState;
|
|
2881
|
+
|
|
2882
|
+
state.controllers.flow.transition("next");
|
|
2883
|
+
await vitest.waitFor(() => {
|
|
2884
|
+
state.controllers.data.set([["input.text", ""]]);
|
|
2885
|
+
});
|
|
2886
|
+
|
|
2887
|
+
await vitest.waitFor(() => {
|
|
2888
|
+
state.controllers.data.set([["input.check", true]]);
|
|
2889
|
+
});
|
|
2890
|
+
|
|
2891
|
+
await vitest.waitFor(() => {
|
|
2892
|
+
const finalState = player.getState() as InProgressState;
|
|
2893
|
+
const otherParam = finalState.controllers.data.get("someOtherParam");
|
|
2894
|
+
expect(otherParam).toBe("notFoo");
|
|
2895
|
+
});
|
|
2896
|
+
});
|
|
2897
|
+
|
|
2898
|
+
describe("Validations with custom field messages", () => {
|
|
2899
|
+
it("can evaluate expressions in message", async () => {
|
|
2900
|
+
const flow = makeFlow({
|
|
2901
|
+
id: "view-1",
|
|
2902
|
+
type: "view",
|
|
2903
|
+
thing1: {
|
|
2904
|
+
asset: {
|
|
2905
|
+
id: "thing-1",
|
|
2906
|
+
binding: "foo.data.thing1",
|
|
2907
|
+
type: "input",
|
|
2908
|
+
},
|
|
2909
|
+
},
|
|
2910
|
+
validation: [
|
|
2911
|
+
{
|
|
2912
|
+
type: "expression",
|
|
2913
|
+
ref: "foo.data.thing1",
|
|
2914
|
+
message: "The entered value {{foo.data.thing1}} is greater than 100",
|
|
2915
|
+
exp: "{{foo.data.thing1}} < 100",
|
|
2916
|
+
},
|
|
2917
|
+
],
|
|
2918
|
+
});
|
|
2919
|
+
const player = new Player({
|
|
2920
|
+
plugins: [new TrackBindingPlugin()],
|
|
2921
|
+
});
|
|
2922
|
+
player.start(flow);
|
|
2923
|
+
const state = player.getState() as InProgressState;
|
|
2924
|
+
|
|
2925
|
+
state.controllers.data.set([["foo.data.thing1", 200]]);
|
|
2926
|
+
state.controllers.flow.transition("next");
|
|
2927
|
+
expect(
|
|
2928
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2929
|
+
).toMatchObject({
|
|
2930
|
+
severity: "error",
|
|
2931
|
+
message: "The entered value 200 is greater than 100",
|
|
2932
|
+
displayTarget: "field",
|
|
2933
|
+
});
|
|
2934
|
+
});
|
|
2935
|
+
|
|
2936
|
+
it("can templatize messages", async () => {
|
|
2937
|
+
const errFlow = makeFlow({
|
|
2938
|
+
id: "view-1",
|
|
2939
|
+
type: "view",
|
|
2940
|
+
thing1: {
|
|
2941
|
+
asset: {
|
|
2942
|
+
id: "thing-1",
|
|
2943
|
+
binding: "foo.data.thing1",
|
|
2944
|
+
type: "integer",
|
|
2945
|
+
},
|
|
2946
|
+
},
|
|
2947
|
+
validation: [
|
|
2948
|
+
{
|
|
2949
|
+
type: "integer",
|
|
2950
|
+
ref: "foo.data.thing1",
|
|
2951
|
+
message:
|
|
2952
|
+
"foo.data.thing1 is a number. You have provided a value of %type, which is correct. But floored value, %flooredValue is not equal to entered value, %value",
|
|
2953
|
+
trigger: "load",
|
|
2954
|
+
severity: "error",
|
|
2955
|
+
},
|
|
2956
|
+
],
|
|
2957
|
+
});
|
|
2958
|
+
|
|
2959
|
+
const player = new Player({ plugins: [new TrackBindingPlugin()] });
|
|
2960
|
+
player.start(errFlow);
|
|
2961
|
+
const state = player.getState() as InProgressState;
|
|
2962
|
+
|
|
2963
|
+
state.controllers.data.set([["foo.data.thing1", 200.567]]);
|
|
2964
|
+
|
|
2965
|
+
await vitest.waitFor(() => {
|
|
2966
|
+
expect(
|
|
2967
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
2968
|
+
).toMatchObject({
|
|
2969
|
+
message:
|
|
2970
|
+
"foo.data.thing1 is a number. You have provided a value of number, which is correct. But floored value, 200 is not equal to entered value, 200.567",
|
|
2971
|
+
severity: "error",
|
|
2972
|
+
displayTarget: "field",
|
|
2973
|
+
});
|
|
2974
|
+
});
|
|
2975
|
+
});
|
|
2976
|
+
});
|
|
2977
|
+
|
|
2978
|
+
describe("Validations with multiple inputs", () => {
|
|
2979
|
+
const complexValidation = makeFlow({
|
|
2980
|
+
id: "view-1",
|
|
2981
|
+
type: "view",
|
|
2982
|
+
thing1: {
|
|
2983
|
+
asset: {
|
|
2984
|
+
id: "thing-1",
|
|
2985
|
+
binding: "foo.a",
|
|
2986
|
+
type: "input",
|
|
2987
|
+
},
|
|
2988
|
+
},
|
|
2989
|
+
thing2: {
|
|
2990
|
+
asset: {
|
|
2991
|
+
id: "thing-2",
|
|
2992
|
+
binding: "foo.b",
|
|
2993
|
+
type: "input",
|
|
2994
|
+
},
|
|
2995
|
+
},
|
|
2996
|
+
validation: [
|
|
2997
|
+
{
|
|
2998
|
+
type: "expression",
|
|
2999
|
+
ref: "foo.a",
|
|
3000
|
+
message: "Both need to equal 100",
|
|
3001
|
+
exp: 'sumValues(["foo.a", "foo.b"]) == 100',
|
|
3002
|
+
severity: "error",
|
|
3003
|
+
trigger: "load",
|
|
3004
|
+
},
|
|
3005
|
+
],
|
|
3006
|
+
});
|
|
3007
|
+
|
|
3008
|
+
let player: Player;
|
|
3009
|
+
let validationController: ValidationController;
|
|
3010
|
+
let schema: SchemaController;
|
|
3011
|
+
let parser: BindingParser;
|
|
3012
|
+
|
|
3013
|
+
beforeEach(() => {
|
|
3014
|
+
player = new Player({
|
|
3015
|
+
plugins: [new TrackBindingPlugin(), new TestExpressionPlugin()],
|
|
3016
|
+
});
|
|
3017
|
+
player.hooks.validationController.tap("test", (vc) => {
|
|
3018
|
+
validationController = vc;
|
|
3019
|
+
});
|
|
3020
|
+
player.hooks.schema.tap("test", (s) => {
|
|
3021
|
+
schema = s;
|
|
3022
|
+
});
|
|
3023
|
+
player.hooks.bindingParser.tap("test", (p) => {
|
|
3024
|
+
parser = p;
|
|
3025
|
+
});
|
|
3026
|
+
|
|
3027
|
+
player.start(flowWithThings);
|
|
3028
|
+
});
|
|
3029
|
+
|
|
3030
|
+
it("Throws errors when a weak referenced field is changed", async () => {
|
|
3031
|
+
complexValidation.data = {
|
|
3032
|
+
foo: {
|
|
3033
|
+
a: 90,
|
|
3034
|
+
b: 10,
|
|
3035
|
+
},
|
|
3036
|
+
};
|
|
3037
|
+
|
|
3038
|
+
player.start(complexValidation);
|
|
3039
|
+
const state = player.getState() as InProgressState;
|
|
3040
|
+
|
|
3041
|
+
expect(
|
|
3042
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
3043
|
+
).toBeUndefined();
|
|
3044
|
+
|
|
3045
|
+
state.controllers.data.set([["foo.b", 70]]);
|
|
3046
|
+
await vitest.waitFor(() => {
|
|
3047
|
+
expect(
|
|
3048
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
3049
|
+
).toMatchObject({
|
|
3050
|
+
severity: "error",
|
|
3051
|
+
message: "Both need to equal 100",
|
|
3052
|
+
});
|
|
3053
|
+
|
|
3054
|
+
expect(state.controllers.data.get("")).toMatchObject({
|
|
3055
|
+
foo: {
|
|
3056
|
+
a: 90,
|
|
3057
|
+
b: 70,
|
|
3058
|
+
},
|
|
3059
|
+
});
|
|
3060
|
+
});
|
|
3061
|
+
});
|
|
3062
|
+
|
|
3063
|
+
it("Clears errors when a weak referenced field is changed", async () => {
|
|
3064
|
+
complexValidation.data = {
|
|
3065
|
+
foo: {
|
|
3066
|
+
a: 90,
|
|
3067
|
+
b: 10,
|
|
3068
|
+
},
|
|
3069
|
+
};
|
|
3070
|
+
|
|
3071
|
+
player.start(complexValidation);
|
|
3072
|
+
const state = player.getState() as InProgressState;
|
|
3073
|
+
|
|
3074
|
+
expect(
|
|
3075
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
3076
|
+
).toBeUndefined();
|
|
3077
|
+
|
|
3078
|
+
state.controllers.data.set([["foo.a", 15]]);
|
|
3079
|
+
await vitest.waitFor(() => {
|
|
3080
|
+
expect(
|
|
3081
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
3082
|
+
).toMatchObject({
|
|
3083
|
+
severity: "error",
|
|
3084
|
+
message: "Both need to equal 100",
|
|
3085
|
+
});
|
|
3086
|
+
|
|
3087
|
+
expect(
|
|
3088
|
+
state.controllers.data.get("", { includeInvalid: false }),
|
|
3089
|
+
).toMatchObject({
|
|
3090
|
+
foo: {
|
|
3091
|
+
a: 90,
|
|
3092
|
+
b: 10,
|
|
3093
|
+
},
|
|
3094
|
+
});
|
|
3095
|
+
|
|
3096
|
+
expect(
|
|
3097
|
+
state.controllers.data.get("", { includeInvalid: true }),
|
|
3098
|
+
).toMatchObject({
|
|
3099
|
+
foo: {
|
|
3100
|
+
a: 15,
|
|
3101
|
+
b: 10,
|
|
3102
|
+
},
|
|
3103
|
+
});
|
|
3104
|
+
});
|
|
3105
|
+
|
|
3106
|
+
state.controllers.data.set([["foo.b", 85]]);
|
|
3107
|
+
await vitest.waitFor(() => {
|
|
3108
|
+
expect(
|
|
3109
|
+
state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
|
|
3110
|
+
).toBeUndefined();
|
|
3111
|
+
|
|
3112
|
+
expect(
|
|
3113
|
+
state.controllers.data.get("", { includeInvalid: false }),
|
|
3114
|
+
).toMatchObject({
|
|
3115
|
+
foo: {
|
|
3116
|
+
a: 15,
|
|
3117
|
+
b: 85,
|
|
3118
|
+
},
|
|
3119
|
+
});
|
|
3120
|
+
|
|
3121
|
+
expect(
|
|
3122
|
+
state.controllers.data.get("", { includeInvalid: true }),
|
|
3123
|
+
).toMatchObject({
|
|
3124
|
+
foo: {
|
|
3125
|
+
a: 15,
|
|
3126
|
+
b: 85,
|
|
3127
|
+
},
|
|
3128
|
+
});
|
|
3129
|
+
});
|
|
3130
|
+
});
|
|
3131
|
+
});
|
|
3132
|
+
|
|
3133
|
+
describe("weak binding edge cases", () => {
|
|
3134
|
+
test("requiredIf", async () => {
|
|
3135
|
+
const flow = makeFlow({
|
|
3136
|
+
id: "view-1",
|
|
3137
|
+
type: "view",
|
|
3138
|
+
thing1: {
|
|
3139
|
+
asset: {
|
|
3140
|
+
id: "thing-1",
|
|
3141
|
+
binding: "input.text",
|
|
3142
|
+
},
|
|
3143
|
+
},
|
|
3144
|
+
thing2: {
|
|
3145
|
+
asset: {
|
|
3146
|
+
id: "thing-2",
|
|
3147
|
+
binding: "input.check",
|
|
3148
|
+
},
|
|
3149
|
+
},
|
|
3150
|
+
validation: [
|
|
3151
|
+
{
|
|
3152
|
+
type: "requiredIf",
|
|
3153
|
+
ref: "input.text",
|
|
3154
|
+
param: "input.check",
|
|
3155
|
+
},
|
|
3156
|
+
],
|
|
3157
|
+
});
|
|
3158
|
+
|
|
3159
|
+
flow.schema = {
|
|
3160
|
+
ROOT: {
|
|
3161
|
+
input: {
|
|
3162
|
+
type: "InputType",
|
|
3163
|
+
},
|
|
3164
|
+
},
|
|
3165
|
+
InputType: {
|
|
3166
|
+
text: {
|
|
3167
|
+
type: "DateType",
|
|
3168
|
+
},
|
|
3169
|
+
check: {
|
|
3170
|
+
type: "BooleanType",
|
|
3171
|
+
validation: [
|
|
3172
|
+
{
|
|
3173
|
+
type: "required",
|
|
3174
|
+
},
|
|
3175
|
+
],
|
|
3176
|
+
},
|
|
3177
|
+
},
|
|
3178
|
+
};
|
|
3179
|
+
|
|
3180
|
+
const basicValidationPlugin = {
|
|
3181
|
+
name: "basic-validation",
|
|
3182
|
+
apply: (player: Player) => {
|
|
3183
|
+
player.hooks.schema.tap("basic-validation", (schema) => {
|
|
3184
|
+
schema.addDataTypes([
|
|
3185
|
+
{
|
|
3186
|
+
type: "DateType",
|
|
3187
|
+
validation: [{ type: "date" }],
|
|
3188
|
+
},
|
|
3189
|
+
{
|
|
3190
|
+
type: "BooleanType",
|
|
3191
|
+
validation: [{ type: "boolean" }],
|
|
3192
|
+
},
|
|
3193
|
+
]);
|
|
3194
|
+
});
|
|
3195
|
+
|
|
3196
|
+
player.hooks.validationController.tap("basic-validation", (vc) => {
|
|
3197
|
+
vc.hooks.createValidatorRegistry.tap(
|
|
3198
|
+
"basic-validation",
|
|
3199
|
+
(registry) => {
|
|
3200
|
+
registry.register("date", (ctx, value) => {
|
|
3201
|
+
if (value === undefined) {
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
return value.match(/^\d{4}-\d{2}-\d{2}$/)
|
|
3206
|
+
? undefined
|
|
3207
|
+
: { message: "Not a date" };
|
|
3208
|
+
});
|
|
3209
|
+
registry.register("boolean", (ctx, value) => {
|
|
3210
|
+
if (value === undefined || value === true || value === false) {
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
return {
|
|
3215
|
+
message: "Not a boolean",
|
|
3216
|
+
};
|
|
3217
|
+
});
|
|
3218
|
+
|
|
3219
|
+
registry.register("required", (ctx, value) => {
|
|
3220
|
+
if (value === undefined) {
|
|
3221
|
+
return {
|
|
3222
|
+
message: "Required",
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
});
|
|
3226
|
+
|
|
3227
|
+
registry.register<any>("requiredIf", (ctx, value, { param }) => {
|
|
3228
|
+
const paramValue = ctx.model.get(param);
|
|
3229
|
+
if (paramValue === undefined) {
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
if (value === undefined) {
|
|
3234
|
+
return {
|
|
3235
|
+
message: "Required",
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
});
|
|
3239
|
+
},
|
|
3240
|
+
);
|
|
3241
|
+
});
|
|
3242
|
+
},
|
|
3243
|
+
};
|
|
3244
|
+
|
|
3245
|
+
const player = new Player({
|
|
3246
|
+
plugins: [new TrackBindingPlugin(), basicValidationPlugin],
|
|
3247
|
+
});
|
|
3248
|
+
player.start(flow);
|
|
3249
|
+
const state = player.getState() as InProgressState;
|
|
3250
|
+
|
|
3251
|
+
state.controllers.flow.transition("next");
|
|
3252
|
+
await vitest.waitFor(() => {
|
|
3253
|
+
state.controllers.data.set([["input.text", "1999-12-31"]]);
|
|
3254
|
+
});
|
|
3255
|
+
await vitest.waitFor(() => {
|
|
3256
|
+
state.controllers.data.set([["input.check", true]]);
|
|
3257
|
+
});
|
|
3258
|
+
await vitest.waitFor(() => {
|
|
3259
|
+
state.controllers.flow.transition("next");
|
|
3260
|
+
});
|
|
3261
|
+
await vitest.waitFor(() => {
|
|
3262
|
+
expect(player.getState().status).toBe("completed");
|
|
3263
|
+
});
|
|
3264
|
+
});
|
|
3265
|
+
});
|
|
3266
|
+
|
|
3267
|
+
describe("Validation Providers", () => {
|
|
3268
|
+
it("uses a locally defined handler", async () => {
|
|
3269
|
+
let shouldError = true;
|
|
3270
|
+
|
|
3271
|
+
const player = new Player({
|
|
3272
|
+
plugins: [
|
|
3273
|
+
new TrackBindingPlugin(),
|
|
3274
|
+
|
|
3275
|
+
{
|
|
3276
|
+
name: "basic-validation",
|
|
3277
|
+
apply: (p: Player) => {
|
|
3278
|
+
p.hooks.validationController.tap("basic-validation", (vc) => {
|
|
3279
|
+
vc.hooks.resolveValidationProviders.tap(
|
|
3280
|
+
"basic-validation",
|
|
3281
|
+
(providers) => {
|
|
3282
|
+
return [
|
|
3283
|
+
...providers,
|
|
3284
|
+
{
|
|
3285
|
+
source: "local-test",
|
|
3286
|
+
provider: {
|
|
3287
|
+
getValidationsForBinding(binding) {
|
|
3288
|
+
if (binding.asString() === "data.thing1") {
|
|
3289
|
+
return [
|
|
3290
|
+
{
|
|
3291
|
+
type: "custom",
|
|
3292
|
+
trigger: "load",
|
|
3293
|
+
severity: "error",
|
|
3294
|
+
handler: (ctx, value) => {
|
|
3295
|
+
if (shouldError) {
|
|
3296
|
+
return {
|
|
3297
|
+
message: "Local Error",
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
},
|
|
3301
|
+
},
|
|
3302
|
+
];
|
|
3303
|
+
}
|
|
3304
|
+
},
|
|
3305
|
+
},
|
|
3306
|
+
},
|
|
3307
|
+
];
|
|
3308
|
+
},
|
|
3309
|
+
);
|
|
3310
|
+
});
|
|
3311
|
+
},
|
|
3312
|
+
},
|
|
3313
|
+
],
|
|
3314
|
+
});
|
|
3315
|
+
|
|
3316
|
+
player.start(simpleFlow);
|
|
3317
|
+
|
|
3318
|
+
/**
|
|
3319
|
+
*
|
|
3320
|
+
*/
|
|
3321
|
+
const getControllers = () => {
|
|
3322
|
+
const state = player.getState() as InProgressState;
|
|
3323
|
+
return state.controllers;
|
|
3324
|
+
};
|
|
3325
|
+
|
|
3326
|
+
/**
|
|
3327
|
+
*
|
|
3328
|
+
*/
|
|
3329
|
+
const getFirstInput = () => {
|
|
3330
|
+
return getControllers().view.currentView?.lastUpdate?.thing1.asset;
|
|
3331
|
+
};
|
|
3332
|
+
|
|
3333
|
+
expect(getFirstInput()?.validation?.message).toBe("Local Error");
|
|
3334
|
+
getControllers().data.set([["data.thing1", "foo"]]);
|
|
3335
|
+
expect(getFirstInput()?.validation?.message).toBe("Local Error");
|
|
3336
|
+
|
|
3337
|
+
shouldError = false;
|
|
3338
|
+
|
|
3339
|
+
getControllers().data.set([["data.thing1", "sam"]]);
|
|
3340
|
+
|
|
3341
|
+
await vitest.waitFor(() => {
|
|
3342
|
+
expect(getFirstInput()?.validation?.message).toBe(undefined);
|
|
3343
|
+
});
|
|
3344
|
+
});
|
|
3345
|
+
});
|
|
3346
|
+
|
|
3347
|
+
describe("Validation + Default Data", () => {
|
|
3348
|
+
it("triggers validation default data is invalid", async () => {
|
|
3349
|
+
const flow = makeFlow({
|
|
3350
|
+
id: "view-1",
|
|
3351
|
+
type: "view",
|
|
3352
|
+
requiredField: {
|
|
3353
|
+
asset: {
|
|
3354
|
+
id: "required-field",
|
|
3355
|
+
type: "input",
|
|
3356
|
+
binding: "input.text",
|
|
3357
|
+
},
|
|
3358
|
+
},
|
|
3359
|
+
thing2: {
|
|
3360
|
+
asset: {
|
|
3361
|
+
id: "thing-2",
|
|
3362
|
+
binding: "input.check",
|
|
3363
|
+
},
|
|
3364
|
+
},
|
|
3365
|
+
validation: [
|
|
3366
|
+
{
|
|
3367
|
+
type: "requiredIf",
|
|
3368
|
+
ref: "input.text",
|
|
3369
|
+
param: "input.check",
|
|
3370
|
+
},
|
|
3371
|
+
],
|
|
3372
|
+
});
|
|
3373
|
+
|
|
3374
|
+
flow.schema = {
|
|
3375
|
+
ROOT: {
|
|
3376
|
+
input: {
|
|
3377
|
+
type: "InputType",
|
|
3378
|
+
},
|
|
3379
|
+
},
|
|
3380
|
+
InputType: {
|
|
3381
|
+
text: {
|
|
3382
|
+
type: "StringType",
|
|
3383
|
+
// The default value is an empty string, which is invalid b/c of the required check
|
|
3384
|
+
default: "",
|
|
3385
|
+
validation: [
|
|
3386
|
+
{
|
|
3387
|
+
type: "required",
|
|
3388
|
+
},
|
|
3389
|
+
],
|
|
3390
|
+
},
|
|
3391
|
+
},
|
|
3392
|
+
};
|
|
3393
|
+
|
|
3394
|
+
const player = new Player({
|
|
3395
|
+
plugins: [new TrackBindingPlugin()],
|
|
3396
|
+
});
|
|
3397
|
+
|
|
3398
|
+
player.start(flow);
|
|
3399
|
+
|
|
3400
|
+
/**
|
|
3401
|
+
*
|
|
3402
|
+
*/
|
|
3403
|
+
const getControllers = () => {
|
|
3404
|
+
const state = player.getState() as InProgressState;
|
|
3405
|
+
return state.controllers;
|
|
3406
|
+
};
|
|
3407
|
+
|
|
3408
|
+
/**
|
|
3409
|
+
*
|
|
3410
|
+
*/
|
|
3411
|
+
const getFirstInput = () => {
|
|
3412
|
+
return getControllers().view.currentView?.lastUpdate?.requiredField.asset;
|
|
3413
|
+
};
|
|
3414
|
+
|
|
3415
|
+
await vitest.waitFor(() => {
|
|
3416
|
+
expect(getFirstInput()?.validation).toBeUndefined();
|
|
3417
|
+
});
|
|
3418
|
+
|
|
3419
|
+
// Set the value to the same as the default
|
|
3420
|
+
getControllers().data.set([["input.text", ""]]);
|
|
3421
|
+
|
|
3422
|
+
await vitest.waitFor(() => {
|
|
3423
|
+
expect(getFirstInput()?.validation.message).toBe("A value is required");
|
|
3424
|
+
});
|
|
3425
|
+
|
|
3426
|
+
// Set the value to something else
|
|
3427
|
+
getControllers().data.set([["input.text", "foo"]]);
|
|
3428
|
+
await vitest.waitFor(() => {
|
|
3429
|
+
expect(getFirstInput()?.validation).toBeUndefined();
|
|
3430
|
+
});
|
|
3431
|
+
});
|
|
3432
|
+
});
|
|
3433
|
+
|
|
3434
|
+
describe("Validation in subflow", () => {
|
|
3435
|
+
it("validations are evaluated when in a subflow", async () => {
|
|
3436
|
+
const flow = {
|
|
3437
|
+
id: "input-validation-flow",
|
|
3438
|
+
views: [
|
|
3439
|
+
{
|
|
3440
|
+
id: "view-1",
|
|
3441
|
+
type: "input",
|
|
3442
|
+
binding: "foo.requiredInput",
|
|
3443
|
+
label: {
|
|
3444
|
+
asset: {
|
|
3445
|
+
id: "input-required-label",
|
|
3446
|
+
type: "text",
|
|
3447
|
+
value: "This input is required",
|
|
3448
|
+
},
|
|
3449
|
+
},
|
|
3450
|
+
},
|
|
3451
|
+
],
|
|
3452
|
+
schema: {
|
|
3453
|
+
ROOT: {
|
|
3454
|
+
foo: {
|
|
3455
|
+
type: "FooType",
|
|
3456
|
+
},
|
|
3457
|
+
},
|
|
3458
|
+
FooType: {
|
|
3459
|
+
requiredInput: {
|
|
3460
|
+
type: "StringType",
|
|
3461
|
+
validation: [
|
|
3462
|
+
{
|
|
3463
|
+
type: "required",
|
|
3464
|
+
},
|
|
3465
|
+
],
|
|
3466
|
+
},
|
|
3467
|
+
},
|
|
3468
|
+
},
|
|
3469
|
+
data: {},
|
|
3470
|
+
navigation: {
|
|
3471
|
+
BEGIN: "FLOW_1",
|
|
3472
|
+
FLOW_1: {
|
|
3473
|
+
startState: "SUBFLOW",
|
|
3474
|
+
SUBFLOW: {
|
|
3475
|
+
state_type: "FLOW",
|
|
3476
|
+
ref: "FLOW_2",
|
|
3477
|
+
transitions: {
|
|
3478
|
+
"*": "END_Done",
|
|
3479
|
+
},
|
|
3480
|
+
},
|
|
3481
|
+
END_Done: {
|
|
3482
|
+
state_type: "END",
|
|
3483
|
+
outcome: "done",
|
|
3484
|
+
},
|
|
3485
|
+
},
|
|
3486
|
+
FLOW_2: {
|
|
3487
|
+
startState: "VIEW_1",
|
|
3488
|
+
VIEW_1: {
|
|
3489
|
+
state_type: "VIEW",
|
|
3490
|
+
ref: "view-1",
|
|
3491
|
+
transitions: {
|
|
3492
|
+
"*": "END_Done",
|
|
3493
|
+
},
|
|
3494
|
+
},
|
|
3495
|
+
END_Done: {
|
|
3496
|
+
state_type: "END",
|
|
3497
|
+
outcome: "done",
|
|
3498
|
+
},
|
|
3499
|
+
},
|
|
3500
|
+
},
|
|
3501
|
+
} as Flow;
|
|
3502
|
+
|
|
3503
|
+
const player = new Player({
|
|
3504
|
+
plugins: [new TrackBindingPlugin()],
|
|
3505
|
+
});
|
|
3506
|
+
|
|
3507
|
+
player.start(flow);
|
|
3508
|
+
|
|
3509
|
+
/**
|
|
3510
|
+
*
|
|
3511
|
+
*/
|
|
3512
|
+
const getControllers = () => {
|
|
3513
|
+
const state = player.getState() as InProgressState;
|
|
3514
|
+
return state.controllers;
|
|
3515
|
+
};
|
|
3516
|
+
|
|
3517
|
+
/**
|
|
3518
|
+
*
|
|
3519
|
+
*/
|
|
3520
|
+
const getValidationMessage = () => {
|
|
3521
|
+
return getControllers().view.currentView?.lastUpdate?.validation;
|
|
3522
|
+
};
|
|
3523
|
+
|
|
3524
|
+
/**
|
|
3525
|
+
*
|
|
3526
|
+
*/
|
|
3527
|
+
const attemptTransition = () => {
|
|
3528
|
+
getControllers().flow.transition("next");
|
|
3529
|
+
};
|
|
3530
|
+
|
|
3531
|
+
await vitest.waitFor(() => {
|
|
3532
|
+
expect(getControllers().view.currentView?.lastUpdate?.id).toStrictEqual(
|
|
3533
|
+
"view-1",
|
|
3534
|
+
);
|
|
3535
|
+
});
|
|
3536
|
+
|
|
3537
|
+
attemptTransition();
|
|
3538
|
+
expect(getControllers().view.currentView?.lastUpdate?.id).toStrictEqual(
|
|
3539
|
+
"view-1",
|
|
3540
|
+
);
|
|
3541
|
+
const firstRequiredValidation = getValidationMessage();
|
|
3542
|
+
expect(firstRequiredValidation.message).toStrictEqual(
|
|
3543
|
+
"A value is required",
|
|
3544
|
+
);
|
|
3545
|
+
getControllers().data.set([["foo.requiredInput", 1]]);
|
|
3546
|
+
|
|
3547
|
+
await vitest.waitFor(() => {
|
|
3548
|
+
attemptTransition();
|
|
3549
|
+
});
|
|
3550
|
+
|
|
3551
|
+
await vitest.waitFor(() => {
|
|
3552
|
+
expect(player.getState().status).toStrictEqual("completed");
|
|
3553
|
+
});
|
|
3554
|
+
});
|
|
3555
|
+
});
|