@fogpipe/forma-core 0.12.0 → 0.13.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/README.md +12 -12
- package/dist/{chunk-U2OXXFEH.js → chunk-BDICNCE2.js} +1 -1
- package/dist/chunk-BDICNCE2.js.map +1 -0
- package/dist/{chunk-ZSW7NIMY.js → chunk-NKA3L2LJ.js} +64 -15
- package/dist/chunk-NKA3L2LJ.js.map +1 -0
- package/dist/engine/calculate.d.ts.map +1 -1
- package/dist/engine/enabled.d.ts.map +1 -1
- package/dist/engine/index.cjs +62 -13
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.ts +8 -8
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +2 -2
- package/dist/engine/readonly.d.ts.map +1 -1
- package/dist/engine/required.d.ts.map +1 -1
- package/dist/engine/validate.d.ts.map +1 -1
- package/dist/engine/visibility.d.ts.map +1 -1
- package/dist/feel/index.cjs.map +1 -1
- package/dist/feel/index.js +1 -1
- package/dist/index.cjs +62 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/feel.test.ts +67 -76
- package/src/__tests__/format.test.ts +19 -6
- package/src/__tests__/validate.test.ts +62 -20
- package/src/__tests__/visibility.test.ts +217 -85
- package/src/engine/calculate.ts +13 -10
- package/src/engine/enabled.ts +16 -6
- package/src/engine/index.ts +8 -28
- package/src/engine/readonly.ts +16 -6
- package/src/engine/required.ts +7 -5
- package/src/engine/validate.ts +43 -22
- package/src/engine/visibility.ts +40 -16
- package/src/feel/index.ts +12 -12
- package/src/format/index.ts +1 -1
- package/src/types.ts +46 -7
- package/dist/chunk-U2OXXFEH.js.map +0 -1
- package/dist/chunk-ZSW7NIMY.js.map +0 -1
|
@@ -3,17 +3,22 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect } from "vitest";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getOptionsVisibility,
|
|
8
|
+
getVisibleOptions,
|
|
9
|
+
} from "../engine/visibility.js";
|
|
7
10
|
import type { Forma, SelectOption } from "../types.js";
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Helper to create a minimal Forma spec for testing
|
|
11
14
|
*/
|
|
12
|
-
function createTestSpec(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
function createTestSpec(
|
|
16
|
+
options: {
|
|
17
|
+
fields?: Record<string, unknown>;
|
|
18
|
+
computed?: Record<string, { expression: string }>;
|
|
19
|
+
referenceData?: Record<string, unknown>;
|
|
20
|
+
} = {},
|
|
21
|
+
): Forma {
|
|
17
22
|
const { fields = {}, computed, referenceData } = options;
|
|
18
23
|
|
|
19
24
|
// Build fieldOrder from fields keys
|
|
@@ -53,7 +58,11 @@ describe("getOptionsVisibility", () => {
|
|
|
53
58
|
type: "select",
|
|
54
59
|
options: [
|
|
55
60
|
{ value: "junior", label: "Junior" },
|
|
56
|
-
{
|
|
61
|
+
{
|
|
62
|
+
value: "senior",
|
|
63
|
+
label: "Senior",
|
|
64
|
+
visibleWhen: "experience >= 5",
|
|
65
|
+
},
|
|
57
66
|
],
|
|
58
67
|
},
|
|
59
68
|
},
|
|
@@ -92,9 +101,21 @@ describe("getOptionsVisibility", () => {
|
|
|
92
101
|
position: {
|
|
93
102
|
type: "select",
|
|
94
103
|
options: [
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
{
|
|
105
|
+
value: "dev_fe",
|
|
106
|
+
label: "Frontend Dev",
|
|
107
|
+
visibleWhen: 'department = "eng"',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
value: "dev_be",
|
|
111
|
+
label: "Backend Dev",
|
|
112
|
+
visibleWhen: 'department = "eng"',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
value: "recruiter",
|
|
116
|
+
label: "Recruiter",
|
|
117
|
+
visibleWhen: 'department = "hr"',
|
|
118
|
+
},
|
|
98
119
|
],
|
|
99
120
|
},
|
|
100
121
|
},
|
|
@@ -102,11 +123,14 @@ describe("getOptionsVisibility", () => {
|
|
|
102
123
|
|
|
103
124
|
// Engineering department
|
|
104
125
|
const engResult = getOptionsVisibility({ department: "eng" }, spec);
|
|
105
|
-
expect(engResult["position"].map(o => o.value)).toEqual([
|
|
126
|
+
expect(engResult["position"].map((o) => o.value)).toEqual([
|
|
127
|
+
"dev_fe",
|
|
128
|
+
"dev_be",
|
|
129
|
+
]);
|
|
106
130
|
|
|
107
131
|
// HR department
|
|
108
132
|
const hrResult = getOptionsVisibility({ department: "hr" }, spec);
|
|
109
|
-
expect(hrResult["position"].map(o => o.value)).toEqual(["recruiter"]);
|
|
133
|
+
expect(hrResult["position"].map((o) => o.value)).toEqual(["recruiter"]);
|
|
110
134
|
});
|
|
111
135
|
|
|
112
136
|
it("should use computed values in expressions", () => {
|
|
@@ -116,7 +140,11 @@ describe("getOptionsVisibility", () => {
|
|
|
116
140
|
type: "select",
|
|
117
141
|
options: [
|
|
118
142
|
{ value: "standard", label: "Standard" },
|
|
119
|
-
{
|
|
143
|
+
{
|
|
144
|
+
value: "express",
|
|
145
|
+
label: "Express",
|
|
146
|
+
visibleWhen: "computed.total >= 50",
|
|
147
|
+
},
|
|
120
148
|
],
|
|
121
149
|
},
|
|
122
150
|
},
|
|
@@ -127,11 +155,14 @@ describe("getOptionsVisibility", () => {
|
|
|
127
155
|
|
|
128
156
|
// Total = 40, only standard
|
|
129
157
|
const lowResult = getOptionsVisibility({ quantity: 2, price: 20 }, spec);
|
|
130
|
-
expect(lowResult["shipping"].map(o => o.value)).toEqual(["standard"]);
|
|
158
|
+
expect(lowResult["shipping"].map((o) => o.value)).toEqual(["standard"]);
|
|
131
159
|
|
|
132
160
|
// Total = 100, both options
|
|
133
161
|
const highResult = getOptionsVisibility({ quantity: 5, price: 20 }, spec);
|
|
134
|
-
expect(highResult["shipping"].map(o => o.value)).toEqual([
|
|
162
|
+
expect(highResult["shipping"].map((o) => o.value)).toEqual([
|
|
163
|
+
"standard",
|
|
164
|
+
"express",
|
|
165
|
+
]);
|
|
135
166
|
});
|
|
136
167
|
|
|
137
168
|
it("should accept pre-computed values", () => {
|
|
@@ -141,14 +172,20 @@ describe("getOptionsVisibility", () => {
|
|
|
141
172
|
type: "select",
|
|
142
173
|
options: [
|
|
143
174
|
{ value: "basic", label: "Basic" },
|
|
144
|
-
{
|
|
175
|
+
{
|
|
176
|
+
value: "premium",
|
|
177
|
+
label: "Premium",
|
|
178
|
+
visibleWhen: "computed.score >= 100",
|
|
179
|
+
},
|
|
145
180
|
],
|
|
146
181
|
},
|
|
147
182
|
},
|
|
148
183
|
});
|
|
149
184
|
|
|
150
|
-
const result = getOptionsVisibility({}, spec, {
|
|
151
|
-
|
|
185
|
+
const result = getOptionsVisibility({}, spec, {
|
|
186
|
+
computed: { score: 150 },
|
|
187
|
+
});
|
|
188
|
+
expect(result["tier"].map((o) => o.value)).toEqual(["basic", "premium"]);
|
|
152
189
|
});
|
|
153
190
|
});
|
|
154
191
|
|
|
@@ -169,8 +206,16 @@ describe("getOptionsVisibility", () => {
|
|
|
169
206
|
addon: {
|
|
170
207
|
type: "select",
|
|
171
208
|
options: [
|
|
172
|
-
{
|
|
173
|
-
|
|
209
|
+
{
|
|
210
|
+
value: "warranty",
|
|
211
|
+
label: "Warranty",
|
|
212
|
+
visibleWhen: 'item.category = "electronics"',
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
value: "gift_wrap",
|
|
216
|
+
label: "Gift Wrap",
|
|
217
|
+
visibleWhen: 'item.category = "clothing"',
|
|
218
|
+
},
|
|
174
219
|
{ value: "insurance", label: "Insurance" },
|
|
175
220
|
],
|
|
176
221
|
},
|
|
@@ -180,19 +225,22 @@ describe("getOptionsVisibility", () => {
|
|
|
180
225
|
});
|
|
181
226
|
|
|
182
227
|
const data = {
|
|
183
|
-
items: [
|
|
184
|
-
{ category: "electronics" },
|
|
185
|
-
{ category: "clothing" },
|
|
186
|
-
],
|
|
228
|
+
items: [{ category: "electronics" }, { category: "clothing" }],
|
|
187
229
|
};
|
|
188
230
|
|
|
189
231
|
const result = getOptionsVisibility(data, spec);
|
|
190
232
|
|
|
191
233
|
// First item (electronics): warranty + insurance
|
|
192
|
-
expect(result["items[0].addon"].map(o => o.value)).toEqual([
|
|
234
|
+
expect(result["items[0].addon"].map((o) => o.value)).toEqual([
|
|
235
|
+
"warranty",
|
|
236
|
+
"insurance",
|
|
237
|
+
]);
|
|
193
238
|
|
|
194
239
|
// Second item (clothing): gift_wrap + insurance
|
|
195
|
-
expect(result["items[1].addon"].map(o => o.value)).toEqual([
|
|
240
|
+
expect(result["items[1].addon"].map((o) => o.value)).toEqual([
|
|
241
|
+
"gift_wrap",
|
|
242
|
+
"insurance",
|
|
243
|
+
]);
|
|
196
244
|
|
|
197
245
|
// Category field has all options (no visibleWhen)
|
|
198
246
|
expect(result["items[0].category"]).toHaveLength(2);
|
|
@@ -209,7 +257,11 @@ describe("getOptionsVisibility", () => {
|
|
|
209
257
|
type: "select",
|
|
210
258
|
options: [
|
|
211
259
|
{ value: "member", label: "Member" },
|
|
212
|
-
{
|
|
260
|
+
{
|
|
261
|
+
value: "lead",
|
|
262
|
+
label: "Team Lead",
|
|
263
|
+
visibleWhen: "itemIndex = 0",
|
|
264
|
+
},
|
|
213
265
|
],
|
|
214
266
|
},
|
|
215
267
|
},
|
|
@@ -224,11 +276,14 @@ describe("getOptionsVisibility", () => {
|
|
|
224
276
|
const result = getOptionsVisibility(data, spec);
|
|
225
277
|
|
|
226
278
|
// First item can be lead
|
|
227
|
-
expect(result["members[0].role"].map(o => o.value)).toEqual([
|
|
279
|
+
expect(result["members[0].role"].map((o) => o.value)).toEqual([
|
|
280
|
+
"member",
|
|
281
|
+
"lead",
|
|
282
|
+
]);
|
|
228
283
|
|
|
229
284
|
// Other items can only be member
|
|
230
|
-
expect(result["members[1].role"].map(o => o.value)).toEqual(["member"]);
|
|
231
|
-
expect(result["members[2].role"].map(o => o.value)).toEqual(["member"]);
|
|
285
|
+
expect(result["members[1].role"].map((o) => o.value)).toEqual(["member"]);
|
|
286
|
+
expect(result["members[2].role"].map((o) => o.value)).toEqual(["member"]);
|
|
232
287
|
});
|
|
233
288
|
|
|
234
289
|
it("should combine form data with item context", () => {
|
|
@@ -242,8 +297,16 @@ describe("getOptionsVisibility", () => {
|
|
|
242
297
|
type: "select",
|
|
243
298
|
options: [
|
|
244
299
|
{ value: "standard", label: "Standard" },
|
|
245
|
-
{
|
|
246
|
-
|
|
300
|
+
{
|
|
301
|
+
value: "express",
|
|
302
|
+
label: "Express",
|
|
303
|
+
visibleWhen: "isPremium = true",
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
value: "priority",
|
|
307
|
+
label: "Priority",
|
|
308
|
+
visibleWhen: "isPremium = true and item.value > 100",
|
|
309
|
+
},
|
|
247
310
|
],
|
|
248
311
|
},
|
|
249
312
|
},
|
|
@@ -252,25 +315,42 @@ describe("getOptionsVisibility", () => {
|
|
|
252
315
|
});
|
|
253
316
|
|
|
254
317
|
// Not premium
|
|
255
|
-
const basicResult = getOptionsVisibility(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
318
|
+
const basicResult = getOptionsVisibility(
|
|
319
|
+
{
|
|
320
|
+
isPremium: false,
|
|
321
|
+
orders: [{ value: 50 }, { value: 200 }],
|
|
322
|
+
},
|
|
323
|
+
spec,
|
|
324
|
+
);
|
|
259
325
|
|
|
260
|
-
expect(basicResult["orders[0].shipping"].map(o => o.value)).toEqual([
|
|
261
|
-
|
|
326
|
+
expect(basicResult["orders[0].shipping"].map((o) => o.value)).toEqual([
|
|
327
|
+
"standard",
|
|
328
|
+
]);
|
|
329
|
+
expect(basicResult["orders[1].shipping"].map((o) => o.value)).toEqual([
|
|
330
|
+
"standard",
|
|
331
|
+
]);
|
|
262
332
|
|
|
263
333
|
// Premium with different order values
|
|
264
|
-
const premiumResult = getOptionsVisibility(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
334
|
+
const premiumResult = getOptionsVisibility(
|
|
335
|
+
{
|
|
336
|
+
isPremium: true,
|
|
337
|
+
orders: [{ value: 50 }, { value: 200 }],
|
|
338
|
+
},
|
|
339
|
+
spec,
|
|
340
|
+
);
|
|
268
341
|
|
|
269
342
|
// Low value order: standard + express
|
|
270
|
-
expect(premiumResult["orders[0].shipping"].map(o => o.value)).toEqual([
|
|
343
|
+
expect(premiumResult["orders[0].shipping"].map((o) => o.value)).toEqual([
|
|
344
|
+
"standard",
|
|
345
|
+
"express",
|
|
346
|
+
]);
|
|
271
347
|
|
|
272
348
|
// High value order: standard + express + priority
|
|
273
|
-
expect(premiumResult["orders[1].shipping"].map(o => o.value)).toEqual([
|
|
349
|
+
expect(premiumResult["orders[1].shipping"].map((o) => o.value)).toEqual([
|
|
350
|
+
"standard",
|
|
351
|
+
"express",
|
|
352
|
+
"priority",
|
|
353
|
+
]);
|
|
274
354
|
});
|
|
275
355
|
|
|
276
356
|
it("should handle empty arrays", () => {
|
|
@@ -291,7 +371,9 @@ describe("getOptionsVisibility", () => {
|
|
|
291
371
|
const result = getOptionsVisibility({ items: [] }, spec);
|
|
292
372
|
|
|
293
373
|
// No item paths should exist
|
|
294
|
-
expect(
|
|
374
|
+
expect(
|
|
375
|
+
Object.keys(result).filter((k) => k.startsWith("items[")),
|
|
376
|
+
).toHaveLength(0);
|
|
295
377
|
});
|
|
296
378
|
|
|
297
379
|
it("should handle missing array data", () => {
|
|
@@ -312,7 +394,9 @@ describe("getOptionsVisibility", () => {
|
|
|
312
394
|
const result = getOptionsVisibility({}, spec);
|
|
313
395
|
|
|
314
396
|
// No item paths should exist
|
|
315
|
-
expect(
|
|
397
|
+
expect(
|
|
398
|
+
Object.keys(result).filter((k) => k.startsWith("items[")),
|
|
399
|
+
).toHaveLength(0);
|
|
316
400
|
});
|
|
317
401
|
});
|
|
318
402
|
|
|
@@ -324,7 +408,11 @@ describe("getOptionsVisibility", () => {
|
|
|
324
408
|
type: "select",
|
|
325
409
|
options: [
|
|
326
410
|
{ value: "valid", label: "Valid" },
|
|
327
|
-
{
|
|
411
|
+
{
|
|
412
|
+
value: "invalid",
|
|
413
|
+
label: "Invalid",
|
|
414
|
+
visibleWhen: "not valid FEEL !!!",
|
|
415
|
+
},
|
|
328
416
|
],
|
|
329
417
|
},
|
|
330
418
|
},
|
|
@@ -332,7 +420,7 @@ describe("getOptionsVisibility", () => {
|
|
|
332
420
|
|
|
333
421
|
const result = getOptionsVisibility({}, spec);
|
|
334
422
|
|
|
335
|
-
expect(result["status"].map(o => o.value)).toEqual(["valid"]);
|
|
423
|
+
expect(result["status"].map((o) => o.value)).toEqual(["valid"]);
|
|
336
424
|
});
|
|
337
425
|
});
|
|
338
426
|
|
|
@@ -344,7 +432,11 @@ describe("getOptionsVisibility", () => {
|
|
|
344
432
|
type: "select",
|
|
345
433
|
options: [
|
|
346
434
|
{ value: "basic", label: "Basic" },
|
|
347
|
-
{
|
|
435
|
+
{
|
|
436
|
+
value: "enterprise",
|
|
437
|
+
label: "Enterprise",
|
|
438
|
+
visibleWhen: "ref.features.enterpriseEnabled = true",
|
|
439
|
+
},
|
|
348
440
|
],
|
|
349
441
|
},
|
|
350
442
|
},
|
|
@@ -355,7 +447,10 @@ describe("getOptionsVisibility", () => {
|
|
|
355
447
|
|
|
356
448
|
const result = getOptionsVisibility({}, spec);
|
|
357
449
|
|
|
358
|
-
expect(result["plan"].map(o => o.value)).toEqual([
|
|
450
|
+
expect(result["plan"].map((o) => o.value)).toEqual([
|
|
451
|
+
"basic",
|
|
452
|
+
"enterprise",
|
|
453
|
+
]);
|
|
359
454
|
});
|
|
360
455
|
});
|
|
361
456
|
});
|
|
@@ -381,15 +476,21 @@ describe("getVisibleOptions", () => {
|
|
|
381
476
|
it("should filter options based on visibleWhen", () => {
|
|
382
477
|
const options: SelectOption[] = [
|
|
383
478
|
{ value: "always", label: "Always" },
|
|
384
|
-
{
|
|
479
|
+
{
|
|
480
|
+
value: "conditional",
|
|
481
|
+
label: "Conditional",
|
|
482
|
+
visibleWhen: "show = true",
|
|
483
|
+
},
|
|
385
484
|
];
|
|
386
485
|
const spec = createTestSpec();
|
|
387
486
|
|
|
388
|
-
expect(
|
|
389
|
-
.
|
|
487
|
+
expect(
|
|
488
|
+
getVisibleOptions(options, { show: false }, spec).map((o) => o.value),
|
|
489
|
+
).toEqual(["always"]);
|
|
390
490
|
|
|
391
|
-
expect(
|
|
392
|
-
|
|
491
|
+
expect(
|
|
492
|
+
getVisibleOptions(options, { show: true }, spec).map((o) => o.value),
|
|
493
|
+
).toEqual(["always", "conditional"]);
|
|
393
494
|
});
|
|
394
495
|
|
|
395
496
|
it("should return empty array for undefined/empty options", () => {
|
|
@@ -413,7 +514,11 @@ describe("getVisibleOptions", () => {
|
|
|
413
514
|
describe("with item context", () => {
|
|
414
515
|
it("should use item data in expressions", () => {
|
|
415
516
|
const options: SelectOption[] = [
|
|
416
|
-
{
|
|
517
|
+
{
|
|
518
|
+
value: "warranty",
|
|
519
|
+
label: "Warranty",
|
|
520
|
+
visibleWhen: 'item.type = "electronics"',
|
|
521
|
+
},
|
|
417
522
|
{ value: "shipping", label: "Shipping" },
|
|
418
523
|
];
|
|
419
524
|
const spec = createTestSpec();
|
|
@@ -421,12 +526,15 @@ describe("getVisibleOptions", () => {
|
|
|
421
526
|
const electronicsResult = getVisibleOptions(options, {}, spec, {
|
|
422
527
|
item: { type: "electronics" },
|
|
423
528
|
});
|
|
424
|
-
expect(electronicsResult.map(o => o.value)).toEqual([
|
|
529
|
+
expect(electronicsResult.map((o) => o.value)).toEqual([
|
|
530
|
+
"warranty",
|
|
531
|
+
"shipping",
|
|
532
|
+
]);
|
|
425
533
|
|
|
426
534
|
const otherResult = getVisibleOptions(options, {}, spec, {
|
|
427
535
|
item: { type: "clothing" },
|
|
428
536
|
});
|
|
429
|
-
expect(otherResult.map(o => o.value)).toEqual(["shipping"]);
|
|
537
|
+
expect(otherResult.map((o) => o.value)).toEqual(["shipping"]);
|
|
430
538
|
});
|
|
431
539
|
|
|
432
540
|
it("should use itemIndex in expressions", () => {
|
|
@@ -436,11 +544,17 @@ describe("getVisibleOptions", () => {
|
|
|
436
544
|
];
|
|
437
545
|
const spec = createTestSpec();
|
|
438
546
|
|
|
439
|
-
expect(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
547
|
+
expect(
|
|
548
|
+
getVisibleOptions(options, {}, spec, { itemIndex: 0, item: {} }).map(
|
|
549
|
+
(o) => o.value,
|
|
550
|
+
),
|
|
551
|
+
).toEqual(["lead", "member"]);
|
|
552
|
+
|
|
553
|
+
expect(
|
|
554
|
+
getVisibleOptions(options, {}, spec, { itemIndex: 1, item: {} }).map(
|
|
555
|
+
(o) => o.value,
|
|
556
|
+
),
|
|
557
|
+
).toEqual(["member"]);
|
|
444
558
|
});
|
|
445
559
|
});
|
|
446
560
|
|
|
@@ -453,14 +567,17 @@ describe("getVisibleOptions", () => {
|
|
|
453
567
|
];
|
|
454
568
|
const spec = createTestSpec();
|
|
455
569
|
|
|
456
|
-
expect(
|
|
457
|
-
.
|
|
570
|
+
expect(
|
|
571
|
+
getVisibleOptions(options, { years: 1 }, spec).map((o) => o.value),
|
|
572
|
+
).toEqual(["junior"]);
|
|
458
573
|
|
|
459
|
-
expect(
|
|
460
|
-
.
|
|
574
|
+
expect(
|
|
575
|
+
getVisibleOptions(options, { years: 5 }, spec).map((o) => o.value),
|
|
576
|
+
).toEqual(["mid"]);
|
|
461
577
|
|
|
462
|
-
expect(
|
|
463
|
-
.
|
|
578
|
+
expect(
|
|
579
|
+
getVisibleOptions(options, { years: 10 }, spec).map((o) => o.value),
|
|
580
|
+
).toEqual(["senior"]);
|
|
464
581
|
});
|
|
465
582
|
|
|
466
583
|
it("should evaluate string equality", () => {
|
|
@@ -470,8 +587,9 @@ describe("getVisibleOptions", () => {
|
|
|
470
587
|
];
|
|
471
588
|
const spec = createTestSpec();
|
|
472
589
|
|
|
473
|
-
expect(
|
|
474
|
-
|
|
590
|
+
expect(
|
|
591
|
+
getVisibleOptions(options, { type: "alpha" }, spec).map((o) => o.value),
|
|
592
|
+
).toEqual(["a"]);
|
|
475
593
|
});
|
|
476
594
|
|
|
477
595
|
it("should evaluate boolean expressions", () => {
|
|
@@ -481,11 +599,17 @@ describe("getVisibleOptions", () => {
|
|
|
481
599
|
];
|
|
482
600
|
const spec = createTestSpec();
|
|
483
601
|
|
|
484
|
-
expect(
|
|
485
|
-
.
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
602
|
+
expect(
|
|
603
|
+
getVisibleOptions(options, { isPremium: false }, spec).map(
|
|
604
|
+
(o) => o.value,
|
|
605
|
+
),
|
|
606
|
+
).toEqual(["basic"]);
|
|
607
|
+
|
|
608
|
+
expect(
|
|
609
|
+
getVisibleOptions(options, { isPremium: true }, spec).map(
|
|
610
|
+
(o) => o.value,
|
|
611
|
+
),
|
|
612
|
+
).toEqual(["premium", "basic"]);
|
|
489
613
|
});
|
|
490
614
|
});
|
|
491
615
|
|
|
@@ -497,11 +621,13 @@ describe("getVisibleOptions", () => {
|
|
|
497
621
|
];
|
|
498
622
|
const spec = createTestSpec();
|
|
499
623
|
|
|
500
|
-
expect(
|
|
501
|
-
|
|
624
|
+
expect(
|
|
625
|
+
getVisibleOptions(options, { level: 1 }, spec).map((o) => o.value),
|
|
626
|
+
).toEqual([1]);
|
|
502
627
|
|
|
503
|
-
expect(
|
|
504
|
-
|
|
628
|
+
expect(
|
|
629
|
+
getVisibleOptions(options, { level: 2 }, spec).map((o) => o.value),
|
|
630
|
+
).toEqual([1, 2]);
|
|
505
631
|
});
|
|
506
632
|
|
|
507
633
|
it("should preserve option order", () => {
|
|
@@ -512,19 +638,25 @@ describe("getVisibleOptions", () => {
|
|
|
512
638
|
];
|
|
513
639
|
const spec = createTestSpec();
|
|
514
640
|
|
|
515
|
-
expect(
|
|
516
|
-
|
|
641
|
+
expect(
|
|
642
|
+
getVisibleOptions(options, { show: true }, spec).map((o) => o.value),
|
|
643
|
+
).toEqual(["z", "a", "m"]);
|
|
517
644
|
});
|
|
518
645
|
|
|
519
646
|
it("should hide options with invalid FEEL", () => {
|
|
520
647
|
const options: SelectOption[] = [
|
|
521
648
|
{ value: "valid", label: "Valid" },
|
|
522
|
-
{
|
|
649
|
+
{
|
|
650
|
+
value: "invalid",
|
|
651
|
+
label: "Invalid",
|
|
652
|
+
visibleWhen: "this is broken !!!",
|
|
653
|
+
},
|
|
523
654
|
];
|
|
524
655
|
const spec = createTestSpec();
|
|
525
656
|
|
|
526
|
-
expect(getVisibleOptions(options, {}, spec).map(o => o.value))
|
|
527
|
-
|
|
657
|
+
expect(getVisibleOptions(options, {}, spec).map((o) => o.value)).toEqual([
|
|
658
|
+
"valid",
|
|
659
|
+
]);
|
|
528
660
|
});
|
|
529
661
|
});
|
|
530
662
|
});
|
package/src/engine/calculate.ts
CHANGED
|
@@ -48,7 +48,7 @@ import type {
|
|
|
48
48
|
*/
|
|
49
49
|
export function calculate(
|
|
50
50
|
data: Record<string, unknown>,
|
|
51
|
-
spec: Forma
|
|
51
|
+
spec: Forma,
|
|
52
52
|
): Record<string, unknown> {
|
|
53
53
|
const result = calculateWithErrors(data, spec);
|
|
54
54
|
return result.values;
|
|
@@ -65,7 +65,7 @@ export function calculate(
|
|
|
65
65
|
*/
|
|
66
66
|
export function calculateWithErrors(
|
|
67
67
|
data: Record<string, unknown>,
|
|
68
|
-
spec: Forma
|
|
68
|
+
spec: Forma,
|
|
69
69
|
): CalculationResult {
|
|
70
70
|
if (!spec.computed) {
|
|
71
71
|
return { values: {}, errors: [] };
|
|
@@ -87,7 +87,7 @@ export function calculateWithErrors(
|
|
|
87
87
|
fieldDef,
|
|
88
88
|
data,
|
|
89
89
|
values, // Pass already-computed values for dependencies
|
|
90
|
-
spec.referenceData // Pass reference data for lookups
|
|
90
|
+
spec.referenceData, // Pass reference data for lookups
|
|
91
91
|
);
|
|
92
92
|
|
|
93
93
|
if (result.success) {
|
|
@@ -130,7 +130,7 @@ function evaluateComputedField(
|
|
|
130
130
|
fieldDef: ComputedField,
|
|
131
131
|
data: Record<string, unknown>,
|
|
132
132
|
computedSoFar: Record<string, unknown>,
|
|
133
|
-
referenceData?: Record<string, unknown
|
|
133
|
+
referenceData?: Record<string, unknown>,
|
|
134
134
|
): ComputeResult {
|
|
135
135
|
// Check if any referenced computed field is null - propagate null to dependents
|
|
136
136
|
// This prevents issues like: bmi is null, but bmiCategory still evaluates to "obese"
|
|
@@ -162,7 +162,7 @@ function evaluateComputedField(
|
|
|
162
162
|
|
|
163
163
|
// Treat NaN and Infinity as null - prevents unexpected behavior in conditional expressions
|
|
164
164
|
// (e.g., NaN < 18.5 is false, causing fallthrough in if-else chains)
|
|
165
|
-
if (typeof result.value === "number" &&
|
|
165
|
+
if (typeof result.value === "number" && !Number.isFinite(result.value)) {
|
|
166
166
|
return {
|
|
167
167
|
success: true,
|
|
168
168
|
value: null,
|
|
@@ -200,14 +200,17 @@ function findComputedReferences(expression: string): string[] {
|
|
|
200
200
|
* evaluated first.
|
|
201
201
|
*/
|
|
202
202
|
function getComputationOrder(
|
|
203
|
-
computed: Record<string, ComputedField
|
|
203
|
+
computed: Record<string, ComputedField>,
|
|
204
204
|
): string[] {
|
|
205
205
|
const fieldNames = Object.keys(computed);
|
|
206
206
|
|
|
207
207
|
// Build dependency graph
|
|
208
208
|
const deps = new Map<string, Set<string>>();
|
|
209
209
|
for (const name of fieldNames) {
|
|
210
|
-
deps.set(
|
|
210
|
+
deps.set(
|
|
211
|
+
name,
|
|
212
|
+
findComputedDependencies(computed[name].expression, fieldNames),
|
|
213
|
+
);
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
// Topological sort
|
|
@@ -249,7 +252,7 @@ function getComputationOrder(
|
|
|
249
252
|
*/
|
|
250
253
|
function findComputedDependencies(
|
|
251
254
|
expression: string,
|
|
252
|
-
availableFields: string[]
|
|
255
|
+
availableFields: string[],
|
|
253
256
|
): Set<string> {
|
|
254
257
|
const deps = new Set<string>();
|
|
255
258
|
|
|
@@ -283,7 +286,7 @@ export function getFormattedValue(
|
|
|
283
286
|
fieldName: string,
|
|
284
287
|
data: Record<string, unknown>,
|
|
285
288
|
spec: Forma,
|
|
286
|
-
options?: FormatOptions
|
|
289
|
+
options?: FormatOptions,
|
|
287
290
|
): string | null {
|
|
288
291
|
if (!spec.computed?.[fieldName]) {
|
|
289
292
|
return null;
|
|
@@ -319,7 +322,7 @@ export function getFormattedValue(
|
|
|
319
322
|
export function calculateField(
|
|
320
323
|
fieldName: string,
|
|
321
324
|
data: Record<string, unknown>,
|
|
322
|
-
spec: Forma
|
|
325
|
+
spec: Forma,
|
|
323
326
|
): unknown {
|
|
324
327
|
const computed = calculate(data, spec);
|
|
325
328
|
return computed[fieldName] ?? null;
|