@flusys/nestjs-form-builder 6.0.2 → 6.1.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/cjs/controllers/form-result.controller.spec.js +257 -0
- package/cjs/controllers/form.controller.spec.js +212 -0
- package/cjs/docs/form-builder-swagger.config.spec.js +75 -0
- package/cjs/entities/index.spec.js +18 -0
- package/cjs/modules/form-builder.module.spec.js +133 -0
- package/cjs/services/form-builder-config.service.spec.js +121 -0
- package/cjs/services/form-builder-datasource.provider.spec.js +147 -0
- package/cjs/services/form-result.service.spec.js +702 -0
- package/cjs/services/form.service.spec.js +746 -0
- package/cjs/utils/computed-field.utils.spec.js +1205 -0
- package/cjs/utils/permission.utils.spec.js +118 -0
- package/fesm/controllers/form-result.controller.spec.js +253 -0
- package/fesm/controllers/form.controller.spec.js +208 -0
- package/fesm/docs/form-builder-swagger.config.spec.js +71 -0
- package/fesm/entities/index.spec.js +14 -0
- package/fesm/modules/form-builder.module.spec.js +129 -0
- package/fesm/services/form-builder-config.service.spec.js +117 -0
- package/fesm/services/form-builder-datasource.provider.spec.js +143 -0
- package/fesm/services/form-result.service.spec.js +657 -0
- package/fesm/services/form.service.spec.js +701 -0
- package/fesm/utils/computed-field.utils.spec.js +1201 -0
- package/fesm/utils/permission.utils.spec.js +114 -0
- package/package.json +3 -3
|
@@ -0,0 +1,1205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _computedfieldutils = require("./computed-field.utils");
|
|
6
|
+
function directField(overrides = {}) {
|
|
7
|
+
return {
|
|
8
|
+
id: 'cf-1',
|
|
9
|
+
name: 'Computed',
|
|
10
|
+
key: 'total',
|
|
11
|
+
valueType: 'number',
|
|
12
|
+
rules: [
|
|
13
|
+
{
|
|
14
|
+
id: 'rule-1',
|
|
15
|
+
computation: {
|
|
16
|
+
type: 'direct',
|
|
17
|
+
config: {
|
|
18
|
+
type: 'direct',
|
|
19
|
+
value: 42
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
...overrides
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
describe('calculateComputedFields', ()=>{
|
|
28
|
+
it('returns an empty object when there are no computed fields', ()=>{
|
|
29
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, undefined)).toEqual({});
|
|
30
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [])).toEqual({});
|
|
31
|
+
});
|
|
32
|
+
it('keys the result by the field.key property, not field.id', ()=>{
|
|
33
|
+
const field = directField({
|
|
34
|
+
id: 'cf-id',
|
|
35
|
+
key: 'my_key'
|
|
36
|
+
});
|
|
37
|
+
const result = (0, _computedfieldutils.calculateComputedFields)({}, [
|
|
38
|
+
field
|
|
39
|
+
]);
|
|
40
|
+
expect(result).toEqual({
|
|
41
|
+
my_key: 42
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
it('evaluates multiple computed fields independently', ()=>{
|
|
45
|
+
const fields = [
|
|
46
|
+
directField({
|
|
47
|
+
key: 'a'
|
|
48
|
+
}),
|
|
49
|
+
directField({
|
|
50
|
+
key: 'b',
|
|
51
|
+
rules: [
|
|
52
|
+
{
|
|
53
|
+
id: 'r2',
|
|
54
|
+
computation: {
|
|
55
|
+
type: 'direct',
|
|
56
|
+
config: {
|
|
57
|
+
type: 'direct',
|
|
58
|
+
value: 'hello'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
valueType: 'string'
|
|
64
|
+
})
|
|
65
|
+
];
|
|
66
|
+
const result = (0, _computedfieldutils.calculateComputedFields)({}, fields);
|
|
67
|
+
expect(result).toEqual({
|
|
68
|
+
a: 42,
|
|
69
|
+
b: 'hello'
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
// ==========================================================================
|
|
73
|
+
// Rule matching / condition evaluation
|
|
74
|
+
// ==========================================================================
|
|
75
|
+
describe('condition evaluation', ()=>{
|
|
76
|
+
it('uses the first matching rule and ignores subsequent rules', ()=>{
|
|
77
|
+
const field = {
|
|
78
|
+
id: 'cf-1',
|
|
79
|
+
name: 'Total',
|
|
80
|
+
key: 'total',
|
|
81
|
+
valueType: 'string',
|
|
82
|
+
rules: [
|
|
83
|
+
{
|
|
84
|
+
id: 'rule-1',
|
|
85
|
+
condition: {
|
|
86
|
+
operator: 'AND',
|
|
87
|
+
conditions: [
|
|
88
|
+
{
|
|
89
|
+
fieldId: 'x',
|
|
90
|
+
comparison: 'equals',
|
|
91
|
+
value: 1
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
computation: {
|
|
96
|
+
type: 'direct',
|
|
97
|
+
config: {
|
|
98
|
+
type: 'direct',
|
|
99
|
+
value: 'first'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'rule-2',
|
|
105
|
+
computation: {
|
|
106
|
+
type: 'direct',
|
|
107
|
+
config: {
|
|
108
|
+
type: 'direct',
|
|
109
|
+
value: 'second'
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
};
|
|
115
|
+
const result = (0, _computedfieldutils.calculateComputedFields)({
|
|
116
|
+
x: 1
|
|
117
|
+
}, [
|
|
118
|
+
field
|
|
119
|
+
]);
|
|
120
|
+
expect(result.total).toBe('first');
|
|
121
|
+
});
|
|
122
|
+
it('falls through to the default value when no rule matches', ()=>{
|
|
123
|
+
const field = {
|
|
124
|
+
id: 'cf-1',
|
|
125
|
+
name: 'Total',
|
|
126
|
+
key: 'total',
|
|
127
|
+
valueType: 'string',
|
|
128
|
+
defaultValue: 'fallback',
|
|
129
|
+
rules: [
|
|
130
|
+
{
|
|
131
|
+
id: 'rule-1',
|
|
132
|
+
condition: {
|
|
133
|
+
operator: 'AND',
|
|
134
|
+
conditions: [
|
|
135
|
+
{
|
|
136
|
+
fieldId: 'x',
|
|
137
|
+
comparison: 'equals',
|
|
138
|
+
value: 1
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
computation: {
|
|
143
|
+
type: 'direct',
|
|
144
|
+
config: {
|
|
145
|
+
type: 'direct',
|
|
146
|
+
value: 'matched'
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
};
|
|
152
|
+
const result = (0, _computedfieldutils.calculateComputedFields)({
|
|
153
|
+
x: 2
|
|
154
|
+
}, [
|
|
155
|
+
field
|
|
156
|
+
]);
|
|
157
|
+
expect(result.total).toBe('fallback');
|
|
158
|
+
});
|
|
159
|
+
it('returns null when no rule matches and no default value is set', ()=>{
|
|
160
|
+
const field = {
|
|
161
|
+
id: 'cf-1',
|
|
162
|
+
name: 'Total',
|
|
163
|
+
key: 'total',
|
|
164
|
+
valueType: 'string',
|
|
165
|
+
rules: [
|
|
166
|
+
{
|
|
167
|
+
id: 'rule-1',
|
|
168
|
+
condition: {
|
|
169
|
+
operator: 'AND',
|
|
170
|
+
conditions: [
|
|
171
|
+
{
|
|
172
|
+
fieldId: 'x',
|
|
173
|
+
comparison: 'equals',
|
|
174
|
+
value: 1
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
computation: {
|
|
179
|
+
type: 'direct',
|
|
180
|
+
config: {
|
|
181
|
+
type: 'direct',
|
|
182
|
+
value: 'matched'
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
};
|
|
188
|
+
const result = (0, _computedfieldutils.calculateComputedFields)({
|
|
189
|
+
x: 2
|
|
190
|
+
}, [
|
|
191
|
+
field
|
|
192
|
+
]);
|
|
193
|
+
expect(result.total).toBeNull();
|
|
194
|
+
});
|
|
195
|
+
it('treats a missing/empty condition group as always-true', ()=>{
|
|
196
|
+
const field = directField({
|
|
197
|
+
key: 'total'
|
|
198
|
+
});
|
|
199
|
+
delete field.rules[0].condition;
|
|
200
|
+
const result = (0, _computedfieldutils.calculateComputedFields)({}, [
|
|
201
|
+
field
|
|
202
|
+
]);
|
|
203
|
+
expect(result.total).toBe(42);
|
|
204
|
+
});
|
|
205
|
+
it('AND operator requires all conditions to match', ()=>{
|
|
206
|
+
const field = {
|
|
207
|
+
id: 'cf-1',
|
|
208
|
+
name: 'Total',
|
|
209
|
+
key: 'total',
|
|
210
|
+
valueType: 'string',
|
|
211
|
+
defaultValue: 'no-match',
|
|
212
|
+
rules: [
|
|
213
|
+
{
|
|
214
|
+
id: 'rule-1',
|
|
215
|
+
condition: {
|
|
216
|
+
operator: 'AND',
|
|
217
|
+
conditions: [
|
|
218
|
+
{
|
|
219
|
+
fieldId: 'a',
|
|
220
|
+
comparison: 'equals',
|
|
221
|
+
value: 1
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
fieldId: 'b',
|
|
225
|
+
comparison: 'equals',
|
|
226
|
+
value: 2
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
computation: {
|
|
231
|
+
type: 'direct',
|
|
232
|
+
config: {
|
|
233
|
+
type: 'direct',
|
|
234
|
+
value: 'matched'
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
};
|
|
240
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
241
|
+
a: 1,
|
|
242
|
+
b: 2
|
|
243
|
+
}, [
|
|
244
|
+
field
|
|
245
|
+
]).total).toBe('matched');
|
|
246
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
247
|
+
a: 1,
|
|
248
|
+
b: 99
|
|
249
|
+
}, [
|
|
250
|
+
field
|
|
251
|
+
]).total).toBe('no-match');
|
|
252
|
+
});
|
|
253
|
+
it('OR operator matches when at least one condition matches', ()=>{
|
|
254
|
+
const field = {
|
|
255
|
+
id: 'cf-1',
|
|
256
|
+
name: 'Total',
|
|
257
|
+
key: 'total',
|
|
258
|
+
valueType: 'string',
|
|
259
|
+
defaultValue: 'no-match',
|
|
260
|
+
rules: [
|
|
261
|
+
{
|
|
262
|
+
id: 'rule-1',
|
|
263
|
+
condition: {
|
|
264
|
+
operator: 'OR',
|
|
265
|
+
conditions: [
|
|
266
|
+
{
|
|
267
|
+
fieldId: 'a',
|
|
268
|
+
comparison: 'equals',
|
|
269
|
+
value: 1
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
fieldId: 'b',
|
|
273
|
+
comparison: 'equals',
|
|
274
|
+
value: 2
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
},
|
|
278
|
+
computation: {
|
|
279
|
+
type: 'direct',
|
|
280
|
+
config: {
|
|
281
|
+
type: 'direct',
|
|
282
|
+
value: 'matched'
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
};
|
|
288
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
289
|
+
a: 1,
|
|
290
|
+
b: 99
|
|
291
|
+
}, [
|
|
292
|
+
field
|
|
293
|
+
]).total).toBe('matched');
|
|
294
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
295
|
+
a: 99,
|
|
296
|
+
b: 99
|
|
297
|
+
}, [
|
|
298
|
+
field
|
|
299
|
+
]).total).toBe('no-match');
|
|
300
|
+
});
|
|
301
|
+
it('normalizes lower-case / unknown operator strings to AND by default', ()=>{
|
|
302
|
+
const field = {
|
|
303
|
+
id: 'cf-1',
|
|
304
|
+
name: 'Total',
|
|
305
|
+
key: 'total',
|
|
306
|
+
valueType: 'string',
|
|
307
|
+
defaultValue: 'no-match',
|
|
308
|
+
rules: [
|
|
309
|
+
{
|
|
310
|
+
id: 'rule-1',
|
|
311
|
+
condition: {
|
|
312
|
+
operator: 'unknown-operator',
|
|
313
|
+
conditions: [
|
|
314
|
+
{
|
|
315
|
+
fieldId: 'a',
|
|
316
|
+
comparison: 'equals',
|
|
317
|
+
value: 1
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
fieldId: 'b',
|
|
321
|
+
comparison: 'equals',
|
|
322
|
+
value: 2
|
|
323
|
+
}
|
|
324
|
+
]
|
|
325
|
+
},
|
|
326
|
+
computation: {
|
|
327
|
+
type: 'direct',
|
|
328
|
+
config: {
|
|
329
|
+
type: 'direct',
|
|
330
|
+
value: 'matched'
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
]
|
|
335
|
+
};
|
|
336
|
+
// Falls back to AND semantics, so a partial match should NOT trigger the rule.
|
|
337
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
338
|
+
a: 1,
|
|
339
|
+
b: 99
|
|
340
|
+
}, [
|
|
341
|
+
field
|
|
342
|
+
]).total).toBe('no-match');
|
|
343
|
+
});
|
|
344
|
+
it('normalizes lower-case "or" to OR semantics', ()=>{
|
|
345
|
+
const field = {
|
|
346
|
+
id: 'cf-1',
|
|
347
|
+
name: 'Total',
|
|
348
|
+
key: 'total',
|
|
349
|
+
valueType: 'string',
|
|
350
|
+
defaultValue: 'no-match',
|
|
351
|
+
rules: [
|
|
352
|
+
{
|
|
353
|
+
id: 'rule-1',
|
|
354
|
+
condition: {
|
|
355
|
+
operator: 'or',
|
|
356
|
+
conditions: [
|
|
357
|
+
{
|
|
358
|
+
fieldId: 'a',
|
|
359
|
+
comparison: 'equals',
|
|
360
|
+
value: 1
|
|
361
|
+
}
|
|
362
|
+
]
|
|
363
|
+
},
|
|
364
|
+
computation: {
|
|
365
|
+
type: 'direct',
|
|
366
|
+
config: {
|
|
367
|
+
type: 'direct',
|
|
368
|
+
value: 'matched'
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
]
|
|
373
|
+
};
|
|
374
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
375
|
+
a: 1
|
|
376
|
+
}, [
|
|
377
|
+
field
|
|
378
|
+
]).total).toBe('matched');
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
// ==========================================================================
|
|
382
|
+
// Comparison operators
|
|
383
|
+
// ==========================================================================
|
|
384
|
+
describe('comparison operators', ()=>{
|
|
385
|
+
function withComparison(comparison, value) {
|
|
386
|
+
const field = {
|
|
387
|
+
id: 'cf-1',
|
|
388
|
+
name: 'Total',
|
|
389
|
+
key: 'total',
|
|
390
|
+
valueType: 'string',
|
|
391
|
+
defaultValue: 'no-match',
|
|
392
|
+
rules: [
|
|
393
|
+
{
|
|
394
|
+
id: 'rule-1',
|
|
395
|
+
condition: {
|
|
396
|
+
operator: 'AND',
|
|
397
|
+
conditions: [
|
|
398
|
+
{
|
|
399
|
+
fieldId: 'x',
|
|
400
|
+
comparison,
|
|
401
|
+
value
|
|
402
|
+
}
|
|
403
|
+
]
|
|
404
|
+
},
|
|
405
|
+
computation: {
|
|
406
|
+
type: 'direct',
|
|
407
|
+
config: {
|
|
408
|
+
type: 'direct',
|
|
409
|
+
value: 'matched'
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
]
|
|
414
|
+
};
|
|
415
|
+
return field;
|
|
416
|
+
}
|
|
417
|
+
it('equals matches by strict or stringified equality', ()=>{
|
|
418
|
+
const field = withComparison('equals', '5');
|
|
419
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
420
|
+
x: 5
|
|
421
|
+
}, [
|
|
422
|
+
field
|
|
423
|
+
]).total).toBe('matched');
|
|
424
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
425
|
+
x: 6
|
|
426
|
+
}, [
|
|
427
|
+
field
|
|
428
|
+
]).total).toBe('no-match');
|
|
429
|
+
});
|
|
430
|
+
it('not_equals is the inverse of equals', ()=>{
|
|
431
|
+
const field = withComparison('not_equals', '5');
|
|
432
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
433
|
+
x: 6
|
|
434
|
+
}, [
|
|
435
|
+
field
|
|
436
|
+
]).total).toBe('matched');
|
|
437
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
438
|
+
x: 5
|
|
439
|
+
}, [
|
|
440
|
+
field
|
|
441
|
+
]).total).toBe('no-match');
|
|
442
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
443
|
+
x: '5'
|
|
444
|
+
}, [
|
|
445
|
+
field
|
|
446
|
+
]).total).toBe('no-match');
|
|
447
|
+
});
|
|
448
|
+
it('is_empty treats null, undefined, empty string and empty array as empty', ()=>{
|
|
449
|
+
const field = withComparison('is_empty', null);
|
|
450
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
451
|
+
x: null
|
|
452
|
+
}, [
|
|
453
|
+
field
|
|
454
|
+
]).total).toBe('matched');
|
|
455
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
456
|
+
x: undefined
|
|
457
|
+
}, [
|
|
458
|
+
field
|
|
459
|
+
]).total).toBe('matched');
|
|
460
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
461
|
+
x: ''
|
|
462
|
+
}, [
|
|
463
|
+
field
|
|
464
|
+
]).total).toBe('matched');
|
|
465
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
466
|
+
x: []
|
|
467
|
+
}, [
|
|
468
|
+
field
|
|
469
|
+
]).total).toBe('matched');
|
|
470
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
471
|
+
x: 0
|
|
472
|
+
}, [
|
|
473
|
+
field
|
|
474
|
+
]).total).toBe('no-match');
|
|
475
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
476
|
+
field
|
|
477
|
+
]).total).toBe('matched');
|
|
478
|
+
});
|
|
479
|
+
it('is_not_empty is the inverse of is_empty', ()=>{
|
|
480
|
+
const field = withComparison('is_not_empty', null);
|
|
481
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
482
|
+
x: 'value'
|
|
483
|
+
}, [
|
|
484
|
+
field
|
|
485
|
+
]).total).toBe('matched');
|
|
486
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
487
|
+
x: ''
|
|
488
|
+
}, [
|
|
489
|
+
field
|
|
490
|
+
]).total).toBe('no-match');
|
|
491
|
+
});
|
|
492
|
+
it('contains / not_contains perform substring checks', ()=>{
|
|
493
|
+
const contains = withComparison('contains', 'ell');
|
|
494
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
495
|
+
x: 'hello'
|
|
496
|
+
}, [
|
|
497
|
+
contains
|
|
498
|
+
]).total).toBe('matched');
|
|
499
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
500
|
+
x: 'world'
|
|
501
|
+
}, [
|
|
502
|
+
contains
|
|
503
|
+
]).total).toBe('no-match');
|
|
504
|
+
const notContains = withComparison('not_contains', 'ell');
|
|
505
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
506
|
+
x: 'world'
|
|
507
|
+
}, [
|
|
508
|
+
notContains
|
|
509
|
+
]).total).toBe('matched');
|
|
510
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
511
|
+
x: 'hello'
|
|
512
|
+
}, [
|
|
513
|
+
notContains
|
|
514
|
+
]).total).toBe('no-match');
|
|
515
|
+
});
|
|
516
|
+
it('starts_with / ends_with perform prefix/suffix checks', ()=>{
|
|
517
|
+
const startsWith = withComparison('starts_with', 'hel');
|
|
518
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
519
|
+
x: 'hello'
|
|
520
|
+
}, [
|
|
521
|
+
startsWith
|
|
522
|
+
]).total).toBe('matched');
|
|
523
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
524
|
+
x: 'shell'
|
|
525
|
+
}, [
|
|
526
|
+
startsWith
|
|
527
|
+
]).total).toBe('no-match');
|
|
528
|
+
const endsWith = withComparison('ends_with', 'llo');
|
|
529
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
530
|
+
x: 'hello'
|
|
531
|
+
}, [
|
|
532
|
+
endsWith
|
|
533
|
+
]).total).toBe('matched');
|
|
534
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
535
|
+
x: 'llothing'
|
|
536
|
+
}, [
|
|
537
|
+
endsWith
|
|
538
|
+
]).total).toBe('no-match');
|
|
539
|
+
});
|
|
540
|
+
it('numeric comparisons coerce operands with Number()', ()=>{
|
|
541
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
542
|
+
x: 10
|
|
543
|
+
}, [
|
|
544
|
+
withComparison('greater_than', 5)
|
|
545
|
+
]).total).toBe('matched');
|
|
546
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
547
|
+
x: 3
|
|
548
|
+
}, [
|
|
549
|
+
withComparison('greater_than', 5)
|
|
550
|
+
]).total).toBe('no-match');
|
|
551
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
552
|
+
x: 3
|
|
553
|
+
}, [
|
|
554
|
+
withComparison('less_than', 5)
|
|
555
|
+
]).total).toBe('matched');
|
|
556
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
557
|
+
x: '5'
|
|
558
|
+
}, [
|
|
559
|
+
withComparison('greater_or_equal', 5)
|
|
560
|
+
]).total).toBe('matched');
|
|
561
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
562
|
+
x: 5
|
|
563
|
+
}, [
|
|
564
|
+
withComparison('less_or_equal', 5)
|
|
565
|
+
]).total).toBe('matched');
|
|
566
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
567
|
+
x: 6
|
|
568
|
+
}, [
|
|
569
|
+
withComparison('less_or_equal', 5)
|
|
570
|
+
]).total).toBe('no-match');
|
|
571
|
+
});
|
|
572
|
+
it('is_before / is_after compare Date values', ()=>{
|
|
573
|
+
const before = withComparison('is_before', '2024-06-01');
|
|
574
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
575
|
+
x: '2024-01-01'
|
|
576
|
+
}, [
|
|
577
|
+
before
|
|
578
|
+
]).total).toBe('matched');
|
|
579
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
580
|
+
x: '2024-12-01'
|
|
581
|
+
}, [
|
|
582
|
+
before
|
|
583
|
+
]).total).toBe('no-match');
|
|
584
|
+
const after = withComparison('is_after', '2024-06-01');
|
|
585
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
586
|
+
x: '2024-12-01'
|
|
587
|
+
}, [
|
|
588
|
+
after
|
|
589
|
+
]).total).toBe('matched');
|
|
590
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
591
|
+
x: '2024-01-01'
|
|
592
|
+
}, [
|
|
593
|
+
after
|
|
594
|
+
]).total).toBe('no-match');
|
|
595
|
+
});
|
|
596
|
+
it('is_checked treats true, "true" and 1 as checked', ()=>{
|
|
597
|
+
const field = withComparison('is_checked', null);
|
|
598
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
599
|
+
x: true
|
|
600
|
+
}, [
|
|
601
|
+
field
|
|
602
|
+
]).total).toBe('matched');
|
|
603
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
604
|
+
x: 'true'
|
|
605
|
+
}, [
|
|
606
|
+
field
|
|
607
|
+
]).total).toBe('matched');
|
|
608
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
609
|
+
x: 1
|
|
610
|
+
}, [
|
|
611
|
+
field
|
|
612
|
+
]).total).toBe('matched');
|
|
613
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
614
|
+
x: false
|
|
615
|
+
}, [
|
|
616
|
+
field
|
|
617
|
+
]).total).toBe('no-match');
|
|
618
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
619
|
+
x: 0
|
|
620
|
+
}, [
|
|
621
|
+
field
|
|
622
|
+
]).total).toBe('no-match');
|
|
623
|
+
});
|
|
624
|
+
it('is_not_checked treats false, "false", 0 and falsy as unchecked', ()=>{
|
|
625
|
+
const field = withComparison('is_not_checked', null);
|
|
626
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
627
|
+
x: false
|
|
628
|
+
}, [
|
|
629
|
+
field
|
|
630
|
+
]).total).toBe('matched');
|
|
631
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
632
|
+
x: 'false'
|
|
633
|
+
}, [
|
|
634
|
+
field
|
|
635
|
+
]).total).toBe('matched');
|
|
636
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
637
|
+
x: 0
|
|
638
|
+
}, [
|
|
639
|
+
field
|
|
640
|
+
]).total).toBe('matched');
|
|
641
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
642
|
+
x: undefined
|
|
643
|
+
}, [
|
|
644
|
+
field
|
|
645
|
+
]).total).toBe('matched');
|
|
646
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
647
|
+
x: true
|
|
648
|
+
}, [
|
|
649
|
+
field
|
|
650
|
+
]).total).toBe('no-match');
|
|
651
|
+
});
|
|
652
|
+
it('is_any_of / in match against an array of allowed values', ()=>{
|
|
653
|
+
const field = withComparison('is_any_of', [
|
|
654
|
+
'a',
|
|
655
|
+
'b',
|
|
656
|
+
'c'
|
|
657
|
+
]);
|
|
658
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
659
|
+
x: 'b'
|
|
660
|
+
}, [
|
|
661
|
+
field
|
|
662
|
+
]).total).toBe('matched');
|
|
663
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
664
|
+
x: 'z'
|
|
665
|
+
}, [
|
|
666
|
+
field
|
|
667
|
+
]).total).toBe('no-match');
|
|
668
|
+
const inField = withComparison('in', [
|
|
669
|
+
'a',
|
|
670
|
+
'b',
|
|
671
|
+
'c'
|
|
672
|
+
]);
|
|
673
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
674
|
+
x: 'a'
|
|
675
|
+
}, [
|
|
676
|
+
inField
|
|
677
|
+
]).total).toBe('matched');
|
|
678
|
+
});
|
|
679
|
+
it('is_any_of returns false (no match) when compareValue is not an array', ()=>{
|
|
680
|
+
const field = withComparison('is_any_of', 'not-an-array');
|
|
681
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
682
|
+
x: 'not-an-array'
|
|
683
|
+
}, [
|
|
684
|
+
field
|
|
685
|
+
]).total).toBe('no-match');
|
|
686
|
+
});
|
|
687
|
+
it('is_none_of / not_in match when value is absent from the array', ()=>{
|
|
688
|
+
const field = withComparison('is_none_of', [
|
|
689
|
+
'a',
|
|
690
|
+
'b',
|
|
691
|
+
'c'
|
|
692
|
+
]);
|
|
693
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
694
|
+
x: 'z'
|
|
695
|
+
}, [
|
|
696
|
+
field
|
|
697
|
+
]).total).toBe('matched');
|
|
698
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
699
|
+
x: 'a'
|
|
700
|
+
}, [
|
|
701
|
+
field
|
|
702
|
+
]).total).toBe('no-match');
|
|
703
|
+
const notIn = withComparison('not_in', [
|
|
704
|
+
'a',
|
|
705
|
+
'b',
|
|
706
|
+
'c'
|
|
707
|
+
]);
|
|
708
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
709
|
+
x: 'z'
|
|
710
|
+
}, [
|
|
711
|
+
notIn
|
|
712
|
+
]).total).toBe('matched');
|
|
713
|
+
});
|
|
714
|
+
it('is_none_of treats a non-array compareValue as always matching', ()=>{
|
|
715
|
+
const field = withComparison('is_none_of', 'not-an-array');
|
|
716
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
717
|
+
x: 'anything'
|
|
718
|
+
}, [
|
|
719
|
+
field
|
|
720
|
+
]).total).toBe('matched');
|
|
721
|
+
});
|
|
722
|
+
it('returns false for unknown comparison operators', ()=>{
|
|
723
|
+
const field = withComparison('some_unknown_operator', 1);
|
|
724
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
725
|
+
x: 1
|
|
726
|
+
}, [
|
|
727
|
+
field
|
|
728
|
+
]).total).toBe('no-match');
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
// ==========================================================================
|
|
732
|
+
// Computation types
|
|
733
|
+
// ==========================================================================
|
|
734
|
+
describe('direct computation', ()=>{
|
|
735
|
+
it('coerces the direct value to a number when valueType is number', ()=>{
|
|
736
|
+
const field = directField({
|
|
737
|
+
valueType: 'number'
|
|
738
|
+
});
|
|
739
|
+
field.rules[0].computation = {
|
|
740
|
+
type: 'direct',
|
|
741
|
+
config: {
|
|
742
|
+
type: 'direct',
|
|
743
|
+
value: '42'
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
747
|
+
field
|
|
748
|
+
]).total).toBe(42);
|
|
749
|
+
});
|
|
750
|
+
it('coerces the direct value to a string when valueType is string', ()=>{
|
|
751
|
+
const field = directField({
|
|
752
|
+
valueType: 'string'
|
|
753
|
+
});
|
|
754
|
+
field.rules[0].computation = {
|
|
755
|
+
type: 'direct',
|
|
756
|
+
config: {
|
|
757
|
+
type: 'direct',
|
|
758
|
+
value: 42
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
762
|
+
field
|
|
763
|
+
]).total).toBe('42');
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
describe('field_reference computation', ()=>{
|
|
767
|
+
it('reads the referenced field value and coerces per valueType', ()=>{
|
|
768
|
+
const field = directField({
|
|
769
|
+
valueType: 'number'
|
|
770
|
+
});
|
|
771
|
+
field.rules[0].computation = {
|
|
772
|
+
type: 'field_reference',
|
|
773
|
+
config: {
|
|
774
|
+
type: 'field_reference',
|
|
775
|
+
fieldId: 'age'
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
779
|
+
age: '30'
|
|
780
|
+
}, [
|
|
781
|
+
field
|
|
782
|
+
]).total).toBe(30);
|
|
783
|
+
});
|
|
784
|
+
it('returns null when the referenced field is null or undefined', ()=>{
|
|
785
|
+
const field = directField({
|
|
786
|
+
valueType: 'number'
|
|
787
|
+
});
|
|
788
|
+
field.rules[0].computation = {
|
|
789
|
+
type: 'field_reference',
|
|
790
|
+
config: {
|
|
791
|
+
type: 'field_reference',
|
|
792
|
+
fieldId: 'age'
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
796
|
+
age: null
|
|
797
|
+
}, [
|
|
798
|
+
field
|
|
799
|
+
]).total).toBeNull();
|
|
800
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
801
|
+
field
|
|
802
|
+
]).total).toBeNull();
|
|
803
|
+
});
|
|
804
|
+
it('coerces the referenced field to a string when valueType is string', ()=>{
|
|
805
|
+
const field = directField({
|
|
806
|
+
valueType: 'string'
|
|
807
|
+
});
|
|
808
|
+
field.rules[0].computation = {
|
|
809
|
+
type: 'field_reference',
|
|
810
|
+
config: {
|
|
811
|
+
type: 'field_reference',
|
|
812
|
+
fieldId: 'age'
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
816
|
+
age: 30
|
|
817
|
+
}, [
|
|
818
|
+
field
|
|
819
|
+
]).total).toBe('30');
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
describe('unknown / malformed computation configs', ()=>{
|
|
823
|
+
it('returns null for an unrecognized computation type', ()=>{
|
|
824
|
+
const field = directField();
|
|
825
|
+
field.rules[0].computation = {
|
|
826
|
+
type: 'not-a-real-type',
|
|
827
|
+
config: {}
|
|
828
|
+
};
|
|
829
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
830
|
+
field
|
|
831
|
+
]).total).toBeNull();
|
|
832
|
+
});
|
|
833
|
+
it('returns null when computation.type does not match its config shape', ()=>{
|
|
834
|
+
const field = directField();
|
|
835
|
+
// type says "direct" but config is missing the "value" property expected by the type guard
|
|
836
|
+
field.rules[0].computation = {
|
|
837
|
+
type: 'direct',
|
|
838
|
+
config: {
|
|
839
|
+
type: 'field_reference',
|
|
840
|
+
fieldId: 'x'
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
844
|
+
field
|
|
845
|
+
]).total).toBeNull();
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
// ==========================================================================
|
|
849
|
+
// Arithmetic computation
|
|
850
|
+
// ==========================================================================
|
|
851
|
+
describe('arithmetic computation', ()=>{
|
|
852
|
+
function arithmeticField(config) {
|
|
853
|
+
return {
|
|
854
|
+
id: 'cf-1',
|
|
855
|
+
name: 'Total',
|
|
856
|
+
key: 'total',
|
|
857
|
+
valueType: 'number',
|
|
858
|
+
rules: [
|
|
859
|
+
{
|
|
860
|
+
id: 'rule-1',
|
|
861
|
+
computation: {
|
|
862
|
+
type: 'arithmetic',
|
|
863
|
+
config
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
]
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
it('sum adds all operand values (fields + constants)', ()=>{
|
|
870
|
+
const field = arithmeticField({
|
|
871
|
+
type: 'arithmetic',
|
|
872
|
+
operation: 'sum',
|
|
873
|
+
operands: [
|
|
874
|
+
{
|
|
875
|
+
type: 'field',
|
|
876
|
+
fieldId: 'a'
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
type: 'field',
|
|
880
|
+
fieldId: 'b'
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
type: 'constant',
|
|
884
|
+
value: 10
|
|
885
|
+
}
|
|
886
|
+
]
|
|
887
|
+
});
|
|
888
|
+
expect((0, _computedfieldutils.calculateComputedFields)({
|
|
889
|
+
a: 5,
|
|
890
|
+
b: 3
|
|
891
|
+
}, [
|
|
892
|
+
field
|
|
893
|
+
]).total).toBe(18);
|
|
894
|
+
});
|
|
895
|
+
it('increment behaves the same as sum', ()=>{
|
|
896
|
+
const field = arithmeticField({
|
|
897
|
+
type: 'arithmetic',
|
|
898
|
+
operation: 'increment',
|
|
899
|
+
operands: [
|
|
900
|
+
{
|
|
901
|
+
type: 'constant',
|
|
902
|
+
value: 1
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
type: 'constant',
|
|
906
|
+
value: 2
|
|
907
|
+
}
|
|
908
|
+
]
|
|
909
|
+
});
|
|
910
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
911
|
+
field
|
|
912
|
+
]).total).toBe(3);
|
|
913
|
+
});
|
|
914
|
+
it('subtract subtracts subsequent operands from the first', ()=>{
|
|
915
|
+
const field = arithmeticField({
|
|
916
|
+
type: 'arithmetic',
|
|
917
|
+
operation: 'subtract',
|
|
918
|
+
operands: [
|
|
919
|
+
{
|
|
920
|
+
type: 'constant',
|
|
921
|
+
value: 10
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
type: 'constant',
|
|
925
|
+
value: 3
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
type: 'constant',
|
|
929
|
+
value: 2
|
|
930
|
+
}
|
|
931
|
+
]
|
|
932
|
+
});
|
|
933
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
934
|
+
field
|
|
935
|
+
]).total).toBe(5);
|
|
936
|
+
});
|
|
937
|
+
it('decrement behaves the same as subtract', ()=>{
|
|
938
|
+
const field = arithmeticField({
|
|
939
|
+
type: 'arithmetic',
|
|
940
|
+
operation: 'decrement',
|
|
941
|
+
operands: [
|
|
942
|
+
{
|
|
943
|
+
type: 'constant',
|
|
944
|
+
value: 10
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
type: 'constant',
|
|
948
|
+
value: 4
|
|
949
|
+
}
|
|
950
|
+
]
|
|
951
|
+
});
|
|
952
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
953
|
+
field
|
|
954
|
+
]).total).toBe(6);
|
|
955
|
+
});
|
|
956
|
+
it('multiply multiplies all operand values', ()=>{
|
|
957
|
+
const field = arithmeticField({
|
|
958
|
+
type: 'arithmetic',
|
|
959
|
+
operation: 'multiply',
|
|
960
|
+
operands: [
|
|
961
|
+
{
|
|
962
|
+
type: 'constant',
|
|
963
|
+
value: 2
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
type: 'constant',
|
|
967
|
+
value: 3
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
type: 'constant',
|
|
971
|
+
value: 4
|
|
972
|
+
}
|
|
973
|
+
]
|
|
974
|
+
});
|
|
975
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
976
|
+
field
|
|
977
|
+
]).total).toBe(24);
|
|
978
|
+
});
|
|
979
|
+
it('divide divides the first operand by each subsequent operand in order', ()=>{
|
|
980
|
+
const field = arithmeticField({
|
|
981
|
+
type: 'arithmetic',
|
|
982
|
+
operation: 'divide',
|
|
983
|
+
operands: [
|
|
984
|
+
{
|
|
985
|
+
type: 'constant',
|
|
986
|
+
value: 100
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
type: 'constant',
|
|
990
|
+
value: 2
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
type: 'constant',
|
|
994
|
+
value: 5
|
|
995
|
+
}
|
|
996
|
+
]
|
|
997
|
+
});
|
|
998
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
999
|
+
field
|
|
1000
|
+
]).total).toBe(10);
|
|
1001
|
+
});
|
|
1002
|
+
it('divide safely skips division by zero operands (guards against Infinity/NaN)', ()=>{
|
|
1003
|
+
const field = arithmeticField({
|
|
1004
|
+
type: 'arithmetic',
|
|
1005
|
+
operation: 'divide',
|
|
1006
|
+
operands: [
|
|
1007
|
+
{
|
|
1008
|
+
type: 'constant',
|
|
1009
|
+
value: 100
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
type: 'constant',
|
|
1013
|
+
value: 0
|
|
1014
|
+
}
|
|
1015
|
+
]
|
|
1016
|
+
});
|
|
1017
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
1018
|
+
field
|
|
1019
|
+
]).total).toBe(100);
|
|
1020
|
+
});
|
|
1021
|
+
it('average computes the mean of all operand values', ()=>{
|
|
1022
|
+
const field = arithmeticField({
|
|
1023
|
+
type: 'arithmetic',
|
|
1024
|
+
operation: 'average',
|
|
1025
|
+
operands: [
|
|
1026
|
+
{
|
|
1027
|
+
type: 'constant',
|
|
1028
|
+
value: 2
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
type: 'constant',
|
|
1032
|
+
value: 4
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
type: 'constant',
|
|
1036
|
+
value: 6
|
|
1037
|
+
}
|
|
1038
|
+
]
|
|
1039
|
+
});
|
|
1040
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
1041
|
+
field
|
|
1042
|
+
]).total).toBe(4);
|
|
1043
|
+
});
|
|
1044
|
+
it('min / max compute the extremes among operand values', ()=>{
|
|
1045
|
+
const operands = [
|
|
1046
|
+
{
|
|
1047
|
+
type: 'constant',
|
|
1048
|
+
value: 5
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
type: 'constant',
|
|
1052
|
+
value: 1
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
type: 'constant',
|
|
1056
|
+
value: 9
|
|
1057
|
+
}
|
|
1058
|
+
];
|
|
1059
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
1060
|
+
arithmeticField({
|
|
1061
|
+
type: 'arithmetic',
|
|
1062
|
+
operation: 'min',
|
|
1063
|
+
operands
|
|
1064
|
+
})
|
|
1065
|
+
]).total).toBe(1);
|
|
1066
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
1067
|
+
arithmeticField({
|
|
1068
|
+
type: 'arithmetic',
|
|
1069
|
+
operation: 'max',
|
|
1070
|
+
operands
|
|
1071
|
+
})
|
|
1072
|
+
]).total).toBe(9);
|
|
1073
|
+
});
|
|
1074
|
+
it('treats missing field operand values as 0', ()=>{
|
|
1075
|
+
const field = arithmeticField({
|
|
1076
|
+
type: 'arithmetic',
|
|
1077
|
+
operation: 'sum',
|
|
1078
|
+
operands: [
|
|
1079
|
+
{
|
|
1080
|
+
type: 'field',
|
|
1081
|
+
fieldId: 'missing'
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
type: 'constant',
|
|
1085
|
+
value: 5
|
|
1086
|
+
}
|
|
1087
|
+
]
|
|
1088
|
+
});
|
|
1089
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
1090
|
+
field
|
|
1091
|
+
]).total).toBe(5);
|
|
1092
|
+
});
|
|
1093
|
+
it('treats a constant operand with an undefined value as 0', ()=>{
|
|
1094
|
+
const field = arithmeticField({
|
|
1095
|
+
type: 'arithmetic',
|
|
1096
|
+
operation: 'sum',
|
|
1097
|
+
operands: [
|
|
1098
|
+
{
|
|
1099
|
+
type: 'constant'
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
type: 'constant',
|
|
1103
|
+
value: 5
|
|
1104
|
+
}
|
|
1105
|
+
]
|
|
1106
|
+
});
|
|
1107
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
1108
|
+
field
|
|
1109
|
+
]).total).toBe(5);
|
|
1110
|
+
});
|
|
1111
|
+
it('returns null for an unknown arithmetic operation', ()=>{
|
|
1112
|
+
const field = arithmeticField({
|
|
1113
|
+
type: 'arithmetic',
|
|
1114
|
+
operation: 'unknown-op',
|
|
1115
|
+
operands: [
|
|
1116
|
+
{
|
|
1117
|
+
type: 'constant',
|
|
1118
|
+
value: 1
|
|
1119
|
+
}
|
|
1120
|
+
]
|
|
1121
|
+
});
|
|
1122
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
1123
|
+
field
|
|
1124
|
+
]).total).toBeNull();
|
|
1125
|
+
});
|
|
1126
|
+
it('returns null when operands array is empty', ()=>{
|
|
1127
|
+
const field = arithmeticField({
|
|
1128
|
+
type: 'arithmetic',
|
|
1129
|
+
operation: 'sum',
|
|
1130
|
+
operands: []
|
|
1131
|
+
});
|
|
1132
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
1133
|
+
field
|
|
1134
|
+
]).total).toBeNull();
|
|
1135
|
+
});
|
|
1136
|
+
it('does not treat an arithmetic config as valid when operands is not an array', ()=>{
|
|
1137
|
+
const field = directField();
|
|
1138
|
+
field.rules[0].computation = {
|
|
1139
|
+
type: 'arithmetic',
|
|
1140
|
+
config: {
|
|
1141
|
+
type: 'arithmetic',
|
|
1142
|
+
operation: 'sum',
|
|
1143
|
+
operands: 'not-an-array'
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
// Fails the isArithmeticConfig type guard -> falls through to "unknown computation type" -> null
|
|
1147
|
+
expect((0, _computedfieldutils.calculateComputedFields)({}, [
|
|
1148
|
+
field
|
|
1149
|
+
]).total).toBeNull();
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
// ==========================================================================
|
|
1153
|
+
// Error handling / malformed field defensive fallback
|
|
1154
|
+
// ==========================================================================
|
|
1155
|
+
describe('defensive error handling', ()=>{
|
|
1156
|
+
it('falls back to defaultValue when field.rules is malformed and calculation throws', ()=>{
|
|
1157
|
+
const field = {
|
|
1158
|
+
id: 'cf-1',
|
|
1159
|
+
name: 'Total',
|
|
1160
|
+
key: 'total',
|
|
1161
|
+
valueType: 'number',
|
|
1162
|
+
defaultValue: -1,
|
|
1163
|
+
rules: undefined
|
|
1164
|
+
};
|
|
1165
|
+
const result = (0, _computedfieldutils.calculateComputedFields)({}, [
|
|
1166
|
+
field
|
|
1167
|
+
]);
|
|
1168
|
+
expect(result.total).toBe(-1);
|
|
1169
|
+
});
|
|
1170
|
+
it('falls back to null when field.rules is malformed and no defaultValue is set', ()=>{
|
|
1171
|
+
const field = {
|
|
1172
|
+
id: 'cf-1',
|
|
1173
|
+
name: 'Total',
|
|
1174
|
+
key: 'total',
|
|
1175
|
+
valueType: 'number',
|
|
1176
|
+
rules: undefined
|
|
1177
|
+
};
|
|
1178
|
+
const result = (0, _computedfieldutils.calculateComputedFields)({}, [
|
|
1179
|
+
field
|
|
1180
|
+
]);
|
|
1181
|
+
expect(result.total).toBeNull();
|
|
1182
|
+
});
|
|
1183
|
+
it('isolates failures per field — one bad field does not break the others', ()=>{
|
|
1184
|
+
const badField = {
|
|
1185
|
+
id: 'cf-bad',
|
|
1186
|
+
name: 'Bad',
|
|
1187
|
+
key: 'bad',
|
|
1188
|
+
valueType: 'number',
|
|
1189
|
+
defaultValue: 0,
|
|
1190
|
+
rules: undefined
|
|
1191
|
+
};
|
|
1192
|
+
const goodField = directField({
|
|
1193
|
+
key: 'good'
|
|
1194
|
+
});
|
|
1195
|
+
const result = (0, _computedfieldutils.calculateComputedFields)({}, [
|
|
1196
|
+
badField,
|
|
1197
|
+
goodField
|
|
1198
|
+
]);
|
|
1199
|
+
expect(result).toEqual({
|
|
1200
|
+
bad: 0,
|
|
1201
|
+
good: 42
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
});
|
|
1205
|
+
});
|