@fogpipe/forma-core 0.10.4 → 0.11.2
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/{chunk-7ANLNW3X.js → chunk-XS3BLVIZ.js} +65 -1
- package/dist/chunk-XS3BLVIZ.js.map +1 -0
- package/dist/engine/index.cjs +66 -0
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +5 -1
- package/dist/engine/visibility.d.ts +61 -9
- package/dist/engine/visibility.d.ts.map +1 -1
- package/dist/index.cjs +66 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -1
- package/package.json +1 -1
- package/src/__tests__/visibility.test.ts +530 -0
- package/src/engine/index.ts +3 -0
- package/src/engine/visibility.ts +183 -35
- package/dist/chunk-7ANLNW3X.js.map +0 -1
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for visibility engine - option visibility
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { getOptionsVisibility, getVisibleOptions } from "../engine/visibility.js";
|
|
7
|
+
import type { Forma, SelectOption } from "../types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Helper to create a minimal Forma spec for testing
|
|
11
|
+
*/
|
|
12
|
+
function createTestSpec(options: {
|
|
13
|
+
fields?: Record<string, unknown>;
|
|
14
|
+
computed?: Record<string, { expression: string }>;
|
|
15
|
+
referenceData?: Record<string, unknown>;
|
|
16
|
+
} = {}): Forma {
|
|
17
|
+
const { fields = {}, computed, referenceData } = options;
|
|
18
|
+
|
|
19
|
+
// Build fieldOrder from fields keys
|
|
20
|
+
const fieldOrder = Object.keys(fields);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
version: "1.0",
|
|
24
|
+
meta: { id: "test", title: "Test" },
|
|
25
|
+
schema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {},
|
|
28
|
+
},
|
|
29
|
+
fields: fields as Forma["fields"],
|
|
30
|
+
fieldOrder,
|
|
31
|
+
computed,
|
|
32
|
+
referenceData,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// getOptionsVisibility (Batch Computation)
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
describe("getOptionsVisibility", () => {
|
|
41
|
+
describe("top-level select fields", () => {
|
|
42
|
+
it("should compute visible options for all select fields", () => {
|
|
43
|
+
const spec = createTestSpec({
|
|
44
|
+
fields: {
|
|
45
|
+
department: {
|
|
46
|
+
type: "select",
|
|
47
|
+
options: [
|
|
48
|
+
{ value: "eng", label: "Engineering" },
|
|
49
|
+
{ value: "hr", label: "HR" },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
level: {
|
|
53
|
+
type: "select",
|
|
54
|
+
options: [
|
|
55
|
+
{ value: "junior", label: "Junior" },
|
|
56
|
+
{ value: "senior", label: "Senior", visibleWhen: "experience >= 5" },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result = getOptionsVisibility({ experience: 3 }, spec);
|
|
63
|
+
|
|
64
|
+
// Department has all options (no visibleWhen)
|
|
65
|
+
expect(result["department"]).toHaveLength(2);
|
|
66
|
+
|
|
67
|
+
// Level only shows junior (senior requires experience >= 5)
|
|
68
|
+
expect(result["level"]).toHaveLength(1);
|
|
69
|
+
expect(result["level"][0].value).toBe("junior");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should not include fields without options", () => {
|
|
73
|
+
const spec = createTestSpec({
|
|
74
|
+
fields: {
|
|
75
|
+
name: { type: "text", label: "Name" },
|
|
76
|
+
department: {
|
|
77
|
+
type: "select",
|
|
78
|
+
options: [{ value: "eng", label: "Engineering" }],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const result = getOptionsVisibility({}, spec);
|
|
84
|
+
|
|
85
|
+
expect(result["name"]).toBeUndefined();
|
|
86
|
+
expect(result["department"]).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should filter options based on form data", () => {
|
|
90
|
+
const spec = createTestSpec({
|
|
91
|
+
fields: {
|
|
92
|
+
position: {
|
|
93
|
+
type: "select",
|
|
94
|
+
options: [
|
|
95
|
+
{ value: "dev_fe", label: "Frontend Dev", visibleWhen: 'department = "eng"' },
|
|
96
|
+
{ value: "dev_be", label: "Backend Dev", visibleWhen: 'department = "eng"' },
|
|
97
|
+
{ value: "recruiter", label: "Recruiter", visibleWhen: 'department = "hr"' },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Engineering department
|
|
104
|
+
const engResult = getOptionsVisibility({ department: "eng" }, spec);
|
|
105
|
+
expect(engResult["position"].map(o => o.value)).toEqual(["dev_fe", "dev_be"]);
|
|
106
|
+
|
|
107
|
+
// HR department
|
|
108
|
+
const hrResult = getOptionsVisibility({ department: "hr" }, spec);
|
|
109
|
+
expect(hrResult["position"].map(o => o.value)).toEqual(["recruiter"]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should use computed values in expressions", () => {
|
|
113
|
+
const spec = createTestSpec({
|
|
114
|
+
fields: {
|
|
115
|
+
shipping: {
|
|
116
|
+
type: "select",
|
|
117
|
+
options: [
|
|
118
|
+
{ value: "standard", label: "Standard" },
|
|
119
|
+
{ value: "express", label: "Express", visibleWhen: "computed.total >= 50" },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
computed: {
|
|
124
|
+
total: { expression: "quantity * price" },
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Total = 40, only standard
|
|
129
|
+
const lowResult = getOptionsVisibility({ quantity: 2, price: 20 }, spec);
|
|
130
|
+
expect(lowResult["shipping"].map(o => o.value)).toEqual(["standard"]);
|
|
131
|
+
|
|
132
|
+
// Total = 100, both options
|
|
133
|
+
const highResult = getOptionsVisibility({ quantity: 5, price: 20 }, spec);
|
|
134
|
+
expect(highResult["shipping"].map(o => o.value)).toEqual(["standard", "express"]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should accept pre-computed values", () => {
|
|
138
|
+
const spec = createTestSpec({
|
|
139
|
+
fields: {
|
|
140
|
+
tier: {
|
|
141
|
+
type: "select",
|
|
142
|
+
options: [
|
|
143
|
+
{ value: "basic", label: "Basic" },
|
|
144
|
+
{ value: "premium", label: "Premium", visibleWhen: "computed.score >= 100" },
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const result = getOptionsVisibility({}, spec, { computed: { score: 150 } });
|
|
151
|
+
expect(result["tier"].map(o => o.value)).toEqual(["basic", "premium"]);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("array item select fields", () => {
|
|
156
|
+
it("should compute options for each array item", () => {
|
|
157
|
+
const spec = createTestSpec({
|
|
158
|
+
fields: {
|
|
159
|
+
items: {
|
|
160
|
+
type: "array",
|
|
161
|
+
itemFields: {
|
|
162
|
+
category: {
|
|
163
|
+
type: "select",
|
|
164
|
+
options: [
|
|
165
|
+
{ value: "electronics", label: "Electronics" },
|
|
166
|
+
{ value: "clothing", label: "Clothing" },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
addon: {
|
|
170
|
+
type: "select",
|
|
171
|
+
options: [
|
|
172
|
+
{ value: "warranty", label: "Warranty", visibleWhen: 'item.category = "electronics"' },
|
|
173
|
+
{ value: "gift_wrap", label: "Gift Wrap", visibleWhen: 'item.category = "clothing"' },
|
|
174
|
+
{ value: "insurance", label: "Insurance" },
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const data = {
|
|
183
|
+
items: [
|
|
184
|
+
{ category: "electronics" },
|
|
185
|
+
{ category: "clothing" },
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const result = getOptionsVisibility(data, spec);
|
|
190
|
+
|
|
191
|
+
// First item (electronics): warranty + insurance
|
|
192
|
+
expect(result["items[0].addon"].map(o => o.value)).toEqual(["warranty", "insurance"]);
|
|
193
|
+
|
|
194
|
+
// Second item (clothing): gift_wrap + insurance
|
|
195
|
+
expect(result["items[1].addon"].map(o => o.value)).toEqual(["gift_wrap", "insurance"]);
|
|
196
|
+
|
|
197
|
+
// Category field has all options (no visibleWhen)
|
|
198
|
+
expect(result["items[0].category"]).toHaveLength(2);
|
|
199
|
+
expect(result["items[1].category"]).toHaveLength(2);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("should use itemIndex in expressions", () => {
|
|
203
|
+
const spec = createTestSpec({
|
|
204
|
+
fields: {
|
|
205
|
+
members: {
|
|
206
|
+
type: "array",
|
|
207
|
+
itemFields: {
|
|
208
|
+
role: {
|
|
209
|
+
type: "select",
|
|
210
|
+
options: [
|
|
211
|
+
{ value: "member", label: "Member" },
|
|
212
|
+
{ value: "lead", label: "Team Lead", visibleWhen: "itemIndex = 0" },
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const data = {
|
|
221
|
+
members: [{}, {}, {}],
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const result = getOptionsVisibility(data, spec);
|
|
225
|
+
|
|
226
|
+
// First item can be lead
|
|
227
|
+
expect(result["members[0].role"].map(o => o.value)).toEqual(["member", "lead"]);
|
|
228
|
+
|
|
229
|
+
// 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"]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should combine form data with item context", () => {
|
|
235
|
+
const spec = createTestSpec({
|
|
236
|
+
fields: {
|
|
237
|
+
isPremium: { type: "boolean" },
|
|
238
|
+
orders: {
|
|
239
|
+
type: "array",
|
|
240
|
+
itemFields: {
|
|
241
|
+
shipping: {
|
|
242
|
+
type: "select",
|
|
243
|
+
options: [
|
|
244
|
+
{ value: "standard", label: "Standard" },
|
|
245
|
+
{ value: "express", label: "Express", visibleWhen: "isPremium = true" },
|
|
246
|
+
{ value: "priority", label: "Priority", visibleWhen: 'isPremium = true and item.value > 100' },
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Not premium
|
|
255
|
+
const basicResult = getOptionsVisibility({
|
|
256
|
+
isPremium: false,
|
|
257
|
+
orders: [{ value: 50 }, { value: 200 }],
|
|
258
|
+
}, spec);
|
|
259
|
+
|
|
260
|
+
expect(basicResult["orders[0].shipping"].map(o => o.value)).toEqual(["standard"]);
|
|
261
|
+
expect(basicResult["orders[1].shipping"].map(o => o.value)).toEqual(["standard"]);
|
|
262
|
+
|
|
263
|
+
// Premium with different order values
|
|
264
|
+
const premiumResult = getOptionsVisibility({
|
|
265
|
+
isPremium: true,
|
|
266
|
+
orders: [{ value: 50 }, { value: 200 }],
|
|
267
|
+
}, spec);
|
|
268
|
+
|
|
269
|
+
// Low value order: standard + express
|
|
270
|
+
expect(premiumResult["orders[0].shipping"].map(o => o.value)).toEqual(["standard", "express"]);
|
|
271
|
+
|
|
272
|
+
// High value order: standard + express + priority
|
|
273
|
+
expect(premiumResult["orders[1].shipping"].map(o => o.value)).toEqual(["standard", "express", "priority"]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("should handle empty arrays", () => {
|
|
277
|
+
const spec = createTestSpec({
|
|
278
|
+
fields: {
|
|
279
|
+
items: {
|
|
280
|
+
type: "array",
|
|
281
|
+
itemFields: {
|
|
282
|
+
type: {
|
|
283
|
+
type: "select",
|
|
284
|
+
options: [{ value: "a", label: "A" }],
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const result = getOptionsVisibility({ items: [] }, spec);
|
|
292
|
+
|
|
293
|
+
// No item paths should exist
|
|
294
|
+
expect(Object.keys(result).filter(k => k.startsWith("items["))).toHaveLength(0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should handle missing array data", () => {
|
|
298
|
+
const spec = createTestSpec({
|
|
299
|
+
fields: {
|
|
300
|
+
items: {
|
|
301
|
+
type: "array",
|
|
302
|
+
itemFields: {
|
|
303
|
+
type: {
|
|
304
|
+
type: "select",
|
|
305
|
+
options: [{ value: "a", label: "A" }],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const result = getOptionsVisibility({}, spec);
|
|
313
|
+
|
|
314
|
+
// No item paths should exist
|
|
315
|
+
expect(Object.keys(result).filter(k => k.startsWith("items["))).toHaveLength(0);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("error handling", () => {
|
|
320
|
+
it("should hide options with invalid FEEL expressions", () => {
|
|
321
|
+
const spec = createTestSpec({
|
|
322
|
+
fields: {
|
|
323
|
+
status: {
|
|
324
|
+
type: "select",
|
|
325
|
+
options: [
|
|
326
|
+
{ value: "valid", label: "Valid" },
|
|
327
|
+
{ value: "invalid", label: "Invalid", visibleWhen: "not valid FEEL !!!" },
|
|
328
|
+
],
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const result = getOptionsVisibility({}, spec);
|
|
334
|
+
|
|
335
|
+
expect(result["status"].map(o => o.value)).toEqual(["valid"]);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("reference data", () => {
|
|
340
|
+
it("should access reference data in expressions", () => {
|
|
341
|
+
const spec = createTestSpec({
|
|
342
|
+
fields: {
|
|
343
|
+
plan: {
|
|
344
|
+
type: "select",
|
|
345
|
+
options: [
|
|
346
|
+
{ value: "basic", label: "Basic" },
|
|
347
|
+
{ value: "enterprise", label: "Enterprise", visibleWhen: "ref.features.enterpriseEnabled = true" },
|
|
348
|
+
],
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
referenceData: {
|
|
352
|
+
features: { enterpriseEnabled: true },
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const result = getOptionsVisibility({}, spec);
|
|
357
|
+
|
|
358
|
+
expect(result["plan"].map(o => o.value)).toEqual(["basic", "enterprise"]);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ============================================================================
|
|
364
|
+
// getVisibleOptions (Individual Computation - Utility)
|
|
365
|
+
// ============================================================================
|
|
366
|
+
|
|
367
|
+
describe("getVisibleOptions", () => {
|
|
368
|
+
describe("basic filtering", () => {
|
|
369
|
+
it("should return all options when none have visibleWhen", () => {
|
|
370
|
+
const options: SelectOption[] = [
|
|
371
|
+
{ value: "a", label: "A" },
|
|
372
|
+
{ value: "b", label: "B" },
|
|
373
|
+
];
|
|
374
|
+
const spec = createTestSpec();
|
|
375
|
+
|
|
376
|
+
const result = getVisibleOptions(options, {}, spec);
|
|
377
|
+
|
|
378
|
+
expect(result).toEqual(options);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("should filter options based on visibleWhen", () => {
|
|
382
|
+
const options: SelectOption[] = [
|
|
383
|
+
{ value: "always", label: "Always" },
|
|
384
|
+
{ value: "conditional", label: "Conditional", visibleWhen: "show = true" },
|
|
385
|
+
];
|
|
386
|
+
const spec = createTestSpec();
|
|
387
|
+
|
|
388
|
+
expect(getVisibleOptions(options, { show: false }, spec).map(o => o.value))
|
|
389
|
+
.toEqual(["always"]);
|
|
390
|
+
|
|
391
|
+
expect(getVisibleOptions(options, { show: true }, spec).map(o => o.value))
|
|
392
|
+
.toEqual(["always", "conditional"]);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("should return empty array for undefined/empty options", () => {
|
|
396
|
+
const spec = createTestSpec();
|
|
397
|
+
|
|
398
|
+
expect(getVisibleOptions(undefined, {}, spec)).toEqual([]);
|
|
399
|
+
expect(getVisibleOptions([], {}, spec)).toEqual([]);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("should return empty array when all options hidden", () => {
|
|
403
|
+
const options: SelectOption[] = [
|
|
404
|
+
{ value: "a", label: "A", visibleWhen: "show = true" },
|
|
405
|
+
{ value: "b", label: "B", visibleWhen: "show = true" },
|
|
406
|
+
];
|
|
407
|
+
const spec = createTestSpec();
|
|
408
|
+
|
|
409
|
+
expect(getVisibleOptions(options, { show: false }, spec)).toEqual([]);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe("with item context", () => {
|
|
414
|
+
it("should use item data in expressions", () => {
|
|
415
|
+
const options: SelectOption[] = [
|
|
416
|
+
{ value: "warranty", label: "Warranty", visibleWhen: 'item.type = "electronics"' },
|
|
417
|
+
{ value: "shipping", label: "Shipping" },
|
|
418
|
+
];
|
|
419
|
+
const spec = createTestSpec();
|
|
420
|
+
|
|
421
|
+
const electronicsResult = getVisibleOptions(options, {}, spec, {
|
|
422
|
+
item: { type: "electronics" },
|
|
423
|
+
});
|
|
424
|
+
expect(electronicsResult.map(o => o.value)).toEqual(["warranty", "shipping"]);
|
|
425
|
+
|
|
426
|
+
const otherResult = getVisibleOptions(options, {}, spec, {
|
|
427
|
+
item: { type: "clothing" },
|
|
428
|
+
});
|
|
429
|
+
expect(otherResult.map(o => o.value)).toEqual(["shipping"]);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("should use itemIndex in expressions", () => {
|
|
433
|
+
const options: SelectOption[] = [
|
|
434
|
+
{ value: "lead", label: "Lead", visibleWhen: "itemIndex = 0" },
|
|
435
|
+
{ value: "member", label: "Member" },
|
|
436
|
+
];
|
|
437
|
+
const spec = createTestSpec();
|
|
438
|
+
|
|
439
|
+
expect(getVisibleOptions(options, {}, spec, { itemIndex: 0, item: {} }).map(o => o.value))
|
|
440
|
+
.toEqual(["lead", "member"]);
|
|
441
|
+
|
|
442
|
+
expect(getVisibleOptions(options, {}, spec, { itemIndex: 1, item: {} }).map(o => o.value))
|
|
443
|
+
.toEqual(["member"]);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe("expression types", () => {
|
|
448
|
+
it("should evaluate numeric comparisons", () => {
|
|
449
|
+
const options: SelectOption[] = [
|
|
450
|
+
{ value: "junior", label: "Junior", visibleWhen: "years < 3" },
|
|
451
|
+
{ value: "mid", label: "Mid", visibleWhen: "years >= 3 and years < 7" },
|
|
452
|
+
{ value: "senior", label: "Senior", visibleWhen: "years >= 7" },
|
|
453
|
+
];
|
|
454
|
+
const spec = createTestSpec();
|
|
455
|
+
|
|
456
|
+
expect(getVisibleOptions(options, { years: 1 }, spec).map(o => o.value))
|
|
457
|
+
.toEqual(["junior"]);
|
|
458
|
+
|
|
459
|
+
expect(getVisibleOptions(options, { years: 5 }, spec).map(o => o.value))
|
|
460
|
+
.toEqual(["mid"]);
|
|
461
|
+
|
|
462
|
+
expect(getVisibleOptions(options, { years: 10 }, spec).map(o => o.value))
|
|
463
|
+
.toEqual(["senior"]);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("should evaluate string equality", () => {
|
|
467
|
+
const options: SelectOption[] = [
|
|
468
|
+
{ value: "a", label: "A", visibleWhen: 'type = "alpha"' },
|
|
469
|
+
{ value: "b", label: "B", visibleWhen: 'type = "beta"' },
|
|
470
|
+
];
|
|
471
|
+
const spec = createTestSpec();
|
|
472
|
+
|
|
473
|
+
expect(getVisibleOptions(options, { type: "alpha" }, spec).map(o => o.value))
|
|
474
|
+
.toEqual(["a"]);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("should evaluate boolean expressions", () => {
|
|
478
|
+
const options: SelectOption[] = [
|
|
479
|
+
{ value: "premium", label: "Premium", visibleWhen: "isPremium = true" },
|
|
480
|
+
{ value: "basic", label: "Basic" },
|
|
481
|
+
];
|
|
482
|
+
const spec = createTestSpec();
|
|
483
|
+
|
|
484
|
+
expect(getVisibleOptions(options, { isPremium: false }, spec).map(o => o.value))
|
|
485
|
+
.toEqual(["basic"]);
|
|
486
|
+
|
|
487
|
+
expect(getVisibleOptions(options, { isPremium: true }, spec).map(o => o.value))
|
|
488
|
+
.toEqual(["premium", "basic"]);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
describe("edge cases", () => {
|
|
493
|
+
it("should handle numeric option values", () => {
|
|
494
|
+
const options: SelectOption[] = [
|
|
495
|
+
{ value: 1, label: "One" },
|
|
496
|
+
{ value: 2, label: "Two", visibleWhen: "level >= 2" },
|
|
497
|
+
];
|
|
498
|
+
const spec = createTestSpec();
|
|
499
|
+
|
|
500
|
+
expect(getVisibleOptions(options, { level: 1 }, spec).map(o => o.value))
|
|
501
|
+
.toEqual([1]);
|
|
502
|
+
|
|
503
|
+
expect(getVisibleOptions(options, { level: 2 }, spec).map(o => o.value))
|
|
504
|
+
.toEqual([1, 2]);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("should preserve option order", () => {
|
|
508
|
+
const options: SelectOption[] = [
|
|
509
|
+
{ value: "z", label: "Z", visibleWhen: "show = true" },
|
|
510
|
+
{ value: "a", label: "A", visibleWhen: "show = true" },
|
|
511
|
+
{ value: "m", label: "M", visibleWhen: "show = true" },
|
|
512
|
+
];
|
|
513
|
+
const spec = createTestSpec();
|
|
514
|
+
|
|
515
|
+
expect(getVisibleOptions(options, { show: true }, spec).map(o => o.value))
|
|
516
|
+
.toEqual(["z", "a", "m"]);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("should hide options with invalid FEEL", () => {
|
|
520
|
+
const options: SelectOption[] = [
|
|
521
|
+
{ value: "valid", label: "Valid" },
|
|
522
|
+
{ value: "invalid", label: "Invalid", visibleWhen: "this is broken !!!" },
|
|
523
|
+
];
|
|
524
|
+
const spec = createTestSpec();
|
|
525
|
+
|
|
526
|
+
expect(getVisibleOptions(options, {}, spec).map(o => o.value))
|
|
527
|
+
.toEqual(["valid"]);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
});
|
package/src/engine/index.ts
CHANGED
|
@@ -28,10 +28,13 @@ export {
|
|
|
28
28
|
getVisibility,
|
|
29
29
|
isFieldVisible,
|
|
30
30
|
getPageVisibility,
|
|
31
|
+
getOptionsVisibility,
|
|
32
|
+
getVisibleOptions,
|
|
31
33
|
} from "./visibility.js";
|
|
32
34
|
|
|
33
35
|
export type {
|
|
34
36
|
VisibilityOptions,
|
|
37
|
+
OptionsVisibilityResult,
|
|
35
38
|
} from "./visibility.js";
|
|
36
39
|
|
|
37
40
|
// Required
|