@morscherlab/mld-sdk 0.6.5 → 0.7.1
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/__tests__/composables/formBuilderRegistry.test.d.ts +1 -0
- package/dist/__tests__/composables/useFormBuilder.test.d.ts +1 -0
- package/dist/components/BaseButton.vue.d.ts +1 -1
- package/dist/components/BasePill.vue.d.ts +1 -1
- package/dist/components/DropdownButton.vue.d.ts +1 -1
- package/dist/components/FormActions.vue.d.ts +33 -0
- package/dist/components/FormActions.vue.js +76 -0
- package/dist/components/FormActions.vue.js.map +1 -0
- package/dist/components/FormActions.vue3.js +6 -0
- package/dist/components/FormActions.vue3.js.map +1 -0
- package/dist/components/FormBuilder.vue.js +205 -0
- package/dist/components/FormBuilder.vue.js.map +1 -0
- package/dist/components/FormBuilder.vue3.js +6 -0
- package/dist/components/FormBuilder.vue3.js.map +1 -0
- package/dist/components/FormFieldRenderer.vue.d.ts +31 -0
- package/dist/components/FormFieldRenderer.vue.js +48 -0
- package/dist/components/FormFieldRenderer.vue.js.map +1 -0
- package/dist/components/FormFieldRenderer.vue2.js +5 -0
- package/dist/components/FormFieldRenderer.vue2.js.map +1 -0
- package/dist/components/FormSection.vue.d.ts +43 -0
- package/dist/components/FormSection.vue.js +117 -0
- package/dist/components/FormSection.vue.js.map +1 -0
- package/dist/components/FormSection.vue3.js +6 -0
- package/dist/components/FormSection.vue3.js.map +1 -0
- package/dist/components/IconButton.vue.d.ts +1 -1
- package/dist/components/LoadingSpinner.vue.d.ts +1 -1
- package/dist/components/ProgressBar.vue.d.ts +1 -1
- package/dist/components/ReagentList.vue.d.ts +2 -2
- package/dist/components/ResourceCard.vue.d.ts +1 -1
- package/dist/components/SegmentedControl.vue.d.ts +1 -1
- package/dist/components/WellEditPopup.vue.d.ts +2 -2
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +19 -8
- package/dist/components/index.js.map +1 -1
- package/dist/composables/formBuilderRegistry.d.ts +13 -0
- package/dist/composables/formBuilderRegistry.js +87 -0
- package/dist/composables/formBuilderRegistry.js.map +1 -0
- package/dist/composables/index.d.ts +3 -0
- package/dist/composables/index.js +8 -0
- package/dist/composables/index.js.map +1 -1
- package/dist/composables/useFormBuilder.d.ts +23 -0
- package/dist/composables/useFormBuilder.js +264 -0
- package/dist/composables/useFormBuilder.js.map +1 -0
- package/dist/composables/usePluginConfig.d.ts +12 -0
- package/dist/composables/usePluginConfig.js +77 -0
- package/dist/composables/usePluginConfig.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/styles.css +247 -6
- package/dist/types/form-builder.d.ts +167 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/__tests__/composables/formBuilderRegistry.test.ts +187 -0
- package/src/__tests__/composables/useFormBuilder.test.ts +917 -0
- package/src/components/FormActions.vue +92 -0
- package/src/components/FormBuilder.vue +214 -0
- package/src/components/FormFieldRenderer.vue +58 -0
- package/src/components/FormSection.vue +90 -0
- package/src/components/index.ts +6 -0
- package/src/composables/formBuilderRegistry.ts +79 -0
- package/src/composables/index.ts +7 -0
- package/src/composables/useFormBuilder.ts +382 -0
- package/src/composables/usePluginConfig.ts +92 -0
- package/src/index.ts +3 -0
- package/src/styles/components/app-container.css +1 -0
- package/src/styles/components/app-layout.css +1 -2
- package/src/styles/components/form-builder.css +69 -0
- package/src/styles/components/number-input.css +4 -1
- package/src/styles/index.css +1 -0
- package/src/types/form-builder.ts +197 -0
- package/src/types/index.ts +14 -0
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// Mock vue lifecycle hooks since we're not in a component context
|
|
4
|
+
vi.mock('vue', async () => {
|
|
5
|
+
const actual = await vi.importActual('vue')
|
|
6
|
+
return {
|
|
7
|
+
...actual,
|
|
8
|
+
onMounted: vi.fn(),
|
|
9
|
+
onUnmounted: vi.fn(),
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
import { evaluateCondition, useFormBuilder } from '../../composables/useFormBuilder'
|
|
14
|
+
import type {
|
|
15
|
+
FieldCondition,
|
|
16
|
+
FormSchema,
|
|
17
|
+
FormSectionSchema,
|
|
18
|
+
FormStepSchema,
|
|
19
|
+
} from '../../types/form-builder'
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
function makeSection(fields: FormSectionSchema['fields'], id = 'section1'): FormSectionSchema {
|
|
26
|
+
return { id, title: 'Section', fields }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeSchema(fields: FormSectionSchema['fields']): FormSchema {
|
|
30
|
+
return { sections: [makeSection(fields)] }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// evaluateCondition
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
describe('evaluateCondition', () => {
|
|
38
|
+
describe('comparison operators', () => {
|
|
39
|
+
it('eq: returns true when field equals value', () => {
|
|
40
|
+
const condition: FieldCondition = { field: 'x', eq: 'hello' }
|
|
41
|
+
expect(evaluateCondition(condition, { x: 'hello' })).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('eq: returns false when field does not equal value', () => {
|
|
45
|
+
const condition: FieldCondition = { field: 'x', eq: 'hello' }
|
|
46
|
+
expect(evaluateCondition(condition, { x: 'world' })).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('eq: performs strict equality (no coercion)', () => {
|
|
50
|
+
const condition: FieldCondition = { field: 'x', eq: 1 }
|
|
51
|
+
expect(evaluateCondition(condition, { x: '1' })).toBe(false)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('neq: returns true when field does not equal value', () => {
|
|
55
|
+
const condition: FieldCondition = { field: 'x', neq: 'a' }
|
|
56
|
+
expect(evaluateCondition(condition, { x: 'b' })).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('neq: returns false when field equals value', () => {
|
|
60
|
+
const condition: FieldCondition = { field: 'x', neq: 'a' }
|
|
61
|
+
expect(evaluateCondition(condition, { x: 'a' })).toBe(false)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('gt: returns true when number field is greater than threshold', () => {
|
|
65
|
+
const condition: FieldCondition = { field: 'n', gt: 5 }
|
|
66
|
+
expect(evaluateCondition(condition, { n: 6 })).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('gt: returns false when number field equals threshold', () => {
|
|
70
|
+
const condition: FieldCondition = { field: 'n', gt: 5 }
|
|
71
|
+
expect(evaluateCondition(condition, { n: 5 })).toBe(false)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('gt: returns false for non-numeric field', () => {
|
|
75
|
+
const condition: FieldCondition = { field: 'n', gt: 5 }
|
|
76
|
+
expect(evaluateCondition(condition, { n: 'six' })).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('lt: returns true when field is less than threshold', () => {
|
|
80
|
+
const condition: FieldCondition = { field: 'n', lt: 10 }
|
|
81
|
+
expect(evaluateCondition(condition, { n: 9 })).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('lt: returns false when field equals threshold', () => {
|
|
85
|
+
const condition: FieldCondition = { field: 'n', lt: 10 }
|
|
86
|
+
expect(evaluateCondition(condition, { n: 10 })).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('gte: returns true when field equals threshold', () => {
|
|
90
|
+
const condition: FieldCondition = { field: 'n', gte: 5 }
|
|
91
|
+
expect(evaluateCondition(condition, { n: 5 })).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('gte: returns true when field exceeds threshold', () => {
|
|
95
|
+
const condition: FieldCondition = { field: 'n', gte: 5 }
|
|
96
|
+
expect(evaluateCondition(condition, { n: 6 })).toBe(true)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('gte: returns false when field is below threshold', () => {
|
|
100
|
+
const condition: FieldCondition = { field: 'n', gte: 5 }
|
|
101
|
+
expect(evaluateCondition(condition, { n: 4 })).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('lte: returns true when field equals threshold', () => {
|
|
105
|
+
const condition: FieldCondition = { field: 'n', lte: 5 }
|
|
106
|
+
expect(evaluateCondition(condition, { n: 5 })).toBe(true)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('lte: returns false when field exceeds threshold', () => {
|
|
110
|
+
const condition: FieldCondition = { field: 'n', lte: 5 }
|
|
111
|
+
expect(evaluateCondition(condition, { n: 6 })).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('in: returns true when field value is in the list', () => {
|
|
115
|
+
const condition: FieldCondition = { field: 'x', in: ['a', 'b', 'c'] }
|
|
116
|
+
expect(evaluateCondition(condition, { x: 'b' })).toBe(true)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('in: returns false when field value is not in the list', () => {
|
|
120
|
+
const condition: FieldCondition = { field: 'x', in: ['a', 'b', 'c'] }
|
|
121
|
+
expect(evaluateCondition(condition, { x: 'd' })).toBe(false)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('notIn: returns true when field value is absent from the list', () => {
|
|
125
|
+
const condition: FieldCondition = { field: 'x', notIn: ['a', 'b'] }
|
|
126
|
+
expect(evaluateCondition(condition, { x: 'c' })).toBe(true)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('notIn: returns false when field value is in the list', () => {
|
|
130
|
+
const condition: FieldCondition = { field: 'x', notIn: ['a', 'b'] }
|
|
131
|
+
expect(evaluateCondition(condition, { x: 'a' })).toBe(false)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('truthy: returns true for truthy field value', () => {
|
|
135
|
+
const condition: FieldCondition = { field: 'x', truthy: true }
|
|
136
|
+
expect(evaluateCondition(condition, { x: 'non-empty' })).toBe(true)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('truthy: returns false for falsy field value', () => {
|
|
140
|
+
const condition: FieldCondition = { field: 'x', truthy: true }
|
|
141
|
+
expect(evaluateCondition(condition, { x: '' })).toBe(false)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('truthy: returns false when field is undefined', () => {
|
|
145
|
+
const condition: FieldCondition = { field: 'x', truthy: true }
|
|
146
|
+
expect(evaluateCondition(condition, {})).toBe(false)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('falsy: returns true for falsy field value', () => {
|
|
150
|
+
const condition: FieldCondition = { field: 'x', falsy: true }
|
|
151
|
+
expect(evaluateCondition(condition, { x: '' })).toBe(true)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('falsy: returns false for truthy field value', () => {
|
|
155
|
+
const condition: FieldCondition = { field: 'x', falsy: true }
|
|
156
|
+
expect(evaluateCondition(condition, { x: 'value' })).toBe(false)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('contains operator', () => {
|
|
161
|
+
it('returns true when string field contains substring', () => {
|
|
162
|
+
const condition: FieldCondition = { field: 'x', contains: 'foo' }
|
|
163
|
+
expect(evaluateCondition(condition, { x: 'foobar' })).toBe(true)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('returns false when string field does not contain substring', () => {
|
|
167
|
+
const condition: FieldCondition = { field: 'x', contains: 'baz' }
|
|
168
|
+
expect(evaluateCondition(condition, { x: 'foobar' })).toBe(false)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('returns true when array field contains the value', () => {
|
|
172
|
+
const condition: FieldCondition = { field: 'arr', contains: 'b' }
|
|
173
|
+
expect(evaluateCondition(condition, { arr: ['a', 'b', 'c'] })).toBe(true)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('returns false when array field does not contain the value', () => {
|
|
177
|
+
const condition: FieldCondition = { field: 'arr', contains: 'z' }
|
|
178
|
+
expect(evaluateCondition(condition, { arr: ['a', 'b', 'c'] })).toBe(false)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('returns false for non-string non-array field', () => {
|
|
182
|
+
const condition: FieldCondition = { field: 'x', contains: 'foo' }
|
|
183
|
+
expect(evaluateCondition(condition, { x: 42 })).toBe(false)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('logical operators', () => {
|
|
188
|
+
it('and: returns true when all sub-conditions pass', () => {
|
|
189
|
+
const condition: FieldCondition = {
|
|
190
|
+
and: [
|
|
191
|
+
{ field: 'a', eq: 1 },
|
|
192
|
+
{ field: 'b', eq: 2 },
|
|
193
|
+
],
|
|
194
|
+
}
|
|
195
|
+
expect(evaluateCondition(condition, { a: 1, b: 2 })).toBe(true)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('and: returns false when any sub-condition fails', () => {
|
|
199
|
+
const condition: FieldCondition = {
|
|
200
|
+
and: [
|
|
201
|
+
{ field: 'a', eq: 1 },
|
|
202
|
+
{ field: 'b', eq: 99 },
|
|
203
|
+
],
|
|
204
|
+
}
|
|
205
|
+
expect(evaluateCondition(condition, { a: 1, b: 2 })).toBe(false)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('and: returns true for empty sub-condition array', () => {
|
|
209
|
+
const condition: FieldCondition = { and: [] }
|
|
210
|
+
expect(evaluateCondition(condition, {})).toBe(true)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('or: returns true when any sub-condition passes', () => {
|
|
214
|
+
const condition: FieldCondition = {
|
|
215
|
+
or: [
|
|
216
|
+
{ field: 'a', eq: 99 },
|
|
217
|
+
{ field: 'b', eq: 2 },
|
|
218
|
+
],
|
|
219
|
+
}
|
|
220
|
+
expect(evaluateCondition(condition, { a: 1, b: 2 })).toBe(true)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('or: returns false when all sub-conditions fail', () => {
|
|
224
|
+
const condition: FieldCondition = {
|
|
225
|
+
or: [
|
|
226
|
+
{ field: 'a', eq: 99 },
|
|
227
|
+
{ field: 'b', eq: 99 },
|
|
228
|
+
],
|
|
229
|
+
}
|
|
230
|
+
expect(evaluateCondition(condition, { a: 1, b: 2 })).toBe(false)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('or: returns false for empty sub-condition array', () => {
|
|
234
|
+
const condition: FieldCondition = { or: [] }
|
|
235
|
+
expect(evaluateCondition(condition, {})).toBe(false)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('not: inverts a passing condition to false', () => {
|
|
239
|
+
const condition: FieldCondition = { not: { field: 'x', eq: 'yes' } }
|
|
240
|
+
expect(evaluateCondition(condition, { x: 'yes' })).toBe(false)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('not: inverts a failing condition to true', () => {
|
|
244
|
+
const condition: FieldCondition = { not: { field: 'x', eq: 'yes' } }
|
|
245
|
+
expect(evaluateCondition(condition, { x: 'no' })).toBe(true)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('supports nested and/or/not', () => {
|
|
249
|
+
// (a == 1 AND b == 2) OR (NOT c == 3)
|
|
250
|
+
const condition: FieldCondition = {
|
|
251
|
+
or: [
|
|
252
|
+
{ and: [{ field: 'a', eq: 1 }, { field: 'b', eq: 2 }] },
|
|
253
|
+
{ not: { field: 'c', eq: 3 } },
|
|
254
|
+
],
|
|
255
|
+
}
|
|
256
|
+
expect(evaluateCondition(condition, { a: 1, b: 2, c: 99 })).toBe(true)
|
|
257
|
+
expect(evaluateCondition(condition, { a: 0, b: 2, c: 3 })).toBe(false)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// useFormBuilder - initial values
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
describe('useFormBuilder', () => {
|
|
267
|
+
describe('initial values', () => {
|
|
268
|
+
it('should use schema defaultValue when no initialData is provided', () => {
|
|
269
|
+
const schema = makeSchema([
|
|
270
|
+
{ name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice' },
|
|
271
|
+
])
|
|
272
|
+
const { form } = useFormBuilder(schema)
|
|
273
|
+
expect(form.data.name).toBe('Alice')
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('should prefer initialData over schema defaultValue', () => {
|
|
277
|
+
const schema = makeSchema([
|
|
278
|
+
{ name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice' },
|
|
279
|
+
])
|
|
280
|
+
const { form } = useFormBuilder(schema, { name: 'Bob' })
|
|
281
|
+
expect(form.data.name).toBe('Bob')
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('should use getTypeDefault when no defaultValue and no initialData', () => {
|
|
285
|
+
const schema = makeSchema([
|
|
286
|
+
{ name: 'flag', label: 'Flag', type: 'checkbox' },
|
|
287
|
+
{ name: 'tags', label: 'Tags', type: 'tags' },
|
|
288
|
+
{ name: 'amount', label: 'Amount', type: 'number' },
|
|
289
|
+
{ name: 'text', label: 'Text', type: 'text' },
|
|
290
|
+
])
|
|
291
|
+
const { form } = useFormBuilder(schema)
|
|
292
|
+
expect(form.data.flag).toBe(false)
|
|
293
|
+
expect(form.data.tags).toEqual([])
|
|
294
|
+
expect(form.data.amount).toBeUndefined()
|
|
295
|
+
expect(form.data.text).toBe('')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('should initialise with initialData value of 0 (falsy override)', () => {
|
|
299
|
+
const schema = makeSchema([
|
|
300
|
+
{ name: 'count', label: 'Count', type: 'number', defaultValue: 10 },
|
|
301
|
+
])
|
|
302
|
+
const { form } = useFormBuilder(schema, { count: 0 })
|
|
303
|
+
expect(form.data.count).toBe(0)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('should initialise with initialData value of false (falsy override)', () => {
|
|
307
|
+
const schema = makeSchema([
|
|
308
|
+
{ name: 'enabled', label: 'Enabled', type: 'checkbox', defaultValue: true },
|
|
309
|
+
])
|
|
310
|
+
const { form } = useFormBuilder(schema, { enabled: false })
|
|
311
|
+
expect(form.data.enabled).toBe(false)
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// Field / section visibility
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
describe('isFieldVisible', () => {
|
|
320
|
+
it('returns true when field has no condition', () => {
|
|
321
|
+
const schema = makeSchema([{ name: 'name', label: 'Name', type: 'text' }])
|
|
322
|
+
const { isFieldVisible } = useFormBuilder(schema)
|
|
323
|
+
expect(isFieldVisible('name')).toBe(true)
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('returns false when field condition evaluates to false', () => {
|
|
327
|
+
const schema = makeSchema([
|
|
328
|
+
{ name: 'type', label: 'Type', type: 'text', defaultValue: 'basic' },
|
|
329
|
+
{
|
|
330
|
+
name: 'extra',
|
|
331
|
+
label: 'Extra',
|
|
332
|
+
type: 'text',
|
|
333
|
+
condition: { field: 'type', eq: 'advanced' },
|
|
334
|
+
},
|
|
335
|
+
])
|
|
336
|
+
const { isFieldVisible } = useFormBuilder(schema)
|
|
337
|
+
expect(isFieldVisible('extra')).toBe(false)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('returns true when field condition evaluates to true', () => {
|
|
341
|
+
const schema = makeSchema([
|
|
342
|
+
{ name: 'type', label: 'Type', type: 'text', defaultValue: 'advanced' },
|
|
343
|
+
{
|
|
344
|
+
name: 'extra',
|
|
345
|
+
label: 'Extra',
|
|
346
|
+
type: 'text',
|
|
347
|
+
condition: { field: 'type', eq: 'advanced' },
|
|
348
|
+
},
|
|
349
|
+
])
|
|
350
|
+
const { isFieldVisible } = useFormBuilder(schema)
|
|
351
|
+
expect(isFieldVisible('extra')).toBe(true)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('enhancement visible() overrides schema condition', () => {
|
|
355
|
+
const schema = makeSchema([
|
|
356
|
+
{ name: 'type', label: 'Type', type: 'text', defaultValue: 'basic' },
|
|
357
|
+
{
|
|
358
|
+
name: 'extra',
|
|
359
|
+
label: 'Extra',
|
|
360
|
+
type: 'text',
|
|
361
|
+
condition: { field: 'type', eq: 'advanced' },
|
|
362
|
+
},
|
|
363
|
+
])
|
|
364
|
+
const { isFieldVisible } = useFormBuilder(schema, undefined, {
|
|
365
|
+
fields: {
|
|
366
|
+
extra: { visible: () => true },
|
|
367
|
+
},
|
|
368
|
+
})
|
|
369
|
+
// Schema condition would say false, but enhancement says true
|
|
370
|
+
expect(isFieldVisible('extra')).toBe(true)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('returns false when unknown field name is queried (no schema match)', () => {
|
|
374
|
+
const schema = makeSchema([{ name: 'name', label: 'Name', type: 'text' }])
|
|
375
|
+
const { isFieldVisible } = useFormBuilder(schema)
|
|
376
|
+
// No field named 'ghost', no enhancement – falls through to return true
|
|
377
|
+
// (function returns true as the default when field has no condition)
|
|
378
|
+
expect(isFieldVisible('ghost')).toBe(true)
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
describe('isSectionVisible', () => {
|
|
383
|
+
it('returns true when section has no condition', () => {
|
|
384
|
+
const schema: FormSchema = {
|
|
385
|
+
sections: [
|
|
386
|
+
{ id: 'sec1', title: 'Section 1', fields: [] },
|
|
387
|
+
],
|
|
388
|
+
}
|
|
389
|
+
const { isSectionVisible } = useFormBuilder(schema)
|
|
390
|
+
expect(isSectionVisible('sec1')).toBe(true)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('returns false when section condition evaluates to false', () => {
|
|
394
|
+
const schema: FormSchema = {
|
|
395
|
+
sections: [
|
|
396
|
+
{
|
|
397
|
+
id: 'advanced',
|
|
398
|
+
title: 'Advanced',
|
|
399
|
+
condition: { field: 'mode', eq: 'expert' },
|
|
400
|
+
fields: [{ name: 'mode', label: 'Mode', type: 'text', defaultValue: 'simple' }],
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
}
|
|
404
|
+
const { isSectionVisible } = useFormBuilder(schema)
|
|
405
|
+
expect(isSectionVisible('advanced')).toBe(false)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('returns true when section condition evaluates to true', () => {
|
|
409
|
+
const schema: FormSchema = {
|
|
410
|
+
sections: [
|
|
411
|
+
{
|
|
412
|
+
id: 'advanced',
|
|
413
|
+
title: 'Advanced',
|
|
414
|
+
condition: { field: 'mode', eq: 'expert' },
|
|
415
|
+
fields: [{ name: 'mode', label: 'Mode', type: 'text', defaultValue: 'expert' }],
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
}
|
|
419
|
+
const { isSectionVisible } = useFormBuilder(schema)
|
|
420
|
+
expect(isSectionVisible('advanced')).toBe(true)
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('returns true for unknown section id', () => {
|
|
424
|
+
const schema: FormSchema = { sections: [] }
|
|
425
|
+
const { isSectionVisible } = useFormBuilder(schema)
|
|
426
|
+
expect(isSectionVisible('non-existent')).toBe(true)
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
// Validation rules
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
describe('validate', () => {
|
|
435
|
+
it('returns true when all required visible fields are filled', () => {
|
|
436
|
+
const schema = makeSchema([
|
|
437
|
+
{ name: 'name', label: 'Name', type: 'text', validation: { required: true } },
|
|
438
|
+
])
|
|
439
|
+
const { validate } = useFormBuilder(schema, { name: 'Alice' })
|
|
440
|
+
expect(validate()).toBe(true)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('returns false when a required visible field is empty', () => {
|
|
444
|
+
const schema = makeSchema([
|
|
445
|
+
{ name: 'name', label: 'Name', type: 'text', validation: { required: true } },
|
|
446
|
+
])
|
|
447
|
+
const { validate } = useFormBuilder(schema)
|
|
448
|
+
expect(validate()).toBe(false)
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('skips validation for hidden fields', () => {
|
|
452
|
+
// 'extra' is required but hidden — validation should still pass
|
|
453
|
+
const schema = makeSchema([
|
|
454
|
+
{ name: 'type', label: 'Type', type: 'text', defaultValue: 'basic' },
|
|
455
|
+
{
|
|
456
|
+
name: 'extra',
|
|
457
|
+
label: 'Extra',
|
|
458
|
+
type: 'text',
|
|
459
|
+
validation: { required: true },
|
|
460
|
+
condition: { field: 'type', eq: 'advanced' },
|
|
461
|
+
},
|
|
462
|
+
])
|
|
463
|
+
const { validate } = useFormBuilder(schema)
|
|
464
|
+
expect(validate()).toBe(true)
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
it('validates hidden-but-required field when it becomes visible', () => {
|
|
468
|
+
const schema = makeSchema([
|
|
469
|
+
{ name: 'type', label: 'Type', type: 'text', defaultValue: 'advanced' },
|
|
470
|
+
{
|
|
471
|
+
name: 'extra',
|
|
472
|
+
label: 'Extra',
|
|
473
|
+
type: 'text',
|
|
474
|
+
validation: { required: true },
|
|
475
|
+
condition: { field: 'type', eq: 'advanced' },
|
|
476
|
+
},
|
|
477
|
+
])
|
|
478
|
+
const { validate } = useFormBuilder(schema)
|
|
479
|
+
// 'extra' is visible and empty, so validation should fail
|
|
480
|
+
expect(validate()).toBe(false)
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it('converts string pattern to RegExp', () => {
|
|
484
|
+
const schema = makeSchema([
|
|
485
|
+
{
|
|
486
|
+
name: 'code',
|
|
487
|
+
label: 'Code',
|
|
488
|
+
type: 'text',
|
|
489
|
+
defaultValue: 'AB',
|
|
490
|
+
validation: { pattern: '^[A-Z]{3}$' },
|
|
491
|
+
},
|
|
492
|
+
])
|
|
493
|
+
const { validate, form } = useFormBuilder(schema)
|
|
494
|
+
// 'AB' does not match ^[A-Z]{3}$
|
|
495
|
+
expect(validate()).toBe(false)
|
|
496
|
+
form.data.code = 'ABC'
|
|
497
|
+
expect(validate()).toBe(true)
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it('converts object pattern to RegExp with custom message', () => {
|
|
501
|
+
const schema = makeSchema([
|
|
502
|
+
{
|
|
503
|
+
name: 'code',
|
|
504
|
+
label: 'Code',
|
|
505
|
+
type: 'text',
|
|
506
|
+
defaultValue: 'bad',
|
|
507
|
+
validation: { pattern: { value: '^[A-Z]{3}$', message: 'Must be 3 uppercase letters' } },
|
|
508
|
+
},
|
|
509
|
+
])
|
|
510
|
+
const { validate, form } = useFormBuilder(schema)
|
|
511
|
+
expect(validate()).toBe(false)
|
|
512
|
+
expect(form.errors.code).toBe('Must be 3 uppercase letters')
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('applies enhancement custom validator', () => {
|
|
516
|
+
const schema = makeSchema([
|
|
517
|
+
{ name: 'age', label: 'Age', type: 'number', defaultValue: 15 },
|
|
518
|
+
])
|
|
519
|
+
const { validate, form } = useFormBuilder(schema, undefined, {
|
|
520
|
+
fields: {
|
|
521
|
+
age: {
|
|
522
|
+
validate: (value) => (typeof value === 'number' && value < 18 ? 'Must be 18+' : undefined),
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
})
|
|
526
|
+
expect(validate()).toBe(false)
|
|
527
|
+
expect(form.errors.age).toBe('Must be 18+')
|
|
528
|
+
})
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
// Submit
|
|
533
|
+
// ---------------------------------------------------------------------------
|
|
534
|
+
|
|
535
|
+
describe('submit', () => {
|
|
536
|
+
it('calls onSubmit with visible field data only', async () => {
|
|
537
|
+
const onSubmit = vi.fn()
|
|
538
|
+
const schema = makeSchema([
|
|
539
|
+
{ name: 'type', label: 'Type', type: 'text', defaultValue: 'basic' },
|
|
540
|
+
{
|
|
541
|
+
name: 'extra',
|
|
542
|
+
label: 'Extra',
|
|
543
|
+
type: 'text',
|
|
544
|
+
defaultValue: 'secret',
|
|
545
|
+
condition: { field: 'type', eq: 'advanced' },
|
|
546
|
+
},
|
|
547
|
+
])
|
|
548
|
+
const { submit } = useFormBuilder(schema, undefined, { onSubmit })
|
|
549
|
+
await submit()
|
|
550
|
+
expect(onSubmit).toHaveBeenCalledOnce()
|
|
551
|
+
const submittedData = onSubmit.mock.calls[0][0] as Record<string, unknown>
|
|
552
|
+
expect(submittedData.type).toBe('basic')
|
|
553
|
+
// 'extra' is hidden (condition not met) — must not appear in submission
|
|
554
|
+
expect('extra' in submittedData).toBe(false)
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
it('does not call onSubmit when validation fails', async () => {
|
|
558
|
+
const onSubmit = vi.fn()
|
|
559
|
+
const schema = makeSchema([
|
|
560
|
+
{ name: 'name', label: 'Name', type: 'text', validation: { required: true } },
|
|
561
|
+
])
|
|
562
|
+
const { submit } = useFormBuilder(schema, undefined, { onSubmit })
|
|
563
|
+
await submit()
|
|
564
|
+
expect(onSubmit).not.toHaveBeenCalled()
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
it('applies transform before passing data to onSubmit', async () => {
|
|
568
|
+
const onSubmit = vi.fn()
|
|
569
|
+
const schema = makeSchema([
|
|
570
|
+
{ name: 'name', label: 'Name', type: 'text', defaultValue: 'alice' },
|
|
571
|
+
])
|
|
572
|
+
const { submit } = useFormBuilder(schema, undefined, {
|
|
573
|
+
transform: (data) => ({ ...data, name: (data.name as string).toUpperCase() }),
|
|
574
|
+
onSubmit,
|
|
575
|
+
})
|
|
576
|
+
await submit()
|
|
577
|
+
const submittedData = onSubmit.mock.calls[0][0] as Record<string, unknown>
|
|
578
|
+
expect(submittedData.name).toBe('ALICE')
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it('sets isSubmitting during onSubmit execution', async () => {
|
|
582
|
+
let observedSubmitting = false
|
|
583
|
+
const schema = makeSchema([{ name: 'n', label: 'N', type: 'text', defaultValue: 'x' }])
|
|
584
|
+
const { submit, form } = useFormBuilder(schema, undefined, {
|
|
585
|
+
onSubmit: async () => {
|
|
586
|
+
observedSubmitting = form.isSubmitting.value
|
|
587
|
+
},
|
|
588
|
+
})
|
|
589
|
+
await submit()
|
|
590
|
+
expect(observedSubmitting).toBe(true)
|
|
591
|
+
expect(form.isSubmitting.value).toBe(false)
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
it('clears isSubmitting even when onSubmit throws', async () => {
|
|
595
|
+
const schema = makeSchema([{ name: 'n', label: 'N', type: 'text', defaultValue: 'x' }])
|
|
596
|
+
const { submit, form } = useFormBuilder(schema, undefined, {
|
|
597
|
+
onSubmit: async () => {
|
|
598
|
+
throw new Error('submit failed')
|
|
599
|
+
},
|
|
600
|
+
})
|
|
601
|
+
await expect(submit()).rejects.toThrow('submit failed')
|
|
602
|
+
expect(form.isSubmitting.value).toBe(false)
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
it('does nothing when onSubmit is not provided and validation passes', async () => {
|
|
606
|
+
const schema = makeSchema([{ name: 'n', label: 'N', type: 'text', defaultValue: 'x' }])
|
|
607
|
+
const { submit } = useFormBuilder(schema)
|
|
608
|
+
// Should not throw
|
|
609
|
+
await expect(submit()).resolves.toBeUndefined()
|
|
610
|
+
})
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
// Wizard step navigation
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
|
|
617
|
+
describe('wizard step navigation', () => {
|
|
618
|
+
function makeWizardSchema(
|
|
619
|
+
step1Fields: FormSectionSchema['fields'],
|
|
620
|
+
step2Fields: FormSectionSchema['fields'],
|
|
621
|
+
): FormSchema {
|
|
622
|
+
const steps: FormStepSchema[] = [
|
|
623
|
+
{
|
|
624
|
+
id: 'step1',
|
|
625
|
+
label: 'Step 1',
|
|
626
|
+
sections: [{ id: 'sec1', title: 'Sec 1', fields: step1Fields }],
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
id: 'step2',
|
|
630
|
+
label: 'Step 2',
|
|
631
|
+
sections: [{ id: 'sec2', title: 'Sec 2', fields: step2Fields }],
|
|
632
|
+
},
|
|
633
|
+
]
|
|
634
|
+
return { steps }
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
it('starts at step 0', () => {
|
|
638
|
+
const schema = makeWizardSchema([], [])
|
|
639
|
+
const { currentStep } = useFormBuilder(schema)
|
|
640
|
+
expect(currentStep.value).toBe(0)
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
it('goNext advances to the next step when current step is valid', () => {
|
|
644
|
+
const schema = makeWizardSchema(
|
|
645
|
+
[{ name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice' }],
|
|
646
|
+
[],
|
|
647
|
+
)
|
|
648
|
+
const { goNext, currentStep } = useFormBuilder(schema)
|
|
649
|
+
const result = goNext()
|
|
650
|
+
expect(result).toBe(true)
|
|
651
|
+
expect(currentStep.value).toBe(1)
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
it('goNext does not advance when a required field on the current step is empty', () => {
|
|
655
|
+
const schema = makeWizardSchema(
|
|
656
|
+
[{ name: 'name', label: 'Name', type: 'text', validation: { required: true } }],
|
|
657
|
+
[],
|
|
658
|
+
)
|
|
659
|
+
const { goNext, currentStep } = useFormBuilder(schema)
|
|
660
|
+
const result = goNext()
|
|
661
|
+
expect(result).toBe(false)
|
|
662
|
+
expect(currentStep.value).toBe(0)
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
it('goNext does not go beyond the last step', () => {
|
|
666
|
+
const schema = makeWizardSchema([], [])
|
|
667
|
+
const { goNext, currentStep } = useFormBuilder(schema)
|
|
668
|
+
goNext() // -> step 1
|
|
669
|
+
goNext() // -> should not go to step 2 (out of bounds)
|
|
670
|
+
expect(currentStep.value).toBe(1)
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
it('goBack decrements the step', () => {
|
|
674
|
+
const schema = makeWizardSchema([], [])
|
|
675
|
+
const { goNext, goBack, currentStep } = useFormBuilder(schema)
|
|
676
|
+
goNext()
|
|
677
|
+
expect(currentStep.value).toBe(1)
|
|
678
|
+
goBack()
|
|
679
|
+
expect(currentStep.value).toBe(0)
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
it('goBack does nothing when already on the first step', () => {
|
|
683
|
+
const schema = makeWizardSchema([], [])
|
|
684
|
+
const { goBack, currentStep } = useFormBuilder(schema)
|
|
685
|
+
goBack()
|
|
686
|
+
expect(currentStep.value).toBe(0)
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
it('goToStep jumps to the specified index', () => {
|
|
690
|
+
const schema: FormSchema = {
|
|
691
|
+
steps: [
|
|
692
|
+
{ id: 's1', label: 'S1', sections: [{ id: 'sec1', title: 'S1', fields: [] }] },
|
|
693
|
+
{ id: 's2', label: 'S2', sections: [{ id: 'sec2', title: 'S2', fields: [] }] },
|
|
694
|
+
{ id: 's3', label: 'S3', sections: [{ id: 'sec3', title: 'S3', fields: [] }] },
|
|
695
|
+
],
|
|
696
|
+
}
|
|
697
|
+
const { goToStep, currentStep } = useFormBuilder(schema)
|
|
698
|
+
goToStep(2)
|
|
699
|
+
expect(currentStep.value).toBe(2)
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
it('goToStep ignores out-of-range negative index', () => {
|
|
703
|
+
const schema = makeWizardSchema([], [])
|
|
704
|
+
const { goToStep, currentStep } = useFormBuilder(schema)
|
|
705
|
+
goToStep(-1)
|
|
706
|
+
expect(currentStep.value).toBe(0)
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it('goToStep ignores out-of-range positive index', () => {
|
|
710
|
+
const schema = makeWizardSchema([], [])
|
|
711
|
+
const { goToStep, currentStep } = useFormBuilder(schema)
|
|
712
|
+
goToStep(99)
|
|
713
|
+
expect(currentStep.value).toBe(0)
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
it('goToStep is a no-op on a flat (non-wizard) schema', () => {
|
|
717
|
+
const schema = makeSchema([])
|
|
718
|
+
const { goToStep, currentStep } = useFormBuilder(schema)
|
|
719
|
+
goToStep(1)
|
|
720
|
+
expect(currentStep.value).toBe(0)
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
it('goNext returns false for flat (non-wizard) schema', () => {
|
|
724
|
+
const schema = makeSchema([])
|
|
725
|
+
const { goNext } = useFormBuilder(schema)
|
|
726
|
+
expect(goNext()).toBe(false)
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
it('goNext skips hidden fields when validating the current step', () => {
|
|
730
|
+
const schema = makeWizardSchema(
|
|
731
|
+
[
|
|
732
|
+
{ name: 'type', label: 'Type', type: 'text', defaultValue: 'basic' },
|
|
733
|
+
{
|
|
734
|
+
name: 'extra',
|
|
735
|
+
label: 'Extra',
|
|
736
|
+
type: 'text',
|
|
737
|
+
validation: { required: true },
|
|
738
|
+
condition: { field: 'type', eq: 'advanced' },
|
|
739
|
+
},
|
|
740
|
+
],
|
|
741
|
+
[],
|
|
742
|
+
)
|
|
743
|
+
const { goNext, currentStep } = useFormBuilder(schema)
|
|
744
|
+
// 'extra' is required but its condition is not met (type=basic) → should still pass
|
|
745
|
+
expect(goNext()).toBe(true)
|
|
746
|
+
expect(currentStep.value).toBe(1)
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
it('reset resets currentStep to 0', () => {
|
|
750
|
+
const schema = makeWizardSchema([], [])
|
|
751
|
+
const { goNext, reset, currentStep } = useFormBuilder(schema)
|
|
752
|
+
goNext()
|
|
753
|
+
expect(currentStep.value).toBe(1)
|
|
754
|
+
reset()
|
|
755
|
+
expect(currentStep.value).toBe(0)
|
|
756
|
+
})
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
// ---------------------------------------------------------------------------
|
|
760
|
+
// isCurrentStepValid
|
|
761
|
+
// ---------------------------------------------------------------------------
|
|
762
|
+
|
|
763
|
+
describe('isCurrentStepValid', () => {
|
|
764
|
+
it('returns true when all visible step fields pass validation', () => {
|
|
765
|
+
const schema: FormSchema = {
|
|
766
|
+
steps: [
|
|
767
|
+
{
|
|
768
|
+
id: 's1',
|
|
769
|
+
label: 'S1',
|
|
770
|
+
sections: [
|
|
771
|
+
{
|
|
772
|
+
id: 'sec1',
|
|
773
|
+
title: 'S1',
|
|
774
|
+
fields: [
|
|
775
|
+
{ name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice', validation: { required: true } },
|
|
776
|
+
],
|
|
777
|
+
},
|
|
778
|
+
],
|
|
779
|
+
},
|
|
780
|
+
],
|
|
781
|
+
}
|
|
782
|
+
const { isCurrentStepValid } = useFormBuilder(schema)
|
|
783
|
+
expect(isCurrentStepValid.value).toBe(true)
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
it('returns false when a required visible field is empty on current step', () => {
|
|
787
|
+
const schema: FormSchema = {
|
|
788
|
+
steps: [
|
|
789
|
+
{
|
|
790
|
+
id: 's1',
|
|
791
|
+
label: 'S1',
|
|
792
|
+
sections: [
|
|
793
|
+
{
|
|
794
|
+
id: 'sec1',
|
|
795
|
+
title: 'S1',
|
|
796
|
+
fields: [
|
|
797
|
+
{ name: 'name', label: 'Name', type: 'text', validation: { required: true } },
|
|
798
|
+
],
|
|
799
|
+
},
|
|
800
|
+
],
|
|
801
|
+
},
|
|
802
|
+
],
|
|
803
|
+
}
|
|
804
|
+
const { isCurrentStepValid } = useFormBuilder(schema)
|
|
805
|
+
expect(isCurrentStepValid.value).toBe(false)
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
it('falls back to form.isValid for flat schemas', () => {
|
|
809
|
+
const schema = makeSchema([
|
|
810
|
+
{ name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice', validation: { required: true } },
|
|
811
|
+
])
|
|
812
|
+
const { isCurrentStepValid } = useFormBuilder(schema)
|
|
813
|
+
expect(isCurrentStepValid.value).toBe(true)
|
|
814
|
+
})
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
// ---------------------------------------------------------------------------
|
|
818
|
+
// getFieldOptions
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
|
|
821
|
+
describe('getFieldOptions', () => {
|
|
822
|
+
it('returns options from field props when defined in schema', () => {
|
|
823
|
+
const options = [
|
|
824
|
+
{ label: 'Option A', value: 'a' },
|
|
825
|
+
{ label: 'Option B', value: 'b' },
|
|
826
|
+
]
|
|
827
|
+
const schema = makeSchema([
|
|
828
|
+
{ name: 'choice', label: 'Choice', type: 'select', props: { options } },
|
|
829
|
+
])
|
|
830
|
+
const { getFieldOptions } = useFormBuilder(schema)
|
|
831
|
+
expect(getFieldOptions('choice')).toEqual(options)
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
it('returns undefined when field has no options', () => {
|
|
835
|
+
const schema = makeSchema([
|
|
836
|
+
{ name: 'name', label: 'Name', type: 'text' },
|
|
837
|
+
])
|
|
838
|
+
const { getFieldOptions } = useFormBuilder(schema)
|
|
839
|
+
expect(getFieldOptions('name')).toBeUndefined()
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
it('returns dynamic options from enhancement over schema props', () => {
|
|
843
|
+
const schemaOptions = [{ label: 'Static', value: 's' }]
|
|
844
|
+
const dynamicOptions = [{ label: 'Dynamic', value: 'd' }]
|
|
845
|
+
const schema = makeSchema([
|
|
846
|
+
{ name: 'choice', label: 'Choice', type: 'select', props: { options: schemaOptions } },
|
|
847
|
+
])
|
|
848
|
+
const { getFieldOptions } = useFormBuilder(schema, undefined, {
|
|
849
|
+
fields: {
|
|
850
|
+
choice: { options: () => dynamicOptions },
|
|
851
|
+
},
|
|
852
|
+
})
|
|
853
|
+
expect(getFieldOptions('choice')).toEqual(dynamicOptions)
|
|
854
|
+
})
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
// ---------------------------------------------------------------------------
|
|
858
|
+
// fields list
|
|
859
|
+
// ---------------------------------------------------------------------------
|
|
860
|
+
|
|
861
|
+
describe('fields', () => {
|
|
862
|
+
it('returns all fields from a flat schema', () => {
|
|
863
|
+
const schema: FormSchema = {
|
|
864
|
+
sections: [
|
|
865
|
+
{ id: 'sec1', title: 'S1', fields: [{ name: 'a', label: 'A', type: 'text' }] },
|
|
866
|
+
{ id: 'sec2', title: 'S2', fields: [{ name: 'b', label: 'B', type: 'number' }] },
|
|
867
|
+
],
|
|
868
|
+
}
|
|
869
|
+
const { fields } = useFormBuilder(schema)
|
|
870
|
+
expect(fields.map((f) => f.name)).toEqual(['a', 'b'])
|
|
871
|
+
})
|
|
872
|
+
|
|
873
|
+
it('returns all fields flattened across wizard steps', () => {
|
|
874
|
+
const schema: FormSchema = {
|
|
875
|
+
steps: [
|
|
876
|
+
{
|
|
877
|
+
id: 's1',
|
|
878
|
+
label: 'S1',
|
|
879
|
+
sections: [{ id: 'sec1', title: 'S1', fields: [{ name: 'a', label: 'A', type: 'text' }] }],
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
id: 's2',
|
|
883
|
+
label: 'S2',
|
|
884
|
+
sections: [{ id: 'sec2', title: 'S2', fields: [{ name: 'b', label: 'B', type: 'number' }] }],
|
|
885
|
+
},
|
|
886
|
+
],
|
|
887
|
+
}
|
|
888
|
+
const { fields } = useFormBuilder(schema)
|
|
889
|
+
expect(fields.map((f) => f.name)).toEqual(['a', 'b'])
|
|
890
|
+
})
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
// ---------------------------------------------------------------------------
|
|
894
|
+
// reset
|
|
895
|
+
// ---------------------------------------------------------------------------
|
|
896
|
+
|
|
897
|
+
describe('reset', () => {
|
|
898
|
+
it('resets form data to initial values', () => {
|
|
899
|
+
const schema = makeSchema([
|
|
900
|
+
{ name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice' },
|
|
901
|
+
])
|
|
902
|
+
const { form, reset } = useFormBuilder(schema)
|
|
903
|
+
form.data.name = 'Bob'
|
|
904
|
+
reset()
|
|
905
|
+
expect(form.data.name).toBe('Alice')
|
|
906
|
+
})
|
|
907
|
+
|
|
908
|
+
it('accepts override values on reset', () => {
|
|
909
|
+
const schema = makeSchema([
|
|
910
|
+
{ name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice' },
|
|
911
|
+
])
|
|
912
|
+
const { form, reset } = useFormBuilder(schema)
|
|
913
|
+
reset({ name: 'Charlie' })
|
|
914
|
+
expect(form.data.name).toBe('Charlie')
|
|
915
|
+
})
|
|
916
|
+
})
|
|
917
|
+
})
|