@remoteoss/json-schema-form 0.1.0-beta.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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/index.cjs +1207 -0
- package/dist/index.js +1176 -0
- package/dist/standalone.js +12580 -0
- package/package.json +79 -0
- package/src/tests/createHeadlessForm.customValidations.test.jsx +799 -0
- package/src/tests/createHeadlessForm.test.js +3343 -0
- package/src/tests/helpers.custom.js +223 -0
- package/src/tests/helpers.js +1983 -0
- package/src/tests/internals.helpers.test.js +175 -0
- package/src/tests/utils.test.jsx +32 -0
|
@@ -0,0 +1,3343 @@
|
|
|
1
|
+
import isNil from 'lodash/isNil';
|
|
2
|
+
import omitBy from 'lodash/omitBy';
|
|
3
|
+
import { object } from 'yup';
|
|
4
|
+
|
|
5
|
+
import { createHeadlessForm } from '../createHeadlessForm';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
JSONSchemaBuilder,
|
|
9
|
+
schemaInputTypeText,
|
|
10
|
+
schemaInputTypeRadioDeprecated,
|
|
11
|
+
schemaInputTypeRadio,
|
|
12
|
+
schemaInputTypeRadioRequiredAndOptional,
|
|
13
|
+
schemaInputTypeSelectSoloDeprecated,
|
|
14
|
+
schemaInputTypeSelectSolo,
|
|
15
|
+
schemaInputTypeSelectMultipleDeprecated,
|
|
16
|
+
schemaInputTypeSelectMultiple,
|
|
17
|
+
schemaInputTypeSelectMultipleOptional,
|
|
18
|
+
schemaInputTypeNumber,
|
|
19
|
+
schemaInputTypeDate,
|
|
20
|
+
schemaInputTypeEmail,
|
|
21
|
+
schemaInputWithStatement,
|
|
22
|
+
schemaInputTypeCheckbox,
|
|
23
|
+
schemaInputTypeCheckboxBooleans,
|
|
24
|
+
schemaWithOrderKeyword,
|
|
25
|
+
schemaWithPositionDeprecated,
|
|
26
|
+
schemaDynamicValidationConst,
|
|
27
|
+
schemaDynamicValidationMinimumMaximum,
|
|
28
|
+
schemaDynamicValidationMinLengthMaxLength,
|
|
29
|
+
schemaDynamicValidationContains,
|
|
30
|
+
schemaAnyOfValidation,
|
|
31
|
+
schemaWithoutInputTypes,
|
|
32
|
+
schemaWithoutTypes,
|
|
33
|
+
mockFileInput,
|
|
34
|
+
mockRadioCardInput,
|
|
35
|
+
mockRadioCardExpandableInput,
|
|
36
|
+
mockTextInput,
|
|
37
|
+
mockTextInputDeprecated,
|
|
38
|
+
mockNumberInput,
|
|
39
|
+
mockNumberInputWithPercentageAndCustomRange,
|
|
40
|
+
mockTextPatternInput,
|
|
41
|
+
mockTextMaxLengthInput,
|
|
42
|
+
mockFieldset,
|
|
43
|
+
mockNestedFieldset,
|
|
44
|
+
mockGroupArrayInput,
|
|
45
|
+
schemaFieldsetScopedCondition,
|
|
46
|
+
schemaWithConditionalPresentationProperties,
|
|
47
|
+
schemaWithConditionalReadOnlyProperty,
|
|
48
|
+
schemaWithWrongConditional,
|
|
49
|
+
schemaWithConditionalAcknowledgementProperty,
|
|
50
|
+
schemaInputTypeNumberWithPercentage,
|
|
51
|
+
schemaForErrorMessageSpecificity,
|
|
52
|
+
jsfConfigForErrorMessageSpecificity,
|
|
53
|
+
} from './helpers';
|
|
54
|
+
|
|
55
|
+
function buildJSONSchemaInput({ presentationFields, inputFields = {}, required }) {
|
|
56
|
+
return {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
test: {
|
|
60
|
+
description: 'Test description',
|
|
61
|
+
presentation: {
|
|
62
|
+
...presentationFields,
|
|
63
|
+
},
|
|
64
|
+
title: 'Test title',
|
|
65
|
+
type: 'number',
|
|
66
|
+
...inputFields,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: required ? ['test'] : [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function friendlyError({ formErrors }) {
|
|
74
|
+
// destruct the formErrors directly
|
|
75
|
+
return formErrors;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
expect(console.error).not.toHaveBeenCalled();
|
|
84
|
+
console.error.mockRestore();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('createHeadlessForm', () => {
|
|
88
|
+
it('returns empty result given no schema', () => {
|
|
89
|
+
const result = createHeadlessForm();
|
|
90
|
+
|
|
91
|
+
expect(result).toMatchObject({
|
|
92
|
+
fields: [],
|
|
93
|
+
});
|
|
94
|
+
expect(result.isError).toBe(false);
|
|
95
|
+
expect(result.error).toBeFalsy();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('returns an error given invalid schema', () => {
|
|
99
|
+
const result = createHeadlessForm({ foo: 1 });
|
|
100
|
+
|
|
101
|
+
expect(result.fields).toHaveLength(0);
|
|
102
|
+
expect(result.isError).toBe(true);
|
|
103
|
+
|
|
104
|
+
expect(console.error).toHaveBeenCalledWith(`JSON Schema invalid!`, expect.any(Error));
|
|
105
|
+
console.error.mockClear();
|
|
106
|
+
|
|
107
|
+
expect(result.error.message).toBe(`Cannot convert undefined or null to object`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('field support fallback', () => {
|
|
111
|
+
it('sets type from presentation.inputType', () => {
|
|
112
|
+
const { fields } = createHeadlessForm({
|
|
113
|
+
properties: {
|
|
114
|
+
age: {
|
|
115
|
+
title: 'Age',
|
|
116
|
+
presentation: { inputType: 'number' },
|
|
117
|
+
type: 'number',
|
|
118
|
+
},
|
|
119
|
+
starting_time: {
|
|
120
|
+
title: 'Starting time',
|
|
121
|
+
presentation: {
|
|
122
|
+
inputType: 'hour', // Arbitrary types are accepted
|
|
123
|
+
set: 'AM', // And even any arbitrary presentation keys
|
|
124
|
+
},
|
|
125
|
+
type: 'string',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const { schema: yupSchema1, ...fieldAge } = omitBy(fields[0], isNil);
|
|
131
|
+
const { schema: yupSchema2, ...fieldTime } = omitBy(fields[1], isNil);
|
|
132
|
+
|
|
133
|
+
expect(yupSchema1).toEqual(expect.any(Object));
|
|
134
|
+
expect(fieldAge).toMatchObject({
|
|
135
|
+
inputType: 'number',
|
|
136
|
+
jsonType: 'number',
|
|
137
|
+
type: 'number',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(yupSchema1).toEqual(expect.any(Object));
|
|
141
|
+
expect(fieldTime).toMatchObject({
|
|
142
|
+
inputType: 'hour',
|
|
143
|
+
jsonType: 'string',
|
|
144
|
+
name: 'starting_time',
|
|
145
|
+
type: 'hour',
|
|
146
|
+
set: 'AM',
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('fails given a json schema without inputType', () => {
|
|
151
|
+
const { fields, error } = createHeadlessForm({
|
|
152
|
+
properties: {
|
|
153
|
+
test: { type: 'string' },
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(fields).toHaveLength(0);
|
|
158
|
+
expect(error.message).toContain('Strict error: Missing inputType to field "test"');
|
|
159
|
+
|
|
160
|
+
expect(console.error).toHaveBeenCalledWith(`JSON Schema invalid!`, expect.any(Error));
|
|
161
|
+
console.error.mockClear();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
function extractTypeOnly(listOfFields) {
|
|
165
|
+
const list = Array.isArray(listOfFields) ? listOfFields : listOfFields?.(); // handle fieldset + group-array
|
|
166
|
+
return list?.map(
|
|
167
|
+
({ name, type, inputType, jsonType, label, options, fields: nestedFields }) => {
|
|
168
|
+
return omitBy(
|
|
169
|
+
{
|
|
170
|
+
name,
|
|
171
|
+
type, // @deprecated
|
|
172
|
+
inputType,
|
|
173
|
+
jsonType,
|
|
174
|
+
label,
|
|
175
|
+
options,
|
|
176
|
+
fields: extractTypeOnly(nestedFields),
|
|
177
|
+
},
|
|
178
|
+
isNil
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
it('given a json schema without inputType, sets type based on json type (when strictInputType:false)', () => {
|
|
185
|
+
const { fields } = createHeadlessForm(schemaWithoutInputTypes, {
|
|
186
|
+
strictInputType: false,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const fieldsByNameAndType = extractTypeOnly(fields);
|
|
190
|
+
expect(fieldsByNameAndType).toMatchInlineSnapshot(`
|
|
191
|
+
[
|
|
192
|
+
{
|
|
193
|
+
"inputType": "text",
|
|
194
|
+
"jsonType": "string",
|
|
195
|
+
"label": "A string -> text",
|
|
196
|
+
"name": "a_string",
|
|
197
|
+
"type": "text",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"inputType": "radio",
|
|
201
|
+
"jsonType": "string",
|
|
202
|
+
"label": "A string with oneOf -> radio",
|
|
203
|
+
"name": "a_string_oneOf",
|
|
204
|
+
"options": [
|
|
205
|
+
{
|
|
206
|
+
"label": "Yes",
|
|
207
|
+
"value": "yes",
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"label": "No",
|
|
211
|
+
"value": "no",
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
"type": "radio",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"inputType": "email",
|
|
218
|
+
"jsonType": "string",
|
|
219
|
+
"label": "A string with format:email -> email",
|
|
220
|
+
"name": "a_string_email",
|
|
221
|
+
"type": "email",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"inputType": "date",
|
|
225
|
+
"jsonType": "string",
|
|
226
|
+
"label": "A string with format:email -> date",
|
|
227
|
+
"name": "a_string_date",
|
|
228
|
+
"type": "date",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"inputType": "file",
|
|
232
|
+
"jsonType": "string",
|
|
233
|
+
"label": "A string with format:data-url -> file",
|
|
234
|
+
"name": "a_string_file",
|
|
235
|
+
"type": "file",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"inputType": "number",
|
|
239
|
+
"jsonType": "number",
|
|
240
|
+
"label": "A number -> number",
|
|
241
|
+
"name": "a_number",
|
|
242
|
+
"type": "number",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"inputType": "number",
|
|
246
|
+
"jsonType": "integer",
|
|
247
|
+
"label": "A integer -> number",
|
|
248
|
+
"name": "a_integer",
|
|
249
|
+
"type": "number",
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"inputType": "checkbox",
|
|
253
|
+
"jsonType": "boolean",
|
|
254
|
+
"label": "A boolean -> checkbox",
|
|
255
|
+
"name": "a_boolean",
|
|
256
|
+
"type": "checkbox",
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"fields": [
|
|
260
|
+
{
|
|
261
|
+
"inputType": "text",
|
|
262
|
+
"jsonType": "string",
|
|
263
|
+
"name": "foo",
|
|
264
|
+
"type": "text",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"inputType": "text",
|
|
268
|
+
"jsonType": "string",
|
|
269
|
+
"name": "bar",
|
|
270
|
+
"type": "text",
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
"inputType": "fieldset",
|
|
274
|
+
"jsonType": "object",
|
|
275
|
+
"label": "An object -> fieldset",
|
|
276
|
+
"name": "a_object",
|
|
277
|
+
"type": "fieldset",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"inputType": "select",
|
|
281
|
+
"jsonType": "array",
|
|
282
|
+
"label": "An array items.anyOf -> select",
|
|
283
|
+
"name": "a_array_items",
|
|
284
|
+
"options": [
|
|
285
|
+
{
|
|
286
|
+
"label": "Chrome",
|
|
287
|
+
"value": "chr",
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"label": "Firefox",
|
|
291
|
+
"value": "ff",
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"label": "Internet Explorer",
|
|
295
|
+
"value": "ie",
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
"type": "select",
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
"fields": [
|
|
302
|
+
{
|
|
303
|
+
"inputType": "text",
|
|
304
|
+
"jsonType": "string",
|
|
305
|
+
"label": "Role",
|
|
306
|
+
"name": "role",
|
|
307
|
+
"type": "text",
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"inputType": "number",
|
|
311
|
+
"jsonType": "number",
|
|
312
|
+
"label": "Years",
|
|
313
|
+
"name": "years",
|
|
314
|
+
"type": "number",
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
"inputType": "group-array",
|
|
318
|
+
"jsonType": "array",
|
|
319
|
+
"label": "An array items.properties -> group-array",
|
|
320
|
+
"name": "a_array_properties",
|
|
321
|
+
"type": "group-array",
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"inputType": "text",
|
|
325
|
+
"label": "A void -> text",
|
|
326
|
+
"name": "a_void",
|
|
327
|
+
"type": "text",
|
|
328
|
+
},
|
|
329
|
+
]
|
|
330
|
+
`);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('given a json schema without json type, sets type based on structure (when strictInputType:false)', () => {
|
|
334
|
+
const { fields } = createHeadlessForm(schemaWithoutTypes, {
|
|
335
|
+
strictInputType: false,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const fieldsByNameAndType = extractTypeOnly(fields);
|
|
339
|
+
expect(fieldsByNameAndType).toMatchInlineSnapshot(`
|
|
340
|
+
[
|
|
341
|
+
{
|
|
342
|
+
"inputType": "text",
|
|
343
|
+
"label": "Default -> text",
|
|
344
|
+
"name": "default",
|
|
345
|
+
"type": "text",
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"inputType": "radio",
|
|
349
|
+
"label": "With oneOf -> radio",
|
|
350
|
+
"name": "with_oneOf",
|
|
351
|
+
"options": [
|
|
352
|
+
{
|
|
353
|
+
"label": "Yes",
|
|
354
|
+
"value": "yes",
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"label": "No",
|
|
358
|
+
"value": "no",
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
"type": "radio",
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
"inputType": "email",
|
|
365
|
+
"label": "With format:email -> email",
|
|
366
|
+
"name": "with_email",
|
|
367
|
+
"type": "email",
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"inputType": "select",
|
|
371
|
+
"label": "With properties -> fieldset",
|
|
372
|
+
"name": "with_object",
|
|
373
|
+
"type": "select",
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
"inputType": "text",
|
|
377
|
+
"label": "With items.anyOf -> select",
|
|
378
|
+
"name": "with_items_anyOf",
|
|
379
|
+
"options": [
|
|
380
|
+
{
|
|
381
|
+
"label": "Chrome",
|
|
382
|
+
"value": "chr",
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
"label": "Firefox",
|
|
386
|
+
"value": "ff",
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"label": "Internet Explorer",
|
|
390
|
+
"value": "ie",
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
"type": "text",
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
"fields": [
|
|
397
|
+
{
|
|
398
|
+
"inputType": "text",
|
|
399
|
+
"label": "Role",
|
|
400
|
+
"name": "role",
|
|
401
|
+
"type": "text",
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
"inputType": "text",
|
|
405
|
+
"label": "Years",
|
|
406
|
+
"name": "years",
|
|
407
|
+
"type": "text",
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
"inputType": "group-array",
|
|
411
|
+
"label": "With items.properties -> group-array",
|
|
412
|
+
"name": "with_items_properties",
|
|
413
|
+
"type": "group-array",
|
|
414
|
+
},
|
|
415
|
+
]
|
|
416
|
+
`);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
describe('field support', () => {
|
|
421
|
+
it('support "text" field type', () => {
|
|
422
|
+
const { fields } = createHeadlessForm(schemaInputTypeText);
|
|
423
|
+
|
|
424
|
+
expect(fields[0]).toMatchObject({
|
|
425
|
+
description: 'The number of your national identification (max 10 digits)',
|
|
426
|
+
label: 'ID number',
|
|
427
|
+
name: 'id_number',
|
|
428
|
+
required: true,
|
|
429
|
+
schema: expect.any(Object),
|
|
430
|
+
inputType: 'text',
|
|
431
|
+
jsonType: 'string',
|
|
432
|
+
maskSecret: 2,
|
|
433
|
+
maxLength: 10,
|
|
434
|
+
isVisible: true,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const fieldValidator = fields[0].schema;
|
|
438
|
+
expect(fieldValidator.isValidSync('CI007')).toBe(true);
|
|
439
|
+
expect(fieldValidator.isValidSync(true)).toBe(true); // @BUG RMT-446 - cannot be a bool
|
|
440
|
+
expect(fieldValidator.isValidSync(1)).toBe(true); // @BUG RMT-446 - cannot be a number
|
|
441
|
+
expect(fieldValidator.isValidSync(0)).toBe(true); // @BUG RMT-446 - cannot be a number
|
|
442
|
+
|
|
443
|
+
expect(() => fieldValidator.validateSync('')).toThrowError('Required field');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('supports both root level "description" and "x-jsf-presentation.description"', () => {
|
|
447
|
+
const resultsWithRootDescription = createHeadlessForm(
|
|
448
|
+
JSONSchemaBuilder()
|
|
449
|
+
.addInput({
|
|
450
|
+
id_number: mockTextInput,
|
|
451
|
+
})
|
|
452
|
+
.setRequiredFields(['id_number'])
|
|
453
|
+
.build()
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
expect(resultsWithRootDescription.fields[0].description).toMatch(
|
|
457
|
+
/the number of your national/i
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
const resultsWithPresentationDescription = createHeadlessForm(
|
|
461
|
+
JSONSchemaBuilder()
|
|
462
|
+
.addInput({
|
|
463
|
+
id_number: {
|
|
464
|
+
...mockTextInput,
|
|
465
|
+
'x-jsf-presentation': {
|
|
466
|
+
inputType: 'text',
|
|
467
|
+
maskSecret: 2,
|
|
468
|
+
// should override the root level description
|
|
469
|
+
description: 'a different description with <span>markup</span>',
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
})
|
|
473
|
+
.setRequiredFields(['id_number'])
|
|
474
|
+
.build()
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
expect(resultsWithPresentationDescription.fields[0].description).toMatch(
|
|
478
|
+
/a different description /i
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('supports both root level "description" and "presentation.description" (deprecated)', () => {
|
|
483
|
+
const resultsWithRootDescription = createHeadlessForm(
|
|
484
|
+
JSONSchemaBuilder()
|
|
485
|
+
.addInput({
|
|
486
|
+
id_number: mockTextInputDeprecated,
|
|
487
|
+
})
|
|
488
|
+
.setRequiredFields(['id_number'])
|
|
489
|
+
.build()
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
expect(resultsWithRootDescription.fields[0].description).toMatch(
|
|
493
|
+
/the number of your national/i
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const resultsWithPresentationDescription = createHeadlessForm(
|
|
497
|
+
JSONSchemaBuilder()
|
|
498
|
+
.addInput({
|
|
499
|
+
id_number: {
|
|
500
|
+
...mockTextInputDeprecated,
|
|
501
|
+
presentation: {
|
|
502
|
+
inputType: 'text',
|
|
503
|
+
maskSecret: 2,
|
|
504
|
+
// should override the root level description
|
|
505
|
+
description: 'a different description with <span>markup</span>',
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
})
|
|
509
|
+
.setRequiredFields(['id_number'])
|
|
510
|
+
.build()
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
expect(resultsWithPresentationDescription.fields[0].description).toMatch(
|
|
514
|
+
/a different description /i
|
|
515
|
+
);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('support "select" field type @deprecated', () => {
|
|
519
|
+
const result = createHeadlessForm(schemaInputTypeSelectSoloDeprecated);
|
|
520
|
+
|
|
521
|
+
expect(result).toMatchObject({
|
|
522
|
+
fields: [
|
|
523
|
+
{
|
|
524
|
+
description: 'Life Insurance',
|
|
525
|
+
label: 'Benefits (solo)',
|
|
526
|
+
name: 'benefits',
|
|
527
|
+
placeholder: 'Select...',
|
|
528
|
+
type: 'select',
|
|
529
|
+
options: [
|
|
530
|
+
{
|
|
531
|
+
label: 'Medical Insurance',
|
|
532
|
+
value: 'Medical Insurance',
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
label: 'Health Insurance',
|
|
536
|
+
value: 'Health Insurance',
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
label: 'Travel Bonus',
|
|
540
|
+
value: 'Travel Bonus',
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
},
|
|
544
|
+
],
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
it('support "select" field type', () => {
|
|
548
|
+
const result = createHeadlessForm(schemaInputTypeSelectSolo);
|
|
549
|
+
|
|
550
|
+
const fieldSelect = result.fields[0];
|
|
551
|
+
expect(fieldSelect).toMatchObject({
|
|
552
|
+
name: 'browsers',
|
|
553
|
+
label: 'Browsers (solo)',
|
|
554
|
+
description: 'This solo select also includes a disabled option.',
|
|
555
|
+
options: [
|
|
556
|
+
{
|
|
557
|
+
value: 'chr',
|
|
558
|
+
label: 'Chrome',
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
value: 'ff',
|
|
562
|
+
label: 'Firefox',
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
value: 'ie',
|
|
566
|
+
label: 'Internet Explorer',
|
|
567
|
+
disabled: true,
|
|
568
|
+
},
|
|
569
|
+
],
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
expect(fieldSelect).not.toHaveProperty('multiple');
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('supports "select" field type with multiple options @deprecated', () => {
|
|
576
|
+
const result = createHeadlessForm(schemaInputTypeSelectMultipleDeprecated);
|
|
577
|
+
expect(result).toMatchObject({
|
|
578
|
+
fields: [
|
|
579
|
+
{
|
|
580
|
+
description: 'Life Insurance',
|
|
581
|
+
label: 'Benefits (multiple)',
|
|
582
|
+
name: 'benefits_multi',
|
|
583
|
+
placeholder: 'Select...',
|
|
584
|
+
type: 'select',
|
|
585
|
+
options: [
|
|
586
|
+
{
|
|
587
|
+
label: 'Medical Insurance',
|
|
588
|
+
value: 'Medical Insurance',
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
label: 'Health Insurance',
|
|
592
|
+
value: 'Health Insurance',
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
label: 'Travel Bonus',
|
|
596
|
+
value: 'Travel Bonus',
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
multiple: true,
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
it('supports "select" field type with multiple options', () => {
|
|
605
|
+
const result = createHeadlessForm(schemaInputTypeSelectMultiple);
|
|
606
|
+
expect(result).toMatchObject({
|
|
607
|
+
fields: [
|
|
608
|
+
{
|
|
609
|
+
name: 'browsers_multi',
|
|
610
|
+
label: 'Browsers (multiple)',
|
|
611
|
+
description: 'This multi-select also includes a disabled option.',
|
|
612
|
+
options: [
|
|
613
|
+
{
|
|
614
|
+
value: 'chr',
|
|
615
|
+
label: 'Chrome',
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
value: 'ff',
|
|
619
|
+
label: 'Firefox',
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
value: 'ie',
|
|
623
|
+
label: 'Internet Explorer',
|
|
624
|
+
disabled: true,
|
|
625
|
+
},
|
|
626
|
+
],
|
|
627
|
+
multiple: true,
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('supports "select" field type with multiple options and optional', () => {
|
|
634
|
+
const result = createHeadlessForm(schemaInputTypeSelectMultipleOptional);
|
|
635
|
+
expect(result).toMatchObject({
|
|
636
|
+
fields: [
|
|
637
|
+
{
|
|
638
|
+
name: 'browsers_multi_optional',
|
|
639
|
+
label: 'Browsers (multiple) (optional)',
|
|
640
|
+
description: 'This optional multi-select also includes a disabled option.',
|
|
641
|
+
options: [
|
|
642
|
+
{
|
|
643
|
+
value: 'chr',
|
|
644
|
+
label: 'Chrome',
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
value: 'ff',
|
|
648
|
+
label: 'Firefox',
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
value: 'ie',
|
|
652
|
+
label: 'Internet Explorer',
|
|
653
|
+
disabled: true,
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
multiple: true,
|
|
657
|
+
},
|
|
658
|
+
],
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('support "radio" field type @deprecated', () => {
|
|
663
|
+
const result = createHeadlessForm(schemaInputTypeRadioDeprecated);
|
|
664
|
+
|
|
665
|
+
expect(result).toMatchObject({
|
|
666
|
+
fields: [
|
|
667
|
+
{
|
|
668
|
+
description: 'Do you have any siblings?',
|
|
669
|
+
label: 'Has siblings',
|
|
670
|
+
name: 'has_siblings',
|
|
671
|
+
options: [
|
|
672
|
+
{
|
|
673
|
+
label: 'Yes',
|
|
674
|
+
value: 'yes',
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
label: 'No',
|
|
678
|
+
value: 'no',
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
required: true,
|
|
682
|
+
schema: expect.any(Object),
|
|
683
|
+
type: 'radio',
|
|
684
|
+
},
|
|
685
|
+
],
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const fieldValidator = result.fields[0].schema;
|
|
689
|
+
expect(fieldValidator.isValidSync('yes')).toBe(true);
|
|
690
|
+
expect(() => fieldValidator.validateSync('')).toThrowError('Required field');
|
|
691
|
+
});
|
|
692
|
+
it('support "radio" field type', () => {
|
|
693
|
+
const result = createHeadlessForm(schemaInputTypeRadio);
|
|
694
|
+
|
|
695
|
+
expect(result).toMatchObject({
|
|
696
|
+
fields: [
|
|
697
|
+
{
|
|
698
|
+
description: 'Do you have any siblings?',
|
|
699
|
+
label: 'Has siblings',
|
|
700
|
+
name: 'has_siblings',
|
|
701
|
+
options: [
|
|
702
|
+
{
|
|
703
|
+
label: 'Yes',
|
|
704
|
+
value: 'yes',
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
label: 'No',
|
|
708
|
+
value: 'no',
|
|
709
|
+
},
|
|
710
|
+
],
|
|
711
|
+
required: true,
|
|
712
|
+
schema: expect.any(Object),
|
|
713
|
+
type: 'radio',
|
|
714
|
+
},
|
|
715
|
+
],
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
const fieldValidator = result.fields[0].schema;
|
|
719
|
+
expect(fieldValidator.isValidSync('yes')).toBe(true);
|
|
720
|
+
expect(() => fieldValidator.validateSync('')).toThrowError('Required field');
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('support "radio" optional field', () => {
|
|
724
|
+
const result = createHeadlessForm(schemaInputTypeRadioRequiredAndOptional);
|
|
725
|
+
|
|
726
|
+
expect(result).toMatchObject({
|
|
727
|
+
fields: [
|
|
728
|
+
{},
|
|
729
|
+
{
|
|
730
|
+
name: 'has_car',
|
|
731
|
+
label: 'Has car',
|
|
732
|
+
description: 'Do you have a car? (optional field, check oneOf)',
|
|
733
|
+
options: [
|
|
734
|
+
{
|
|
735
|
+
label: 'Yes',
|
|
736
|
+
value: 'yes',
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
label: 'No',
|
|
740
|
+
value: 'no',
|
|
741
|
+
},
|
|
742
|
+
],
|
|
743
|
+
required: false,
|
|
744
|
+
schema: expect.any(Object),
|
|
745
|
+
type: 'radio',
|
|
746
|
+
},
|
|
747
|
+
],
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
const fieldValidator = result.fields[0].schema;
|
|
751
|
+
expect(fieldValidator.isValidSync('yes')).toBe(true);
|
|
752
|
+
expect(() => fieldValidator.validateSync('')).toThrowError('Required field');
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it('support "number" field type', () => {
|
|
756
|
+
const result = createHeadlessForm(schemaInputTypeNumber);
|
|
757
|
+
expect(result).toMatchObject({
|
|
758
|
+
fields: [
|
|
759
|
+
{
|
|
760
|
+
description: 'How many open tabs do you have?',
|
|
761
|
+
label: 'Tabs',
|
|
762
|
+
name: 'tabs',
|
|
763
|
+
required: true,
|
|
764
|
+
schema: expect.any(Object),
|
|
765
|
+
type: 'number',
|
|
766
|
+
minimum: 1,
|
|
767
|
+
maximum: 10,
|
|
768
|
+
},
|
|
769
|
+
],
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
const fieldValidator = result.fields[0].schema;
|
|
773
|
+
expect(fieldValidator.isValidSync('0')).toBe(false);
|
|
774
|
+
expect(fieldValidator.isValidSync('10')).toBe(true);
|
|
775
|
+
expect(fieldValidator.isValidSync('11')).toBe(false);
|
|
776
|
+
expect(fieldValidator.isValidSync('this is text with a number 1')).toBe(false);
|
|
777
|
+
expect(() => fieldValidator.validateSync('some text')).toThrowError(
|
|
778
|
+
'The value must be a number'
|
|
779
|
+
);
|
|
780
|
+
expect(() => fieldValidator.validateSync('')).toThrowError('The value must be a number');
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it('support "number" field type with the percentage attribute', () => {
|
|
784
|
+
const result = createHeadlessForm(schemaInputTypeNumberWithPercentage);
|
|
785
|
+
expect(result).toMatchObject({
|
|
786
|
+
fields: [
|
|
787
|
+
{
|
|
788
|
+
description: 'What % of shares do you own?',
|
|
789
|
+
label: 'Shares',
|
|
790
|
+
name: 'shares',
|
|
791
|
+
percentage: true,
|
|
792
|
+
required: true,
|
|
793
|
+
schema: expect.any(Object),
|
|
794
|
+
type: 'number',
|
|
795
|
+
minimum: 1,
|
|
796
|
+
maximum: 100,
|
|
797
|
+
},
|
|
798
|
+
],
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
const fieldValidator = result.fields[0].schema;
|
|
802
|
+
const { percentage } = result.fields[0];
|
|
803
|
+
expect(fieldValidator.isValidSync('0')).toBe(false);
|
|
804
|
+
expect(fieldValidator.isValidSync('10')).toBe(true);
|
|
805
|
+
expect(fieldValidator.isValidSync('101')).toBe(false);
|
|
806
|
+
expect(fieldValidator.isValidSync('this is text with a number 1')).toBe(false);
|
|
807
|
+
expect(() => fieldValidator.validateSync('some text')).toThrowError(
|
|
808
|
+
'The value must be a number'
|
|
809
|
+
);
|
|
810
|
+
expect(() => fieldValidator.validateSync('')).toThrowError('The value must be a number');
|
|
811
|
+
expect(percentage).toBe(true);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('support "number" field type with the percentage attribute and custom range values', () => {
|
|
815
|
+
const result = createHeadlessForm(
|
|
816
|
+
JSONSchemaBuilder()
|
|
817
|
+
.addInput({
|
|
818
|
+
shares: {
|
|
819
|
+
...mockNumberInputWithPercentageAndCustomRange,
|
|
820
|
+
},
|
|
821
|
+
})
|
|
822
|
+
.setRequiredFields(['shares'])
|
|
823
|
+
.build()
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
expect(result).toMatchObject({
|
|
827
|
+
fields: [
|
|
828
|
+
{
|
|
829
|
+
description: 'What % of shares do you own?',
|
|
830
|
+
label: 'Shares',
|
|
831
|
+
name: 'shares',
|
|
832
|
+
percentage: true,
|
|
833
|
+
required: true,
|
|
834
|
+
schema: expect.any(Object),
|
|
835
|
+
type: 'number',
|
|
836
|
+
minimum: 50,
|
|
837
|
+
maximum: 70,
|
|
838
|
+
},
|
|
839
|
+
],
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
const fieldValidatorCustom = result.fields[0].schema;
|
|
843
|
+
const { percentage: percentageCustom } = result.fields[0];
|
|
844
|
+
expect(fieldValidatorCustom.isValidSync('0')).toBe(false);
|
|
845
|
+
expect(fieldValidatorCustom.isValidSync('49')).toBe(false);
|
|
846
|
+
expect(fieldValidatorCustom.isValidSync('55')).toBe(true);
|
|
847
|
+
expect(fieldValidatorCustom.isValidSync('70')).toBe(true);
|
|
848
|
+
expect(fieldValidatorCustom.isValidSync('101')).toBe(false);
|
|
849
|
+
expect(fieldValidatorCustom.isValidSync('this is text with a number 1')).toBe(false);
|
|
850
|
+
expect(() => fieldValidatorCustom.validateSync('some text')).toThrowError(
|
|
851
|
+
'The value must be a number'
|
|
852
|
+
);
|
|
853
|
+
expect(() => fieldValidatorCustom.validateSync('')).toThrowError(
|
|
854
|
+
'The value must be a number'
|
|
855
|
+
);
|
|
856
|
+
expect(percentageCustom).toBe(true);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it('support "date" field type', () => {
|
|
860
|
+
const result = createHeadlessForm(schemaInputTypeDate);
|
|
861
|
+
|
|
862
|
+
expect(result).toMatchObject({
|
|
863
|
+
fields: [
|
|
864
|
+
{
|
|
865
|
+
label: 'Birthdate',
|
|
866
|
+
name: 'birthdate',
|
|
867
|
+
required: true,
|
|
868
|
+
schema: expect.any(Object),
|
|
869
|
+
type: 'date',
|
|
870
|
+
maxLength: 10,
|
|
871
|
+
minDate: '1922-03-01',
|
|
872
|
+
maxDate: '2022-03-01',
|
|
873
|
+
},
|
|
874
|
+
],
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
const fieldValidator = result.fields[0].schema;
|
|
878
|
+
const todayDateHint = new Date().toISOString().substring(0, 10);
|
|
879
|
+
expect(fieldValidator.isValidSync('2020-10-10')).toBe(true);
|
|
880
|
+
expect(fieldValidator.isValidSync('2020-13-10')).toBe(false);
|
|
881
|
+
expect(() => fieldValidator.validateSync('')).toThrowError(
|
|
882
|
+
`Must be a valid date in yyyy-mm-dd format. e.g. ${todayDateHint}`
|
|
883
|
+
);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it('supports "file" field type', () => {
|
|
887
|
+
const result = createHeadlessForm(
|
|
888
|
+
JSONSchemaBuilder()
|
|
889
|
+
.addInput({
|
|
890
|
+
fileInput: mockFileInput,
|
|
891
|
+
})
|
|
892
|
+
.build()
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
expect(result).toMatchObject({
|
|
896
|
+
fields: [
|
|
897
|
+
{
|
|
898
|
+
type: 'file',
|
|
899
|
+
fileDownload: 'http://some.domain.com/file-name.pdf',
|
|
900
|
+
description: 'File Input Description',
|
|
901
|
+
fileName: 'My File',
|
|
902
|
+
label: 'File Input',
|
|
903
|
+
name: 'fileInput',
|
|
904
|
+
required: false,
|
|
905
|
+
accept: '.png,.jpg,.jpeg,.pdf',
|
|
906
|
+
},
|
|
907
|
+
],
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
describe('supports "group-array" field type', () => {
|
|
912
|
+
it('basic test', () => {
|
|
913
|
+
const result = createHeadlessForm(
|
|
914
|
+
JSONSchemaBuilder()
|
|
915
|
+
.addInput({
|
|
916
|
+
dependent_details: mockGroupArrayInput,
|
|
917
|
+
})
|
|
918
|
+
.build()
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
expect(result).toMatchObject({
|
|
922
|
+
fields: [
|
|
923
|
+
{
|
|
924
|
+
type: 'group-array',
|
|
925
|
+
description: 'Add the dependents you claim below',
|
|
926
|
+
label: 'Child details',
|
|
927
|
+
name: 'dependent_details',
|
|
928
|
+
required: false,
|
|
929
|
+
fields: expect.any(Function),
|
|
930
|
+
addFieldText: 'Add new field',
|
|
931
|
+
},
|
|
932
|
+
],
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
// Validations
|
|
936
|
+
const fieldValidator = result.fields[0].schema;
|
|
937
|
+
// nthfields are required
|
|
938
|
+
expect(
|
|
939
|
+
fieldValidator.isValidSync([
|
|
940
|
+
{
|
|
941
|
+
birthdate: '',
|
|
942
|
+
full_name: '',
|
|
943
|
+
sex: '',
|
|
944
|
+
},
|
|
945
|
+
])
|
|
946
|
+
).toBe(false);
|
|
947
|
+
// date is invalid
|
|
948
|
+
expect(
|
|
949
|
+
fieldValidator.isValidSync([
|
|
950
|
+
{
|
|
951
|
+
birthdate: 'invalidate date',
|
|
952
|
+
full_name: 'John Doe',
|
|
953
|
+
sex: 'male',
|
|
954
|
+
},
|
|
955
|
+
])
|
|
956
|
+
).toBe(false);
|
|
957
|
+
// all good
|
|
958
|
+
expect(
|
|
959
|
+
fieldValidator.isValidSync([
|
|
960
|
+
{
|
|
961
|
+
birthdate: '2021-12-04',
|
|
962
|
+
full_name: 'John Doe',
|
|
963
|
+
sex: 'male',
|
|
964
|
+
},
|
|
965
|
+
])
|
|
966
|
+
).toBe(true);
|
|
967
|
+
|
|
968
|
+
const nestedFieldsFromResult = result.fields[0].fields();
|
|
969
|
+
expect(nestedFieldsFromResult).toMatchObject([
|
|
970
|
+
{
|
|
971
|
+
type: 'text',
|
|
972
|
+
description: 'Enter your child’s full name',
|
|
973
|
+
maxLength: 255,
|
|
974
|
+
nameKey: 'full_name',
|
|
975
|
+
label: 'Child Full Name',
|
|
976
|
+
name: 'full_name',
|
|
977
|
+
required: true,
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
type: 'date',
|
|
981
|
+
name: 'birthdate',
|
|
982
|
+
label: 'Child Birthdate',
|
|
983
|
+
required: true,
|
|
984
|
+
description: 'Enter your child’s date of birth',
|
|
985
|
+
maxLength: 255,
|
|
986
|
+
nameKey: 'birthdate',
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
type: 'radio',
|
|
990
|
+
name: 'sex',
|
|
991
|
+
label: 'Child Sex',
|
|
992
|
+
options: [
|
|
993
|
+
{
|
|
994
|
+
label: 'Male',
|
|
995
|
+
value: 'male',
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
label: 'Female',
|
|
999
|
+
value: 'female',
|
|
1000
|
+
},
|
|
1001
|
+
],
|
|
1002
|
+
required: true,
|
|
1003
|
+
description:
|
|
1004
|
+
'We know sex is non-binary but for insurance and payroll purposes, we need to collect this information.',
|
|
1005
|
+
nameKey: 'sex',
|
|
1006
|
+
},
|
|
1007
|
+
]);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it('nested fields (native, core and custom) has correct validations', () => {
|
|
1011
|
+
const { handleValidation } = createHeadlessForm({
|
|
1012
|
+
properties: {
|
|
1013
|
+
break_schedule: {
|
|
1014
|
+
title: 'Work schedule',
|
|
1015
|
+
type: 'array',
|
|
1016
|
+
presentation: {
|
|
1017
|
+
inputType: 'group-array',
|
|
1018
|
+
},
|
|
1019
|
+
items: {
|
|
1020
|
+
properties: {
|
|
1021
|
+
minutes_native: {
|
|
1022
|
+
title: 'Minutes of break (native)',
|
|
1023
|
+
type: 'integer',
|
|
1024
|
+
minimum: 60,
|
|
1025
|
+
// without presentation.inputType
|
|
1026
|
+
},
|
|
1027
|
+
minutes_core: {
|
|
1028
|
+
title: 'Minutes of break (core)',
|
|
1029
|
+
type: 'integer',
|
|
1030
|
+
minimum: 60,
|
|
1031
|
+
presentation: {
|
|
1032
|
+
inputType: 'number', // a core inputType
|
|
1033
|
+
},
|
|
1034
|
+
},
|
|
1035
|
+
minutes_custom: {
|
|
1036
|
+
title: 'Minutes of break (custom)',
|
|
1037
|
+
type: 'integer',
|
|
1038
|
+
minimum: 60,
|
|
1039
|
+
presentation: {
|
|
1040
|
+
inputType: 'hour', // a custom inputType
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
required: ['weekday', 'minutes_native', 'minutes_core', 'minutes_custom'],
|
|
1045
|
+
},
|
|
1046
|
+
},
|
|
1047
|
+
},
|
|
1048
|
+
required: ['break_schedule'],
|
|
1049
|
+
});
|
|
1050
|
+
const validateForm = (vals) => friendlyError(handleValidation(vals));
|
|
1051
|
+
|
|
1052
|
+
// Given empty, it says it's required
|
|
1053
|
+
expect(validateForm({})).toEqual({
|
|
1054
|
+
break_schedule: 'Required field',
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// Given empty fields, it mentions nested required fields
|
|
1058
|
+
expect(
|
|
1059
|
+
validateForm({
|
|
1060
|
+
break_schedule: [{}],
|
|
1061
|
+
})
|
|
1062
|
+
).toEqual({
|
|
1063
|
+
break_schedule: [
|
|
1064
|
+
{
|
|
1065
|
+
minutes_native: 'Required field',
|
|
1066
|
+
minutes_core: 'Required field',
|
|
1067
|
+
minutes_custom: 'Required field',
|
|
1068
|
+
},
|
|
1069
|
+
],
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
// Given correct values, it's all valid.
|
|
1073
|
+
expect(
|
|
1074
|
+
validateForm({
|
|
1075
|
+
break_schedule: [
|
|
1076
|
+
{
|
|
1077
|
+
minutes_native: 60,
|
|
1078
|
+
minutes_core: 60,
|
|
1079
|
+
minutes_custom: 60,
|
|
1080
|
+
},
|
|
1081
|
+
],
|
|
1082
|
+
})
|
|
1083
|
+
).toBeUndefined();
|
|
1084
|
+
|
|
1085
|
+
// Given invalid values, the validation is triggered.
|
|
1086
|
+
expect(
|
|
1087
|
+
validateForm({
|
|
1088
|
+
break_schedule: [
|
|
1089
|
+
{
|
|
1090
|
+
minutes_native: 50,
|
|
1091
|
+
minutes_core: 50,
|
|
1092
|
+
minutes_custom: 50,
|
|
1093
|
+
},
|
|
1094
|
+
],
|
|
1095
|
+
})
|
|
1096
|
+
).toEqual({
|
|
1097
|
+
break_schedule: [
|
|
1098
|
+
{
|
|
1099
|
+
minutes_core: 'Must be greater or equal to 60',
|
|
1100
|
+
minutes_native: 'Must be greater or equal to 60',
|
|
1101
|
+
minutes_custom: 'Must be greater or equal to 60',
|
|
1102
|
+
},
|
|
1103
|
+
],
|
|
1104
|
+
});
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
it('can pass custom field attributes', () => {
|
|
1108
|
+
const result = createHeadlessForm(
|
|
1109
|
+
{
|
|
1110
|
+
properties: {
|
|
1111
|
+
children_basic: mockGroupArrayInput,
|
|
1112
|
+
children_custom: mockGroupArrayInput,
|
|
1113
|
+
},
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
customProperties: {
|
|
1117
|
+
children_custom: {
|
|
1118
|
+
'data-foo': 'baz',
|
|
1119
|
+
},
|
|
1120
|
+
},
|
|
1121
|
+
}
|
|
1122
|
+
);
|
|
1123
|
+
|
|
1124
|
+
expect(result).toMatchObject({
|
|
1125
|
+
fields: [
|
|
1126
|
+
{
|
|
1127
|
+
label: 'Child details',
|
|
1128
|
+
name: 'children_basic',
|
|
1129
|
+
required: false,
|
|
1130
|
+
type: 'group-array',
|
|
1131
|
+
inputType: 'group-array',
|
|
1132
|
+
jsonType: 'array',
|
|
1133
|
+
fields: expect.any(Function), // This is what makes the field work
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
label: 'Child details',
|
|
1137
|
+
name: 'children_custom',
|
|
1138
|
+
type: 'group-array',
|
|
1139
|
+
inputType: 'group-array',
|
|
1140
|
+
jsonType: 'array',
|
|
1141
|
+
required: false,
|
|
1142
|
+
'data-foo': 'baz', // check that custom property is properly propagated
|
|
1143
|
+
fields: expect.any(Function), // This is what makes the field work
|
|
1144
|
+
},
|
|
1145
|
+
],
|
|
1146
|
+
});
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
it('supports "fieldset" field type', () => {
|
|
1151
|
+
const result = createHeadlessForm(
|
|
1152
|
+
JSONSchemaBuilder()
|
|
1153
|
+
.addInput({
|
|
1154
|
+
fieldset: mockFieldset,
|
|
1155
|
+
})
|
|
1156
|
+
.build()
|
|
1157
|
+
);
|
|
1158
|
+
|
|
1159
|
+
expect(result).toMatchObject({
|
|
1160
|
+
fields: [
|
|
1161
|
+
{
|
|
1162
|
+
description: 'Fieldset description',
|
|
1163
|
+
label: 'Fieldset title',
|
|
1164
|
+
name: 'fieldset',
|
|
1165
|
+
type: 'fieldset',
|
|
1166
|
+
required: false,
|
|
1167
|
+
fields: [
|
|
1168
|
+
{
|
|
1169
|
+
description: 'The number of your national identification (max 10 digits)',
|
|
1170
|
+
label: 'ID number',
|
|
1171
|
+
name: 'id_number',
|
|
1172
|
+
type: 'text',
|
|
1173
|
+
required: true,
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
description: 'How many open tabs do you have?',
|
|
1177
|
+
label: 'Tabs',
|
|
1178
|
+
maximum: 10,
|
|
1179
|
+
minimum: 1,
|
|
1180
|
+
name: 'tabs',
|
|
1181
|
+
type: 'number',
|
|
1182
|
+
required: false,
|
|
1183
|
+
},
|
|
1184
|
+
],
|
|
1185
|
+
},
|
|
1186
|
+
],
|
|
1187
|
+
});
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
it('supports "radio" field type with its "card" and "card-expandable" variants', () => {
|
|
1191
|
+
const result = createHeadlessForm(
|
|
1192
|
+
JSONSchemaBuilder()
|
|
1193
|
+
.addInput({
|
|
1194
|
+
experience_level: mockRadioCardExpandableInput,
|
|
1195
|
+
payment_method: mockRadioCardInput,
|
|
1196
|
+
})
|
|
1197
|
+
.build()
|
|
1198
|
+
);
|
|
1199
|
+
|
|
1200
|
+
expect(result).toMatchObject({
|
|
1201
|
+
fields: [
|
|
1202
|
+
{
|
|
1203
|
+
description:
|
|
1204
|
+
'Please select the experience level that aligns with this role based on the job description (not the employees overall experience)',
|
|
1205
|
+
label: 'Experience level',
|
|
1206
|
+
name: 'experience_level',
|
|
1207
|
+
type: 'radio',
|
|
1208
|
+
required: false,
|
|
1209
|
+
variant: 'card-expandable',
|
|
1210
|
+
options: [
|
|
1211
|
+
{
|
|
1212
|
+
label: 'Junior level',
|
|
1213
|
+
value: 'junior',
|
|
1214
|
+
description:
|
|
1215
|
+
'Entry level employees who perform tasks under the supervision of a more experienced employee.',
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
label: 'Mid level',
|
|
1219
|
+
value: 'mid',
|
|
1220
|
+
description:
|
|
1221
|
+
'Employees who perform tasks with a good degree of autonomy and/or with coordination and control functions.',
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
label: 'Senior level',
|
|
1225
|
+
value: 'senior',
|
|
1226
|
+
description:
|
|
1227
|
+
'Employees who perform tasks with a high degree of autonomy and/or with coordination and control functions.',
|
|
1228
|
+
},
|
|
1229
|
+
],
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
description: 'Chose how you want to be paid',
|
|
1233
|
+
label: 'Payment method',
|
|
1234
|
+
name: 'payment_method',
|
|
1235
|
+
type: 'radio',
|
|
1236
|
+
variant: 'card',
|
|
1237
|
+
required: false,
|
|
1238
|
+
options: [
|
|
1239
|
+
{
|
|
1240
|
+
label: 'Credit Card',
|
|
1241
|
+
value: 'cc',
|
|
1242
|
+
description: 'Plastic money, which is still money',
|
|
1243
|
+
},
|
|
1244
|
+
{
|
|
1245
|
+
label: 'Cash',
|
|
1246
|
+
value: 'cash',
|
|
1247
|
+
description: 'Rules Everything Around Me',
|
|
1248
|
+
},
|
|
1249
|
+
],
|
|
1250
|
+
},
|
|
1251
|
+
],
|
|
1252
|
+
});
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
it('supports nested "fieldset" field type', () => {
|
|
1256
|
+
const result = createHeadlessForm(
|
|
1257
|
+
JSONSchemaBuilder()
|
|
1258
|
+
.addInput({
|
|
1259
|
+
nestedFieldset: mockNestedFieldset,
|
|
1260
|
+
})
|
|
1261
|
+
.build()
|
|
1262
|
+
);
|
|
1263
|
+
|
|
1264
|
+
expect(result).toMatchObject({
|
|
1265
|
+
fields: [
|
|
1266
|
+
{
|
|
1267
|
+
label: 'Nested fieldset title',
|
|
1268
|
+
description: 'Nested fieldset description',
|
|
1269
|
+
name: 'nestedFieldset',
|
|
1270
|
+
type: 'fieldset',
|
|
1271
|
+
required: false,
|
|
1272
|
+
fields: [
|
|
1273
|
+
{
|
|
1274
|
+
description: 'Fieldset description',
|
|
1275
|
+
label: 'Fieldset title',
|
|
1276
|
+
name: 'innerFieldset',
|
|
1277
|
+
type: 'fieldset',
|
|
1278
|
+
required: false,
|
|
1279
|
+
fields: [
|
|
1280
|
+
{
|
|
1281
|
+
description: 'The number of your national identification (max 10 digits)',
|
|
1282
|
+
label: 'ID number',
|
|
1283
|
+
name: 'id_number',
|
|
1284
|
+
type: 'text',
|
|
1285
|
+
required: true,
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
description: 'How many open tabs do you have?',
|
|
1289
|
+
label: 'Tabs',
|
|
1290
|
+
maximum: 10,
|
|
1291
|
+
minimum: 1,
|
|
1292
|
+
name: 'tabs',
|
|
1293
|
+
type: 'number',
|
|
1294
|
+
required: false,
|
|
1295
|
+
},
|
|
1296
|
+
],
|
|
1297
|
+
},
|
|
1298
|
+
],
|
|
1299
|
+
},
|
|
1300
|
+
],
|
|
1301
|
+
});
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
it('supported "fieldset" with scoped conditionals', () => {
|
|
1305
|
+
const { handleValidation } = createHeadlessForm(schemaFieldsetScopedCondition, {});
|
|
1306
|
+
const validateForm = (vals) => friendlyError(handleValidation(vals));
|
|
1307
|
+
|
|
1308
|
+
// The "child.has_child" is required
|
|
1309
|
+
expect(validateForm({})).toEqual({
|
|
1310
|
+
child: {
|
|
1311
|
+
has_child: 'Required field',
|
|
1312
|
+
},
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
// The "child.no" is valid
|
|
1316
|
+
expect(
|
|
1317
|
+
validateForm({
|
|
1318
|
+
child: {
|
|
1319
|
+
has_child: 'no',
|
|
1320
|
+
},
|
|
1321
|
+
})
|
|
1322
|
+
).toBeUndefined();
|
|
1323
|
+
|
|
1324
|
+
// Invalid because it expect child.age too
|
|
1325
|
+
expect(
|
|
1326
|
+
validateForm({
|
|
1327
|
+
child: {
|
|
1328
|
+
has_child: 'yes',
|
|
1329
|
+
},
|
|
1330
|
+
})
|
|
1331
|
+
).toEqual({
|
|
1332
|
+
child: {
|
|
1333
|
+
age: 'Required field',
|
|
1334
|
+
},
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
// Valid without optional child.passport_id
|
|
1338
|
+
expect(
|
|
1339
|
+
validateForm({
|
|
1340
|
+
child: {
|
|
1341
|
+
has_child: 'yes',
|
|
1342
|
+
age: 15,
|
|
1343
|
+
},
|
|
1344
|
+
})
|
|
1345
|
+
).toBeUndefined();
|
|
1346
|
+
|
|
1347
|
+
// Valid with optional child.passport_id
|
|
1348
|
+
expect(
|
|
1349
|
+
validateForm({
|
|
1350
|
+
child: {
|
|
1351
|
+
has_child: 'yes',
|
|
1352
|
+
age: 15,
|
|
1353
|
+
passport_id: 'asdf',
|
|
1354
|
+
},
|
|
1355
|
+
})
|
|
1356
|
+
).toBeUndefined();
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
it('should set any nested "fieldset" form values to null when they are invisible', async () => {
|
|
1360
|
+
const { handleValidation } = createHeadlessForm(schemaFieldsetScopedCondition, {});
|
|
1361
|
+
const validateForm = (vals) => friendlyError(handleValidation(vals));
|
|
1362
|
+
|
|
1363
|
+
const formValues = {
|
|
1364
|
+
child: {
|
|
1365
|
+
has_child: 'yes',
|
|
1366
|
+
age: 15,
|
|
1367
|
+
},
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
await expect(validateForm(formValues)).toBeUndefined();
|
|
1371
|
+
expect(formValues.child.age).toBe(15);
|
|
1372
|
+
|
|
1373
|
+
formValues.child.has_child = 'no';
|
|
1374
|
+
// form value updates re-validate; see computeYupSchema()
|
|
1375
|
+
await expect(validateForm(formValues)).toBeUndefined();
|
|
1376
|
+
|
|
1377
|
+
// when child.has_child is 'no' child.age is invisible
|
|
1378
|
+
expect(formValues.child.age).toBe(null);
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
it('support "email" field type', () => {
|
|
1382
|
+
const result = createHeadlessForm(schemaInputTypeEmail);
|
|
1383
|
+
|
|
1384
|
+
expect(result).toMatchObject({
|
|
1385
|
+
fields: [
|
|
1386
|
+
{
|
|
1387
|
+
description: 'Enter your email address',
|
|
1388
|
+
label: 'Email address',
|
|
1389
|
+
name: 'email_address',
|
|
1390
|
+
required: true,
|
|
1391
|
+
schema: expect.any(Object),
|
|
1392
|
+
type: 'email',
|
|
1393
|
+
maxLength: 255,
|
|
1394
|
+
},
|
|
1395
|
+
],
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
const fieldValidator = result.fields[0].schema;
|
|
1399
|
+
expect(fieldValidator.isValidSync('test@gmail.com')).toBe(true);
|
|
1400
|
+
expect(() => fieldValidator.validateSync('ffsdf')).toThrowError(
|
|
1401
|
+
'Please enter a valid email address'
|
|
1402
|
+
);
|
|
1403
|
+
expect(() => fieldValidator.validateSync(undefined)).toThrowError('Required field');
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
describe('supports "checkbox" field type', () => {
|
|
1407
|
+
describe('checkbox as string', () => {
|
|
1408
|
+
it('required: only accept the value in "checkboxValue"', () => {
|
|
1409
|
+
const result = createHeadlessForm(schemaInputTypeCheckbox);
|
|
1410
|
+
const checkboxField = result.fields.find((field) => field.name === 'contract_duration');
|
|
1411
|
+
|
|
1412
|
+
expect(checkboxField).toMatchObject({
|
|
1413
|
+
description:
|
|
1414
|
+
'I acknowledge that all employees in France will be hired on indefinite contracts.',
|
|
1415
|
+
label: 'Contract duration',
|
|
1416
|
+
name: 'contract_duration',
|
|
1417
|
+
type: 'checkbox',
|
|
1418
|
+
checkboxValue: 'Permanent',
|
|
1419
|
+
});
|
|
1420
|
+
expect(checkboxField).not.toHaveProperty('default'); // ensure it's not checked by default.
|
|
1421
|
+
|
|
1422
|
+
const fieldValidator = checkboxField.schema;
|
|
1423
|
+
expect(fieldValidator.isValidSync('Permanent')).toBe(true);
|
|
1424
|
+
expect(() => fieldValidator.validateSync(undefined)).toThrowError(
|
|
1425
|
+
'Please acknowledge this field'
|
|
1426
|
+
);
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
it('required checked: returns a default value', () => {
|
|
1430
|
+
const result = createHeadlessForm(schemaInputTypeCheckbox);
|
|
1431
|
+
const checkboxField = result.fields.find(
|
|
1432
|
+
(field) => field.name === 'contract_duration_checked'
|
|
1433
|
+
);
|
|
1434
|
+
|
|
1435
|
+
expect(checkboxField).toMatchObject({
|
|
1436
|
+
default: 'Permanent',
|
|
1437
|
+
checkboxValue: 'Permanent',
|
|
1438
|
+
});
|
|
1439
|
+
});
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
describe('checkbox as boolean', () => {
|
|
1443
|
+
it('optional: Accepts true or false', () => {
|
|
1444
|
+
const result = createHeadlessForm(schemaInputTypeCheckboxBooleans);
|
|
1445
|
+
const checkboxField = result.fields.find((field) => field.name === 'boolean_empty');
|
|
1446
|
+
|
|
1447
|
+
expect(checkboxField).toMatchObject({
|
|
1448
|
+
checkboxValue: true,
|
|
1449
|
+
});
|
|
1450
|
+
expect(checkboxField).not.toHaveProperty('default'); // ensure it's not checked by default.
|
|
1451
|
+
|
|
1452
|
+
const fieldValidator = checkboxField.schema;
|
|
1453
|
+
expect(fieldValidator.isValidSync(true)).toBe(true);
|
|
1454
|
+
expect(fieldValidator.isValidSync(false)).toBe(true);
|
|
1455
|
+
expect(fieldValidator.isValidSync(undefined)).toBe(true);
|
|
1456
|
+
expect(() => fieldValidator.validateSync('foo')).toThrowError(
|
|
1457
|
+
'this must be a `boolean` type, but the final value was: `"foo"`.'
|
|
1458
|
+
);
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
it('required: Only accepts true', () => {
|
|
1462
|
+
const result = createHeadlessForm(schemaInputTypeCheckboxBooleans);
|
|
1463
|
+
const checkboxField = result.fields.find((field) => field.name === 'boolean_required');
|
|
1464
|
+
|
|
1465
|
+
expect(checkboxField).toMatchObject({
|
|
1466
|
+
checkboxValue: true,
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
const fieldValidator = checkboxField.schema;
|
|
1470
|
+
expect(fieldValidator.isValidSync(true)).toBe(true);
|
|
1471
|
+
expect(() => fieldValidator.validateSync(false)).toThrowError(
|
|
1472
|
+
'Please acknowledge this field'
|
|
1473
|
+
);
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
it('checked: returns default: true', () => {
|
|
1477
|
+
const result = createHeadlessForm(schemaInputTypeCheckboxBooleans);
|
|
1478
|
+
const checkboxField = result.fields.find((field) => field.name === 'boolean_checked');
|
|
1479
|
+
|
|
1480
|
+
expect(checkboxField).toMatchObject({
|
|
1481
|
+
checkboxValue: true,
|
|
1482
|
+
default: true,
|
|
1483
|
+
});
|
|
1484
|
+
});
|
|
1485
|
+
});
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
describe('supports custom inputType (eg "hour")', () => {
|
|
1489
|
+
it('as required, optional, and mixed types', () => {
|
|
1490
|
+
const { fields, handleValidation } = createHeadlessForm(
|
|
1491
|
+
{
|
|
1492
|
+
properties: {
|
|
1493
|
+
start_time: {
|
|
1494
|
+
title: 'Starting time',
|
|
1495
|
+
type: 'string',
|
|
1496
|
+
presentation: {
|
|
1497
|
+
inputType: 'hour',
|
|
1498
|
+
},
|
|
1499
|
+
},
|
|
1500
|
+
pause: {
|
|
1501
|
+
title: 'Pause time (optional)',
|
|
1502
|
+
type: 'string',
|
|
1503
|
+
presentation: {
|
|
1504
|
+
inputType: 'hour',
|
|
1505
|
+
},
|
|
1506
|
+
},
|
|
1507
|
+
end_time: {
|
|
1508
|
+
title: 'Finishing time (optional)',
|
|
1509
|
+
type: ['null', 'string'], // ensure it supports mix types (array) (optional/null)
|
|
1510
|
+
presentation: {
|
|
1511
|
+
inputType: 'hour',
|
|
1512
|
+
},
|
|
1513
|
+
},
|
|
1514
|
+
},
|
|
1515
|
+
required: ['start_time'],
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
strictInputType: false,
|
|
1519
|
+
}
|
|
1520
|
+
);
|
|
1521
|
+
const validateForm = (vals) => friendlyError(handleValidation(vals));
|
|
1522
|
+
|
|
1523
|
+
const commonAttrs = {
|
|
1524
|
+
type: 'hour',
|
|
1525
|
+
inputType: 'hour',
|
|
1526
|
+
jsonType: 'string',
|
|
1527
|
+
schema: expect.any(Object),
|
|
1528
|
+
};
|
|
1529
|
+
expect(fields).toMatchObject([
|
|
1530
|
+
{
|
|
1531
|
+
name: 'start_time',
|
|
1532
|
+
label: 'Starting time',
|
|
1533
|
+
...commonAttrs,
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
name: 'pause',
|
|
1537
|
+
label: 'Pause time (optional)',
|
|
1538
|
+
...commonAttrs,
|
|
1539
|
+
},
|
|
1540
|
+
{
|
|
1541
|
+
name: 'end_time',
|
|
1542
|
+
label: 'Finishing time (optional)',
|
|
1543
|
+
...commonAttrs,
|
|
1544
|
+
jsonType: ['null', 'string'],
|
|
1545
|
+
},
|
|
1546
|
+
]);
|
|
1547
|
+
|
|
1548
|
+
expect(validateForm({})).toEqual({
|
|
1549
|
+
start_time: 'Required field',
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
expect(validateForm({ start_time: '08:30' })).toBeUndefined();
|
|
1553
|
+
});
|
|
1554
|
+
});
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
describe('validation options', () => {
|
|
1558
|
+
it('given invalid values it returns both yupError and formErrors', () => {
|
|
1559
|
+
const { handleValidation } = createHeadlessForm(schemaInputTypeText);
|
|
1560
|
+
|
|
1561
|
+
const { formErrors, yupError } = handleValidation({});
|
|
1562
|
+
|
|
1563
|
+
// Assert the yupError shape is really a YupError
|
|
1564
|
+
expect(yupError).toEqual(expect.any(Error));
|
|
1565
|
+
expect(yupError.inner[0].path).toBe('id_number');
|
|
1566
|
+
expect(yupError.inner[0].message).toBe('Required field');
|
|
1567
|
+
|
|
1568
|
+
// Assert the converted YupError to formErrors
|
|
1569
|
+
expect(formErrors).toEqual({
|
|
1570
|
+
id_number: 'Required field',
|
|
1571
|
+
});
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
describe('x-jsf-presentation attribute', () => {
|
|
1576
|
+
it('support field with "x-jsf-presentation.statement"', () => {
|
|
1577
|
+
const result = createHeadlessForm(schemaInputWithStatement);
|
|
1578
|
+
|
|
1579
|
+
expect(result).toMatchObject({
|
|
1580
|
+
fields: [
|
|
1581
|
+
{
|
|
1582
|
+
name: 'bonus',
|
|
1583
|
+
label: 'Bonus',
|
|
1584
|
+
type: 'text',
|
|
1585
|
+
statement: {
|
|
1586
|
+
description: 'This is a custom statement message.',
|
|
1587
|
+
inputType: 'statement',
|
|
1588
|
+
severity: 'info',
|
|
1589
|
+
},
|
|
1590
|
+
},
|
|
1591
|
+
{
|
|
1592
|
+
name: 'a_or_b',
|
|
1593
|
+
label: 'A dropdown',
|
|
1594
|
+
type: 'select',
|
|
1595
|
+
statement: {
|
|
1596
|
+
description: 'This is another statement message, but more severe.',
|
|
1597
|
+
inputType: 'statement',
|
|
1598
|
+
severity: 'warning',
|
|
1599
|
+
},
|
|
1600
|
+
},
|
|
1601
|
+
],
|
|
1602
|
+
});
|
|
1603
|
+
});
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
describe('property misc attributes', () => {
|
|
1607
|
+
it('pass readOnly to field', () => {
|
|
1608
|
+
const result = createHeadlessForm({
|
|
1609
|
+
properties: {
|
|
1610
|
+
secret: {
|
|
1611
|
+
title: 'Secret code',
|
|
1612
|
+
readOnly: true,
|
|
1613
|
+
type: 'string',
|
|
1614
|
+
presentation: {
|
|
1615
|
+
inputType: 'text',
|
|
1616
|
+
},
|
|
1617
|
+
},
|
|
1618
|
+
},
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
expect(result).toMatchObject({
|
|
1622
|
+
fields: [
|
|
1623
|
+
{
|
|
1624
|
+
name: 'secret',
|
|
1625
|
+
label: 'Secret code',
|
|
1626
|
+
schema: expect.any(Object),
|
|
1627
|
+
readOnly: true,
|
|
1628
|
+
},
|
|
1629
|
+
],
|
|
1630
|
+
});
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
it('pass "deprecated" attributes to field', () => {
|
|
1634
|
+
const result = createHeadlessForm({
|
|
1635
|
+
properties: {
|
|
1636
|
+
secret: {
|
|
1637
|
+
title: 'Age',
|
|
1638
|
+
type: 'number',
|
|
1639
|
+
deprecated: true,
|
|
1640
|
+
presentation: {
|
|
1641
|
+
inputType: 'number',
|
|
1642
|
+
deprecated: {
|
|
1643
|
+
description: 'Deprecated in favor of "birthdate".',
|
|
1644
|
+
},
|
|
1645
|
+
},
|
|
1646
|
+
},
|
|
1647
|
+
},
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1650
|
+
expect(result).toMatchObject({
|
|
1651
|
+
fields: [
|
|
1652
|
+
{
|
|
1653
|
+
type: 'number',
|
|
1654
|
+
name: 'secret',
|
|
1655
|
+
label: 'Age',
|
|
1656
|
+
schema: expect.any(Object),
|
|
1657
|
+
deprecated: {
|
|
1658
|
+
description: 'Deprecated in favor of "birthdate".',
|
|
1659
|
+
},
|
|
1660
|
+
},
|
|
1661
|
+
],
|
|
1662
|
+
});
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
it('pass "description" to field', () => {
|
|
1666
|
+
const result = createHeadlessForm({
|
|
1667
|
+
properties: {
|
|
1668
|
+
plain: {
|
|
1669
|
+
title: 'Regular',
|
|
1670
|
+
description: 'I am regular',
|
|
1671
|
+
presentation: { inputType: 'text' },
|
|
1672
|
+
},
|
|
1673
|
+
html: {
|
|
1674
|
+
title: 'Name',
|
|
1675
|
+
description: 'I am regular',
|
|
1676
|
+
presentation: {
|
|
1677
|
+
description: 'I am <b>bold</b>.',
|
|
1678
|
+
inputType: 'text',
|
|
1679
|
+
},
|
|
1680
|
+
},
|
|
1681
|
+
},
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1684
|
+
expect(result).toMatchObject({
|
|
1685
|
+
fields: [
|
|
1686
|
+
{ description: 'I am regular' },
|
|
1687
|
+
{ description: '<span class="jsf-description">I am <b>bold</b>.</span>' },
|
|
1688
|
+
],
|
|
1689
|
+
});
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
it('passes scopedJsonSchema to each field', () => {
|
|
1693
|
+
const { fields } = createHeadlessForm(schemaWithoutInputTypes, {
|
|
1694
|
+
strictInputType: false,
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
const getByName = (fieldList, name) => fieldList.find((f) => f.name === name);
|
|
1698
|
+
|
|
1699
|
+
const aFieldInRoot = getByName(fields, 'a_string');
|
|
1700
|
+
// It's the entire json schema
|
|
1701
|
+
expect(aFieldInRoot.scopedJsonSchema).toEqual(schemaWithoutInputTypes);
|
|
1702
|
+
|
|
1703
|
+
const aFieldset = getByName(fields, 'a_object');
|
|
1704
|
+
const aFieldInTheFieldset = getByName(aFieldset.fields, 'foo');
|
|
1705
|
+
|
|
1706
|
+
// It's only the json schema of that fieldset
|
|
1707
|
+
expect(aFieldInTheFieldset.scopedJsonSchema).toEqual(
|
|
1708
|
+
schemaWithoutInputTypes.properties.a_object
|
|
1709
|
+
);
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
describe('Order of fields', () => {
|
|
1713
|
+
it('sorts fields based on presentation.position keyword (deprecated)', () => {
|
|
1714
|
+
const { fields } = createHeadlessForm(schemaWithPositionDeprecated);
|
|
1715
|
+
|
|
1716
|
+
// Assert the order from the original schema object
|
|
1717
|
+
expect(Object.keys(schemaWithPositionDeprecated.properties)).toEqual([
|
|
1718
|
+
'age',
|
|
1719
|
+
'street',
|
|
1720
|
+
'username',
|
|
1721
|
+
]);
|
|
1722
|
+
expect(Object.keys(schemaWithPositionDeprecated.properties.street.properties)).toEqual([
|
|
1723
|
+
'line_one',
|
|
1724
|
+
'postal_code',
|
|
1725
|
+
'number',
|
|
1726
|
+
]);
|
|
1727
|
+
|
|
1728
|
+
// Assert the Fields order
|
|
1729
|
+
const fieldsByName = fields.map((f) => f.name);
|
|
1730
|
+
expect(fieldsByName).toEqual(['username', 'age', 'street']);
|
|
1731
|
+
|
|
1732
|
+
const fieldsetByName = fields[2].fields.map((f) => f.name);
|
|
1733
|
+
expect(fieldsetByName).toEqual(['line_one', 'number', 'postal_code']);
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
it('sorts fields based on x-jsf-order keyword', () => {
|
|
1737
|
+
const { fields } = createHeadlessForm(schemaWithOrderKeyword);
|
|
1738
|
+
|
|
1739
|
+
// Assert the order from the original schema object
|
|
1740
|
+
expect(Object.keys(schemaWithOrderKeyword.properties)).toEqual([
|
|
1741
|
+
'age',
|
|
1742
|
+
'street',
|
|
1743
|
+
'username',
|
|
1744
|
+
]);
|
|
1745
|
+
expect(Object.keys(schemaWithOrderKeyword.properties.street.properties)).toEqual([
|
|
1746
|
+
'line_one',
|
|
1747
|
+
'postal_code',
|
|
1748
|
+
'number',
|
|
1749
|
+
]);
|
|
1750
|
+
|
|
1751
|
+
// Assert the Fields order
|
|
1752
|
+
const fieldsByName = fields.map((f) => f.name);
|
|
1753
|
+
expect(fieldsByName).toEqual(['username', 'age', 'street']);
|
|
1754
|
+
|
|
1755
|
+
const fieldsetByName = fields[2].fields.map((f) => f.name);
|
|
1756
|
+
expect(fieldsetByName).toEqual(['line_one', 'number', 'postal_code']);
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
it('sorts fields based on original properties (wihout x-jsf-order)', () => {
|
|
1760
|
+
// Assert the sample schema has x-jsf-order
|
|
1761
|
+
expect(schemaWithOrderKeyword['x-jsf-order']).toBeDefined();
|
|
1762
|
+
|
|
1763
|
+
const schemaWithoutOrder = {
|
|
1764
|
+
...schemaWithOrderKeyword,
|
|
1765
|
+
'x-jsf-order': undefined,
|
|
1766
|
+
};
|
|
1767
|
+
const { fields } = createHeadlessForm(schemaWithoutOrder);
|
|
1768
|
+
|
|
1769
|
+
const originalOrder = ['age', 'street', 'username'];
|
|
1770
|
+
// Assert the order from the original schema object
|
|
1771
|
+
expect(Object.keys(schemaWithoutOrder.properties)).toEqual(originalOrder);
|
|
1772
|
+
|
|
1773
|
+
// Assert the order of fields is the same as the original object
|
|
1774
|
+
const fieldsByName = fields.map((f) => f.name);
|
|
1775
|
+
expect(fieldsByName).toEqual(originalOrder);
|
|
1776
|
+
});
|
|
1777
|
+
});
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
describe('when a field is required', () => {
|
|
1781
|
+
let fields;
|
|
1782
|
+
beforeEach(() => {
|
|
1783
|
+
const result = createHeadlessForm(
|
|
1784
|
+
buildJSONSchemaInput({ presentationFields: { inputType: 'text' }, required: true })
|
|
1785
|
+
);
|
|
1786
|
+
fields = result.fields;
|
|
1787
|
+
});
|
|
1788
|
+
describe('and value is empty', () => {
|
|
1789
|
+
it('should throw an error', async () =>
|
|
1790
|
+
expect(
|
|
1791
|
+
object()
|
|
1792
|
+
.shape({
|
|
1793
|
+
test: fields[0].schema,
|
|
1794
|
+
})
|
|
1795
|
+
.validate({ test: '' })
|
|
1796
|
+
).rejects.toMatchObject({ errors: ['Required field'] }));
|
|
1797
|
+
});
|
|
1798
|
+
describe('and value is defined', () => {
|
|
1799
|
+
it('should validate field', async () => {
|
|
1800
|
+
const assertObj = { test: 'Hello' };
|
|
1801
|
+
return expect(
|
|
1802
|
+
object()
|
|
1803
|
+
.shape({
|
|
1804
|
+
test: fields[0].schema,
|
|
1805
|
+
})
|
|
1806
|
+
.validate(assertObj)
|
|
1807
|
+
).resolves.toEqual(assertObj);
|
|
1808
|
+
});
|
|
1809
|
+
});
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
describe('when a field is number', () => {
|
|
1813
|
+
let fields;
|
|
1814
|
+
beforeEach(() => {
|
|
1815
|
+
const result = createHeadlessForm(
|
|
1816
|
+
buildJSONSchemaInput({ presentationFields: { inputType: 'number' } })
|
|
1817
|
+
);
|
|
1818
|
+
fields = result.fields;
|
|
1819
|
+
});
|
|
1820
|
+
describe('and value is a string', () => {
|
|
1821
|
+
it('should throw an error', async () =>
|
|
1822
|
+
expect(
|
|
1823
|
+
object()
|
|
1824
|
+
.shape({
|
|
1825
|
+
test: fields[0].schema,
|
|
1826
|
+
})
|
|
1827
|
+
.validate({ test: 'Hello' })
|
|
1828
|
+
).rejects.toThrow());
|
|
1829
|
+
});
|
|
1830
|
+
describe('and value is a number', () => {
|
|
1831
|
+
it('should validate field', async () => {
|
|
1832
|
+
const assertObj = { test: 3 };
|
|
1833
|
+
return expect(
|
|
1834
|
+
object()
|
|
1835
|
+
.shape({
|
|
1836
|
+
test: fields[0].schema,
|
|
1837
|
+
})
|
|
1838
|
+
.validate(assertObj)
|
|
1839
|
+
).resolves.toEqual(assertObj);
|
|
1840
|
+
});
|
|
1841
|
+
});
|
|
1842
|
+
});
|
|
1843
|
+
|
|
1844
|
+
describe('when a field has a maxLength of 10', () => {
|
|
1845
|
+
let fields;
|
|
1846
|
+
beforeEach(() => {
|
|
1847
|
+
const result = createHeadlessForm(
|
|
1848
|
+
buildJSONSchemaInput({
|
|
1849
|
+
presentationFields: { inputType: 'text' },
|
|
1850
|
+
inputFields: { maxLength: 10 },
|
|
1851
|
+
})
|
|
1852
|
+
);
|
|
1853
|
+
fields = result.fields;
|
|
1854
|
+
});
|
|
1855
|
+
describe('and value is greater than that', () => {
|
|
1856
|
+
it('should throw an error', async () =>
|
|
1857
|
+
expect(
|
|
1858
|
+
object()
|
|
1859
|
+
.shape({
|
|
1860
|
+
test: fields[0].schema,
|
|
1861
|
+
})
|
|
1862
|
+
.validate({ test: 'Hello Mr John Doe' })
|
|
1863
|
+
).rejects.toMatchObject({ errors: ['Please insert up to 10 characters'] }));
|
|
1864
|
+
});
|
|
1865
|
+
describe('and value is less than that', () => {
|
|
1866
|
+
it('should validate field', async () => {
|
|
1867
|
+
const assertObj = { test: 'Hello John' };
|
|
1868
|
+
return expect(
|
|
1869
|
+
object()
|
|
1870
|
+
.shape({
|
|
1871
|
+
test: fields[0].schema,
|
|
1872
|
+
})
|
|
1873
|
+
.validate(assertObj)
|
|
1874
|
+
).resolves.toEqual(assertObj);
|
|
1875
|
+
});
|
|
1876
|
+
});
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
describe('when a field has a minLength of 2', () => {
|
|
1880
|
+
let fields;
|
|
1881
|
+
beforeEach(() => {
|
|
1882
|
+
const result = createHeadlessForm(
|
|
1883
|
+
buildJSONSchemaInput({
|
|
1884
|
+
presentationFields: { inputType: 'text' },
|
|
1885
|
+
inputFields: { minLength: 2 },
|
|
1886
|
+
})
|
|
1887
|
+
);
|
|
1888
|
+
fields = result.fields;
|
|
1889
|
+
});
|
|
1890
|
+
describe('and value is smaller than that', () => {
|
|
1891
|
+
it('should throw an error', async () =>
|
|
1892
|
+
expect(
|
|
1893
|
+
object()
|
|
1894
|
+
.shape({
|
|
1895
|
+
test: fields[0].schema,
|
|
1896
|
+
})
|
|
1897
|
+
.validate({ test: 'H' })
|
|
1898
|
+
).rejects.toMatchObject({ errors: ['Please insert at least 2 characters'] }));
|
|
1899
|
+
});
|
|
1900
|
+
describe('and value is greater than that', () => {
|
|
1901
|
+
it('should validate field', async () => {
|
|
1902
|
+
const assertObj = { test: 'Hello John' };
|
|
1903
|
+
return expect(
|
|
1904
|
+
object()
|
|
1905
|
+
.shape({
|
|
1906
|
+
test: fields[0].schema,
|
|
1907
|
+
})
|
|
1908
|
+
.validate(assertObj)
|
|
1909
|
+
).resolves.toEqual(assertObj);
|
|
1910
|
+
});
|
|
1911
|
+
});
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
describe('when a field has a minimum of 0', () => {
|
|
1915
|
+
let fields;
|
|
1916
|
+
beforeEach(() => {
|
|
1917
|
+
const result = createHeadlessForm(
|
|
1918
|
+
buildJSONSchemaInput({
|
|
1919
|
+
presentationFields: { inputType: 'number' },
|
|
1920
|
+
inputFields: { minimum: 0 },
|
|
1921
|
+
})
|
|
1922
|
+
);
|
|
1923
|
+
fields = result.fields;
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
describe('and value is less than that', () => {
|
|
1927
|
+
it('should throw an error', async () =>
|
|
1928
|
+
expect(
|
|
1929
|
+
object()
|
|
1930
|
+
.shape({
|
|
1931
|
+
test: fields[0].schema,
|
|
1932
|
+
})
|
|
1933
|
+
.validate({ test: -1 })
|
|
1934
|
+
).rejects.toMatchObject({ errors: ['Must be greater or equal to 0'] }));
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
describe('and value is greater than that', () => {
|
|
1938
|
+
it('should validate field', async () => {
|
|
1939
|
+
const assertObj = { test: 4 };
|
|
1940
|
+
return expect(
|
|
1941
|
+
object()
|
|
1942
|
+
.shape({
|
|
1943
|
+
test: fields[0].schema,
|
|
1944
|
+
})
|
|
1945
|
+
.validate(assertObj)
|
|
1946
|
+
).resolves.toEqual(assertObj);
|
|
1947
|
+
});
|
|
1948
|
+
});
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
describe('when a field has a maximum of 10', () => {
|
|
1952
|
+
let fields;
|
|
1953
|
+
beforeEach(() => {
|
|
1954
|
+
const result = createHeadlessForm(
|
|
1955
|
+
buildJSONSchemaInput({
|
|
1956
|
+
presentationFields: { inputType: 'number' },
|
|
1957
|
+
inputFields: { maximum: 10 },
|
|
1958
|
+
})
|
|
1959
|
+
);
|
|
1960
|
+
fields = result.fields;
|
|
1961
|
+
});
|
|
1962
|
+
|
|
1963
|
+
describe('and value is greater than that', () => {
|
|
1964
|
+
it('should throw an error', async () =>
|
|
1965
|
+
expect(
|
|
1966
|
+
object()
|
|
1967
|
+
.shape({
|
|
1968
|
+
test: fields[0].schema,
|
|
1969
|
+
})
|
|
1970
|
+
.validate({ test: 11 })
|
|
1971
|
+
).rejects.toMatchObject({ errors: ['Must be smaller or equal to 10'] }));
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
describe('and value is greater than that', () => {
|
|
1975
|
+
it('should validate field', async () => {
|
|
1976
|
+
const assertObj = { test: 4 };
|
|
1977
|
+
return expect(
|
|
1978
|
+
object()
|
|
1979
|
+
.shape({
|
|
1980
|
+
test: fields[0].schema,
|
|
1981
|
+
})
|
|
1982
|
+
.validate(assertObj)
|
|
1983
|
+
).resolves.toEqual(assertObj);
|
|
1984
|
+
});
|
|
1985
|
+
});
|
|
1986
|
+
});
|
|
1987
|
+
|
|
1988
|
+
describe('when a field has a pattern', () => {
|
|
1989
|
+
let fields;
|
|
1990
|
+
beforeEach(() => {
|
|
1991
|
+
const result = createHeadlessForm(
|
|
1992
|
+
buildJSONSchemaInput({
|
|
1993
|
+
presentationFields: { inputType: 'text' },
|
|
1994
|
+
inputFields: { pattern: '^[0-9]{3}-[0-9]{2}-(?!0{4})[0-9]{4}$' },
|
|
1995
|
+
})
|
|
1996
|
+
);
|
|
1997
|
+
fields = result.fields;
|
|
1998
|
+
});
|
|
1999
|
+
describe('and value does not match the pattern', () => {
|
|
2000
|
+
it('should throw an error', async () =>
|
|
2001
|
+
expect(
|
|
2002
|
+
object()
|
|
2003
|
+
.shape({
|
|
2004
|
+
test: fields[0].schema,
|
|
2005
|
+
})
|
|
2006
|
+
.validate({ test: 'Hello' })
|
|
2007
|
+
).rejects.toMatchObject({ errors: [expect.any(String)] }));
|
|
2008
|
+
});
|
|
2009
|
+
describe('and value matches the pattern', () => {
|
|
2010
|
+
it('should validate field', async () => {
|
|
2011
|
+
const assertObj = { test: '401-85-1950' };
|
|
2012
|
+
return expect(
|
|
2013
|
+
object()
|
|
2014
|
+
.shape({
|
|
2015
|
+
test: fields[0].schema,
|
|
2016
|
+
})
|
|
2017
|
+
.validate(assertObj)
|
|
2018
|
+
).resolves.toEqual(assertObj);
|
|
2019
|
+
});
|
|
2020
|
+
});
|
|
2021
|
+
});
|
|
2022
|
+
|
|
2023
|
+
describe('when a field has max file size', () => {
|
|
2024
|
+
let fields;
|
|
2025
|
+
beforeEach(() => {
|
|
2026
|
+
const result = createHeadlessForm(
|
|
2027
|
+
JSONSchemaBuilder().addInput({ fileInput: mockFileInput }).build()
|
|
2028
|
+
);
|
|
2029
|
+
fields = result.fields;
|
|
2030
|
+
});
|
|
2031
|
+
describe('and file is greater than that', () => {
|
|
2032
|
+
const file = new File([''], 'file.png');
|
|
2033
|
+
Object.defineProperty(file, 'size', { value: 1024 * 1024 * 1024 });
|
|
2034
|
+
|
|
2035
|
+
it('should throw an error', async () =>
|
|
2036
|
+
expect(
|
|
2037
|
+
object()
|
|
2038
|
+
.shape({
|
|
2039
|
+
fileInput: fields[0].schema,
|
|
2040
|
+
})
|
|
2041
|
+
.validate({ fileInput: [file] })
|
|
2042
|
+
).rejects.toMatchObject({ errors: ['File size too large. The limit is 20 MB.'] }));
|
|
2043
|
+
});
|
|
2044
|
+
describe('and file is smaller than that', () => {
|
|
2045
|
+
const file = new File([''], 'file.png');
|
|
2046
|
+
Object.defineProperty(file, 'size', { value: 1024 * 1024 });
|
|
2047
|
+
|
|
2048
|
+
const assertObj = { fileInput: [file] };
|
|
2049
|
+
it('should validate field', async () =>
|
|
2050
|
+
expect(
|
|
2051
|
+
object()
|
|
2052
|
+
.shape({
|
|
2053
|
+
fileInput: fields[0].schema,
|
|
2054
|
+
})
|
|
2055
|
+
.validate({ fileInput: [file] })
|
|
2056
|
+
).resolves.toEqual(assertObj));
|
|
2057
|
+
});
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
describe('when a field file is optional', () => {
|
|
2061
|
+
it('it accepts an empty array', () => {
|
|
2062
|
+
const result = createHeadlessForm(
|
|
2063
|
+
JSONSchemaBuilder().addInput({ fileInput: mockFileInput }).build()
|
|
2064
|
+
);
|
|
2065
|
+
const emptyFile = { fileInput: [] };
|
|
2066
|
+
expect(
|
|
2067
|
+
object()
|
|
2068
|
+
.shape({
|
|
2069
|
+
fileInput: result.fields[0].schema,
|
|
2070
|
+
})
|
|
2071
|
+
.validate(emptyFile)
|
|
2072
|
+
).resolves.toEqual(emptyFile);
|
|
2073
|
+
});
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
describe('when a field has accepted extensions', () => {
|
|
2077
|
+
let fields;
|
|
2078
|
+
beforeEach(() => {
|
|
2079
|
+
const result = createHeadlessForm(
|
|
2080
|
+
JSONSchemaBuilder().addInput({ fileInput: mockFileInput }).build()
|
|
2081
|
+
);
|
|
2082
|
+
fields = result.fields;
|
|
2083
|
+
});
|
|
2084
|
+
describe('and file is of inccorrect format', () => {
|
|
2085
|
+
const file = new File(['foo'], 'file.txt', {
|
|
2086
|
+
type: 'text/plain',
|
|
2087
|
+
});
|
|
2088
|
+
|
|
2089
|
+
it('should throw an error', async () =>
|
|
2090
|
+
expect(
|
|
2091
|
+
object()
|
|
2092
|
+
.shape({
|
|
2093
|
+
fileInput: fields[0].schema,
|
|
2094
|
+
})
|
|
2095
|
+
.validate({ fileInput: [file] })
|
|
2096
|
+
).rejects.toMatchObject({
|
|
2097
|
+
errors: ['Unsupported file format. The acceptable formats are .png,.jpg,.jpeg,.pdf.'],
|
|
2098
|
+
}));
|
|
2099
|
+
});
|
|
2100
|
+
describe('and file is of correct format', () => {
|
|
2101
|
+
const file = new File(['foo'], 'file.png', {
|
|
2102
|
+
type: 'image/png',
|
|
2103
|
+
});
|
|
2104
|
+
Object.defineProperty(file, 'size', { value: 1024 * 1024 });
|
|
2105
|
+
|
|
2106
|
+
const assertObj = { fileInput: [file] };
|
|
2107
|
+
it('should validate field', async () =>
|
|
2108
|
+
expect(
|
|
2109
|
+
object()
|
|
2110
|
+
.shape({
|
|
2111
|
+
fileInput: fields[0].schema,
|
|
2112
|
+
})
|
|
2113
|
+
.validate({ fileInput: [file] })
|
|
2114
|
+
).resolves.toEqual(assertObj));
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2117
|
+
describe('and file is of correct but uppercase format ', () => {
|
|
2118
|
+
const file = new File(['foo'], 'file.PNG', {
|
|
2119
|
+
type: 'image/png',
|
|
2120
|
+
});
|
|
2121
|
+
Object.defineProperty(file, 'size', { value: 1024 * 1024 });
|
|
2122
|
+
|
|
2123
|
+
const assertObj = { fileInput: [file] };
|
|
2124
|
+
it('should validate field', async () =>
|
|
2125
|
+
expect(
|
|
2126
|
+
object()
|
|
2127
|
+
.shape({
|
|
2128
|
+
fileInput: fields[0].schema,
|
|
2129
|
+
})
|
|
2130
|
+
.validate({ fileInput: [file] })
|
|
2131
|
+
).resolves.toEqual(assertObj));
|
|
2132
|
+
});
|
|
2133
|
+
});
|
|
2134
|
+
|
|
2135
|
+
describe('when a field has conditional presentation properties', () => {
|
|
2136
|
+
it('adds .jsf-statement to nested statement markup when visible', () => {
|
|
2137
|
+
const { fields } = createHeadlessForm(schemaWithConditionalPresentationProperties, {
|
|
2138
|
+
initialValues: {
|
|
2139
|
+
// show the hidden statement
|
|
2140
|
+
mock_radio: 'no',
|
|
2141
|
+
},
|
|
2142
|
+
});
|
|
2143
|
+
|
|
2144
|
+
expect(fields[0].statement.description).toBe(
|
|
2145
|
+
`<span class="jsf-statement"><a href="">conditional statement markup</a></span>`
|
|
2146
|
+
);
|
|
2147
|
+
});
|
|
2148
|
+
});
|
|
2149
|
+
|
|
2150
|
+
describe('when a JSON Schema is provided', () => {
|
|
2151
|
+
const getByName = (fields, name) => fields.find((f) => f.name === name);
|
|
2152
|
+
|
|
2153
|
+
describe('and all fields are optional', () => {
|
|
2154
|
+
let handleValidation;
|
|
2155
|
+
const validateForm = (vals) => friendlyError(handleValidation(vals));
|
|
2156
|
+
|
|
2157
|
+
beforeEach(() => {
|
|
2158
|
+
const result = JSONSchemaBuilder()
|
|
2159
|
+
.addInput({ textInput: mockTextInput })
|
|
2160
|
+
.addInput({ numberInput: mockNumberInput })
|
|
2161
|
+
.build();
|
|
2162
|
+
const { handleValidation: handleValidationEach } = createHeadlessForm(result);
|
|
2163
|
+
handleValidation = handleValidationEach;
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
it.each([
|
|
2167
|
+
[
|
|
2168
|
+
'validation should return true when the object has empty values',
|
|
2169
|
+
{ textInput: '' },
|
|
2170
|
+
undefined,
|
|
2171
|
+
],
|
|
2172
|
+
[
|
|
2173
|
+
'validation should return true when object is valid',
|
|
2174
|
+
{ textInput: 'abcde', numberInput: 9 },
|
|
2175
|
+
undefined,
|
|
2176
|
+
],
|
|
2177
|
+
])('%s', (_, value, errors) => {
|
|
2178
|
+
const testValue = validateForm(value);
|
|
2179
|
+
if (errors) {
|
|
2180
|
+
expect(testValue).toEqual(errors);
|
|
2181
|
+
} else {
|
|
2182
|
+
expect(testValue).toBeUndefined();
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2185
|
+
});
|
|
2186
|
+
|
|
2187
|
+
describe('and all fields are mandatory', () => {
|
|
2188
|
+
let handleValidation;
|
|
2189
|
+
const validateForm = (vals) => friendlyError(handleValidation(vals));
|
|
2190
|
+
|
|
2191
|
+
beforeEach(() => {
|
|
2192
|
+
const result = JSONSchemaBuilder()
|
|
2193
|
+
.addInput({ textInput: mockTextInput })
|
|
2194
|
+
.addInput({ numberInput: mockNumberInput })
|
|
2195
|
+
.setRequiredFields(['numberInput', 'textInput'])
|
|
2196
|
+
.build();
|
|
2197
|
+
const { handleValidation: handleValidationEach } = createHeadlessForm(result);
|
|
2198
|
+
handleValidation = handleValidationEach;
|
|
2199
|
+
});
|
|
2200
|
+
|
|
2201
|
+
it.each([
|
|
2202
|
+
[
|
|
2203
|
+
'validation should return false when value is an empty object',
|
|
2204
|
+
{},
|
|
2205
|
+
{
|
|
2206
|
+
numberInput: 'Required field',
|
|
2207
|
+
textInput: 'Required field',
|
|
2208
|
+
},
|
|
2209
|
+
],
|
|
2210
|
+
[
|
|
2211
|
+
'validation should return false when value is an object with null values',
|
|
2212
|
+
{ textInput: null, numberInput: null },
|
|
2213
|
+
{ numberInput: 'Required field', textInput: 'Required field' },
|
|
2214
|
+
],
|
|
2215
|
+
[
|
|
2216
|
+
'validation should return false when value is an object with empty values',
|
|
2217
|
+
{ textInput: '', numberInput: '' },
|
|
2218
|
+
{ numberInput: 'The value must be a number', textInput: 'Required field' },
|
|
2219
|
+
],
|
|
2220
|
+
[
|
|
2221
|
+
'validation should return false when one value is empty',
|
|
2222
|
+
{ textInput: '986-39-076', numberInput: '' },
|
|
2223
|
+
{ numberInput: 'The value must be a number' },
|
|
2224
|
+
],
|
|
2225
|
+
[
|
|
2226
|
+
'validation should return false a numeric field is not a number',
|
|
2227
|
+
{ textInput: '986-39-076', numberInput: 'not a number' },
|
|
2228
|
+
{ numberInput: 'The value must be a number' },
|
|
2229
|
+
],
|
|
2230
|
+
[
|
|
2231
|
+
'validation should return true when object is valid',
|
|
2232
|
+
{ textInput: 'abc-xy-asd', numberInput: 9 },
|
|
2233
|
+
undefined,
|
|
2234
|
+
],
|
|
2235
|
+
])('%s', (_, values, errors) => {
|
|
2236
|
+
const testValue = validateForm(values);
|
|
2237
|
+
if (errors) {
|
|
2238
|
+
expect(testValue).toEqual(errors);
|
|
2239
|
+
} else {
|
|
2240
|
+
expect(testValue).toBeUndefined();
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
|
|
2244
|
+
describe('and one field has pattern validation', () => {
|
|
2245
|
+
beforeEach(() => {
|
|
2246
|
+
const result = JSONSchemaBuilder()
|
|
2247
|
+
.addInput({ patternTextInput: mockTextPatternInput })
|
|
2248
|
+
.setRequiredFields(['patternTextInput'])
|
|
2249
|
+
.build();
|
|
2250
|
+
const { handleValidation: handleValidationEach } = createHeadlessForm(result);
|
|
2251
|
+
handleValidation = handleValidationEach;
|
|
2252
|
+
});
|
|
2253
|
+
|
|
2254
|
+
it.each([
|
|
2255
|
+
[
|
|
2256
|
+
'validation should return false when a value does not match a pattern',
|
|
2257
|
+
{ patternTextInput: 'abc-xy-asd' },
|
|
2258
|
+
{ patternTextInput: expect.stringMatching(/Must have a valid format. E.g./i) },
|
|
2259
|
+
],
|
|
2260
|
+
[
|
|
2261
|
+
'validation should return true when value matches the pattern',
|
|
2262
|
+
{ patternTextInput: '986-39-0716' },
|
|
2263
|
+
undefined,
|
|
2264
|
+
],
|
|
2265
|
+
])('%s', (_, values, errors) => {
|
|
2266
|
+
const testValue = validateForm(values);
|
|
2267
|
+
if (errors) {
|
|
2268
|
+
expect(testValue).toEqual(errors);
|
|
2269
|
+
} else {
|
|
2270
|
+
expect(testValue).toBeUndefined();
|
|
2271
|
+
}
|
|
2272
|
+
});
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
describe('and one field has max length validation', () => {
|
|
2276
|
+
beforeEach(() => {
|
|
2277
|
+
const result = JSONSchemaBuilder()
|
|
2278
|
+
.addInput({ maxLengthTextInput: mockTextMaxLengthInput })
|
|
2279
|
+
.setRequiredFields(['maxLengthTextInput'])
|
|
2280
|
+
.build();
|
|
2281
|
+
const { handleValidation: handleValidationEach } = createHeadlessForm(result);
|
|
2282
|
+
handleValidation = handleValidationEach;
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
it.each([
|
|
2286
|
+
[
|
|
2287
|
+
'validation should return false when a value is greater than the limit',
|
|
2288
|
+
{ maxLengthTextInput: 'Hello John Dow' },
|
|
2289
|
+
{ maxLengthTextInput: 'Please insert up to 10 characters' },
|
|
2290
|
+
],
|
|
2291
|
+
[
|
|
2292
|
+
'validation should return true when value is within the limit',
|
|
2293
|
+
{ maxLengthTextInput: 'Hello John' },
|
|
2294
|
+
undefined,
|
|
2295
|
+
],
|
|
2296
|
+
])('%s', (_, values, errors) => {
|
|
2297
|
+
const testValue = validateForm(values);
|
|
2298
|
+
if (errors) {
|
|
2299
|
+
expect(testValue).toEqual(errors);
|
|
2300
|
+
} else {
|
|
2301
|
+
expect(testValue).toBeUndefined();
|
|
2302
|
+
}
|
|
2303
|
+
});
|
|
2304
|
+
});
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
describe('and fields are dynamically required/optional', () => {
|
|
2308
|
+
it('applies correct validation for single-value based conditionals', async () => {
|
|
2309
|
+
const { fields, handleValidation } = createHeadlessForm(schemaDynamicValidationConst);
|
|
2310
|
+
const validateForm = (vals) => friendlyError(handleValidation(vals));
|
|
2311
|
+
|
|
2312
|
+
expect(
|
|
2313
|
+
validateForm({
|
|
2314
|
+
validate_tabs: 'no',
|
|
2315
|
+
a_fieldset: {
|
|
2316
|
+
id_number: 123,
|
|
2317
|
+
},
|
|
2318
|
+
mandatory_group_array: 'no',
|
|
2319
|
+
})
|
|
2320
|
+
).toBeUndefined();
|
|
2321
|
+
|
|
2322
|
+
const getTabsField = () =>
|
|
2323
|
+
fields.find((f) => f.name === 'a_fieldset').fields.find((f) => f.name === 'tabs');
|
|
2324
|
+
|
|
2325
|
+
expect(getTabsField().required).toBeFalsy();
|
|
2326
|
+
|
|
2327
|
+
expect(
|
|
2328
|
+
validateForm({
|
|
2329
|
+
validate_tabs: 'yes',
|
|
2330
|
+
a_fieldset: {
|
|
2331
|
+
id_number: 123,
|
|
2332
|
+
},
|
|
2333
|
+
mandatory_group_array: 'no',
|
|
2334
|
+
})
|
|
2335
|
+
).toEqual({
|
|
2336
|
+
a_fieldset: {
|
|
2337
|
+
tabs: 'Required field',
|
|
2338
|
+
},
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
expect(getTabsField().required).toBeTruthy();
|
|
2342
|
+
|
|
2343
|
+
expect(
|
|
2344
|
+
validateForm({
|
|
2345
|
+
validate_tabs: 'yes',
|
|
2346
|
+
a_fieldset: {
|
|
2347
|
+
id_number: 123,
|
|
2348
|
+
},
|
|
2349
|
+
mandatory_group_array: 'yes',
|
|
2350
|
+
a_group_array: [{ full_name: 'adfs' }],
|
|
2351
|
+
})
|
|
2352
|
+
).toEqual({ a_fieldset: { tabs: 'Required field' } });
|
|
2353
|
+
|
|
2354
|
+
expect(
|
|
2355
|
+
validateForm({
|
|
2356
|
+
validate_tabs: 'yes',
|
|
2357
|
+
a_fieldset: {
|
|
2358
|
+
id_number: 123,
|
|
2359
|
+
tabs: 2,
|
|
2360
|
+
},
|
|
2361
|
+
mandatory_group_array: 'no',
|
|
2362
|
+
})
|
|
2363
|
+
).toBeUndefined();
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
it('applies correct validation for minimum/maximum conditionals', async () => {
|
|
2367
|
+
const { handleValidation } = createHeadlessForm(schemaDynamicValidationMinimumMaximum);
|
|
2368
|
+
const validateForm = (vals) => friendlyError(handleValidation(vals));
|
|
2369
|
+
|
|
2370
|
+
// Check for minimum condition
|
|
2371
|
+
expect(
|
|
2372
|
+
validateForm({
|
|
2373
|
+
a_number: 0,
|
|
2374
|
+
})
|
|
2375
|
+
).toEqual({
|
|
2376
|
+
a_conditional_text: 'Required field',
|
|
2377
|
+
a_number: 'Must be greater or equal to 1',
|
|
2378
|
+
});
|
|
2379
|
+
|
|
2380
|
+
// Check for maximum condition
|
|
2381
|
+
expect(
|
|
2382
|
+
validateForm({
|
|
2383
|
+
a_number: 11,
|
|
2384
|
+
})
|
|
2385
|
+
).toEqual({
|
|
2386
|
+
a_conditional_text: 'Required field',
|
|
2387
|
+
a_number: 'Must be smaller or equal to 10',
|
|
2388
|
+
});
|
|
2389
|
+
|
|
2390
|
+
// Check for absence of a_number
|
|
2391
|
+
expect(validateForm({})).toEqual({
|
|
2392
|
+
a_conditional_text: 'Required field',
|
|
2393
|
+
});
|
|
2394
|
+
|
|
2395
|
+
// Check for number within range
|
|
2396
|
+
expect(
|
|
2397
|
+
validateForm({
|
|
2398
|
+
a_number: 5,
|
|
2399
|
+
})
|
|
2400
|
+
).toBeUndefined();
|
|
2401
|
+
});
|
|
2402
|
+
|
|
2403
|
+
it('applies correct validation for minLength/maxLength conditionals', async () => {
|
|
2404
|
+
const { handleValidation } = createHeadlessForm(schemaDynamicValidationMinLengthMaxLength);
|
|
2405
|
+
const validateForm = (vals) => friendlyError(handleValidation(vals));
|
|
2406
|
+
const formError = {
|
|
2407
|
+
a_conditional_text: 'Required field',
|
|
2408
|
+
};
|
|
2409
|
+
// By default a_conditional_text is required.
|
|
2410
|
+
expect(validateForm({})).toEqual(formError);
|
|
2411
|
+
|
|
2412
|
+
// Check for minimum length condition - a_text >= 3 chars
|
|
2413
|
+
expect(
|
|
2414
|
+
validateForm({
|
|
2415
|
+
a_text: 'Foo',
|
|
2416
|
+
})
|
|
2417
|
+
).toBeUndefined();
|
|
2418
|
+
|
|
2419
|
+
// Check for maximum length condition - a_text <= 5 chars
|
|
2420
|
+
expect(
|
|
2421
|
+
validateForm({
|
|
2422
|
+
a_text: 'Fooba',
|
|
2423
|
+
})
|
|
2424
|
+
).toBeUndefined();
|
|
2425
|
+
|
|
2426
|
+
// Check for text out of length range (7 chars)
|
|
2427
|
+
expect(
|
|
2428
|
+
validateForm({
|
|
2429
|
+
a_text: 'Foobaaz',
|
|
2430
|
+
})
|
|
2431
|
+
).toEqual(formError);
|
|
2432
|
+
|
|
2433
|
+
// Check for text out of length range (2 chars)
|
|
2434
|
+
expect(
|
|
2435
|
+
validateForm({
|
|
2436
|
+
a_text: 'Fe',
|
|
2437
|
+
})
|
|
2438
|
+
).toEqual(formError);
|
|
2439
|
+
});
|
|
2440
|
+
|
|
2441
|
+
it('applies correct validation for array-contain based conditionals', async () => {
|
|
2442
|
+
const { handleValidation } = createHeadlessForm(schemaDynamicValidationContains);
|
|
2443
|
+
const validateForm = (vals) => friendlyError(handleValidation(vals));
|
|
2444
|
+
|
|
2445
|
+
expect(
|
|
2446
|
+
validateForm({
|
|
2447
|
+
validate_fieldset: ['id_number'],
|
|
2448
|
+
a_fieldset: {
|
|
2449
|
+
id_number: 123,
|
|
2450
|
+
},
|
|
2451
|
+
})
|
|
2452
|
+
).toBeUndefined();
|
|
2453
|
+
|
|
2454
|
+
expect(
|
|
2455
|
+
validateForm({
|
|
2456
|
+
validate_fieldset: ['id_number', 'all'],
|
|
2457
|
+
a_fieldset: {
|
|
2458
|
+
id_number: 123,
|
|
2459
|
+
},
|
|
2460
|
+
})
|
|
2461
|
+
).toEqual({
|
|
2462
|
+
a_fieldset: {
|
|
2463
|
+
tabs: 'Required field',
|
|
2464
|
+
},
|
|
2465
|
+
});
|
|
2466
|
+
|
|
2467
|
+
expect(
|
|
2468
|
+
validateForm({
|
|
2469
|
+
validate_fieldset: ['id_number', 'all'],
|
|
2470
|
+
a_fieldset: {
|
|
2471
|
+
id_number: 123,
|
|
2472
|
+
tabs: 2,
|
|
2473
|
+
},
|
|
2474
|
+
})
|
|
2475
|
+
).toBeUndefined();
|
|
2476
|
+
});
|
|
2477
|
+
|
|
2478
|
+
it('applies correct validation for fieldset fields', async () => {
|
|
2479
|
+
const { handleValidation } = createHeadlessForm(schemaDynamicValidationContains);
|
|
2480
|
+
const validateForm = (vals) => friendlyError(handleValidation(vals));
|
|
2481
|
+
|
|
2482
|
+
expect(
|
|
2483
|
+
validateForm({
|
|
2484
|
+
validate_fieldset: ['id_number'],
|
|
2485
|
+
a_fieldset: {
|
|
2486
|
+
id_number: 123,
|
|
2487
|
+
},
|
|
2488
|
+
})
|
|
2489
|
+
).toBeUndefined();
|
|
2490
|
+
|
|
2491
|
+
expect(
|
|
2492
|
+
validateForm({
|
|
2493
|
+
validate_fieldset: ['id_number', 'all'],
|
|
2494
|
+
a_fieldset: {
|
|
2495
|
+
id_number: 123,
|
|
2496
|
+
},
|
|
2497
|
+
})
|
|
2498
|
+
).toEqual({
|
|
2499
|
+
a_fieldset: {
|
|
2500
|
+
tabs: 'Required field',
|
|
2501
|
+
},
|
|
2502
|
+
});
|
|
2503
|
+
|
|
2504
|
+
expect(
|
|
2505
|
+
validateForm({
|
|
2506
|
+
validate_fieldset: ['id_number', 'all'],
|
|
2507
|
+
a_fieldset: {
|
|
2508
|
+
id_number: 123,
|
|
2509
|
+
tabs: 2,
|
|
2510
|
+
},
|
|
2511
|
+
})
|
|
2512
|
+
).toBeUndefined();
|
|
2513
|
+
});
|
|
2514
|
+
|
|
2515
|
+
it('applies any of the validation alternatives in a anyOf branch', async () => {
|
|
2516
|
+
const { handleValidation } = createHeadlessForm(schemaAnyOfValidation);
|
|
2517
|
+
const validateForm = (vals) => friendlyError(handleValidation(vals));
|
|
2518
|
+
|
|
2519
|
+
expect(
|
|
2520
|
+
validateForm({
|
|
2521
|
+
field_a: '123',
|
|
2522
|
+
})
|
|
2523
|
+
).toBeUndefined();
|
|
2524
|
+
|
|
2525
|
+
expect(
|
|
2526
|
+
validateForm({
|
|
2527
|
+
field_b: '456',
|
|
2528
|
+
})
|
|
2529
|
+
).toEqual({ field_c: 'Required field' });
|
|
2530
|
+
|
|
2531
|
+
expect(
|
|
2532
|
+
validateForm({
|
|
2533
|
+
field_b: '456',
|
|
2534
|
+
field_c: '789',
|
|
2535
|
+
})
|
|
2536
|
+
).toBeUndefined();
|
|
2537
|
+
|
|
2538
|
+
expect(
|
|
2539
|
+
validateForm({
|
|
2540
|
+
field_a: '123',
|
|
2541
|
+
field_c: '789',
|
|
2542
|
+
})
|
|
2543
|
+
).toBeUndefined();
|
|
2544
|
+
|
|
2545
|
+
expect(
|
|
2546
|
+
validateForm({
|
|
2547
|
+
field_a: '123',
|
|
2548
|
+
field_b: '456',
|
|
2549
|
+
field_c: '789',
|
|
2550
|
+
})
|
|
2551
|
+
).toBeUndefined();
|
|
2552
|
+
});
|
|
2553
|
+
|
|
2554
|
+
describe('nested conditionals', () => {
|
|
2555
|
+
it('given empty values, runs "else" (gets hidden)', () => {
|
|
2556
|
+
const { fields } = createHeadlessForm(schemaWithConditionalReadOnlyProperty, {
|
|
2557
|
+
field_a: null,
|
|
2558
|
+
});
|
|
2559
|
+
expect(getByName(fields, 'field_b').isVisible).toBe(false);
|
|
2560
|
+
});
|
|
2561
|
+
|
|
2562
|
+
it('given a match, runs "then" (turns visible and editable)', () => {
|
|
2563
|
+
const { fields } = createHeadlessForm(schemaWithConditionalReadOnlyProperty, {
|
|
2564
|
+
initialValues: { field_a: 'yes' },
|
|
2565
|
+
});
|
|
2566
|
+
expect(getByName(fields, 'field_b').isVisible).toBe(true);
|
|
2567
|
+
expect(getByName(fields, 'field_b').readOnly).toBe(false);
|
|
2568
|
+
});
|
|
2569
|
+
|
|
2570
|
+
it('given a nested match, runs "else-then" (turns visible but readOnly)', () => {
|
|
2571
|
+
const { fields } = createHeadlessForm(schemaWithConditionalReadOnlyProperty, {
|
|
2572
|
+
initialValues: { field_a: 'no' },
|
|
2573
|
+
});
|
|
2574
|
+
expect(getByName(fields, 'field_b').isVisible).toBe(true);
|
|
2575
|
+
expect(getByName(fields, 'field_b').readOnly).toBe(true);
|
|
2576
|
+
});
|
|
2577
|
+
});
|
|
2578
|
+
|
|
2579
|
+
describe('conditional fields (incorrectly done)', () => {
|
|
2580
|
+
// this catches the typical scenario where developers forget to set the if.required[]
|
|
2581
|
+
|
|
2582
|
+
it('given empty values, the incorrect conditional runs "then" instead of "else"', () => {
|
|
2583
|
+
const { fields: fieldsEmpty } = createHeadlessForm(schemaWithWrongConditional, {
|
|
2584
|
+
initialValues: { field_a: null, field_a_wrong: null },
|
|
2585
|
+
});
|
|
2586
|
+
// The dependent correct field gets hidden, but...
|
|
2587
|
+
expect(getByName(fieldsEmpty, 'field_b').isVisible).toBe(false);
|
|
2588
|
+
// ...the dependent wrong field stays visible because the
|
|
2589
|
+
// conditional is wrong (it's missing the if.required[])
|
|
2590
|
+
expect(getByName(fieldsEmpty, 'field_b_wrong').isVisible).toBe(true);
|
|
2591
|
+
});
|
|
2592
|
+
|
|
2593
|
+
it('given a match ("yes"), both runs "then" (turn visible)', () => {
|
|
2594
|
+
const { fields: fieldsVisible } = createHeadlessForm(schemaWithWrongConditional, {
|
|
2595
|
+
initialValues: { field_a: 'yes', field_a_wrong: 'yes' },
|
|
2596
|
+
});
|
|
2597
|
+
expect(getByName(fieldsVisible, 'field_b').isVisible).toBe(true);
|
|
2598
|
+
expect(getByName(fieldsVisible, 'field_b_wrong').isVisible).toBe(true);
|
|
2599
|
+
});
|
|
2600
|
+
|
|
2601
|
+
it('not given a match ("no"), both run else (stay hidden)', () => {
|
|
2602
|
+
const { fields: fieldsHidden } = createHeadlessForm(schemaWithWrongConditional, {
|
|
2603
|
+
initialValues: { field_a: 'no', field_a_wrong: 'no' },
|
|
2604
|
+
});
|
|
2605
|
+
expect(getByName(fieldsHidden, 'field_b').isVisible).toBe(false);
|
|
2606
|
+
expect(getByName(fieldsHidden, 'field_b_wrong').isVisible).toBe(false);
|
|
2607
|
+
});
|
|
2608
|
+
});
|
|
2609
|
+
|
|
2610
|
+
it('checkbox should have no initial value when its dynamically shown and invisible', () => {
|
|
2611
|
+
const { fields } = createHeadlessForm(schemaWithConditionalAcknowledgementProperty, {
|
|
2612
|
+
initialValues: {
|
|
2613
|
+
field_a: 'no',
|
|
2614
|
+
},
|
|
2615
|
+
});
|
|
2616
|
+
const dependentField = getByName(fields, 'field_b');
|
|
2617
|
+
expect(dependentField.isVisible).toBe(false);
|
|
2618
|
+
expect(dependentField.value).toBe(undefined);
|
|
2619
|
+
});
|
|
2620
|
+
|
|
2621
|
+
it('checkbox should have no initial value when its dynamically shown and visible', () => {
|
|
2622
|
+
const { fields } = createHeadlessForm(schemaWithConditionalAcknowledgementProperty, {
|
|
2623
|
+
initialValues: {
|
|
2624
|
+
field_a: 'yes',
|
|
2625
|
+
},
|
|
2626
|
+
});
|
|
2627
|
+
const dependentField = getByName(fields, 'field_b');
|
|
2628
|
+
expect(dependentField.isVisible).toBe(true);
|
|
2629
|
+
expect(dependentField.value).toBe(undefined);
|
|
2630
|
+
});
|
|
2631
|
+
});
|
|
2632
|
+
});
|
|
2633
|
+
|
|
2634
|
+
// TODO: delete after migration to x-jsf-errorMessage is completed
|
|
2635
|
+
describe('Throwing custom error messages using errorMessage (deprecated)', () => {
|
|
2636
|
+
it.each([
|
|
2637
|
+
[
|
|
2638
|
+
'type',
|
|
2639
|
+
JSONSchemaBuilder()
|
|
2640
|
+
.addInput({
|
|
2641
|
+
numberInput: {
|
|
2642
|
+
...mockNumberInput,
|
|
2643
|
+
errorMessage: { type: 'It has to be a number.' },
|
|
2644
|
+
},
|
|
2645
|
+
})
|
|
2646
|
+
.build(),
|
|
2647
|
+
{ numberInput: 'Two' },
|
|
2648
|
+
{
|
|
2649
|
+
numberInput: 'It has to be a number.',
|
|
2650
|
+
},
|
|
2651
|
+
false,
|
|
2652
|
+
],
|
|
2653
|
+
[
|
|
2654
|
+
'minimum',
|
|
2655
|
+
JSONSchemaBuilder()
|
|
2656
|
+
.addInput({
|
|
2657
|
+
numberInput: {
|
|
2658
|
+
...mockNumberInput,
|
|
2659
|
+
errorMessage: { minimum: 'I am a custom error message' },
|
|
2660
|
+
},
|
|
2661
|
+
})
|
|
2662
|
+
.build(),
|
|
2663
|
+
{ numberInput: -1 },
|
|
2664
|
+
{
|
|
2665
|
+
numberInput: 'I am a custom error message',
|
|
2666
|
+
},
|
|
2667
|
+
false,
|
|
2668
|
+
],
|
|
2669
|
+
[
|
|
2670
|
+
'required',
|
|
2671
|
+
JSONSchemaBuilder()
|
|
2672
|
+
.addInput({
|
|
2673
|
+
numberInput: {
|
|
2674
|
+
...mockNumberInput,
|
|
2675
|
+
errorMessage: { required: 'I am a custom error message' },
|
|
2676
|
+
},
|
|
2677
|
+
})
|
|
2678
|
+
.setRequiredFields(['numberInput'])
|
|
2679
|
+
.build(),
|
|
2680
|
+
{},
|
|
2681
|
+
{
|
|
2682
|
+
numberInput: 'I am a custom error message',
|
|
2683
|
+
},
|
|
2684
|
+
],
|
|
2685
|
+
[
|
|
2686
|
+
'required (ignored because it is optional)',
|
|
2687
|
+
JSONSchemaBuilder()
|
|
2688
|
+
.addInput({
|
|
2689
|
+
numberInput: {
|
|
2690
|
+
...mockNumberInput,
|
|
2691
|
+
errorMessage: { required: 'I am a custom error message' },
|
|
2692
|
+
},
|
|
2693
|
+
})
|
|
2694
|
+
.build(),
|
|
2695
|
+
{},
|
|
2696
|
+
undefined,
|
|
2697
|
+
],
|
|
2698
|
+
[
|
|
2699
|
+
'maximum',
|
|
2700
|
+
JSONSchemaBuilder()
|
|
2701
|
+
.addInput({
|
|
2702
|
+
numberInput: {
|
|
2703
|
+
...mockNumberInput,
|
|
2704
|
+
errorMessage: { maximum: 'I am a custom error message' },
|
|
2705
|
+
},
|
|
2706
|
+
})
|
|
2707
|
+
.build(),
|
|
2708
|
+
{ numberInput: 11 },
|
|
2709
|
+
{
|
|
2710
|
+
numberInput: 'I am a custom error message',
|
|
2711
|
+
},
|
|
2712
|
+
],
|
|
2713
|
+
[
|
|
2714
|
+
'minLength',
|
|
2715
|
+
JSONSchemaBuilder()
|
|
2716
|
+
.addInput({
|
|
2717
|
+
stringInput: {
|
|
2718
|
+
...mockTextInput,
|
|
2719
|
+
minLength: 3,
|
|
2720
|
+
errorMessage: { minLength: 'I am a custom error message' },
|
|
2721
|
+
},
|
|
2722
|
+
})
|
|
2723
|
+
.build(),
|
|
2724
|
+
{ stringInput: 'aa' },
|
|
2725
|
+
{
|
|
2726
|
+
stringInput: 'I am a custom error message',
|
|
2727
|
+
},
|
|
2728
|
+
],
|
|
2729
|
+
[
|
|
2730
|
+
'maxLength',
|
|
2731
|
+
JSONSchemaBuilder()
|
|
2732
|
+
.addInput({
|
|
2733
|
+
stringInput: {
|
|
2734
|
+
...mockTextInput,
|
|
2735
|
+
maxLength: 3,
|
|
2736
|
+
errorMessage: { maxLength: 'I am a custom error message' },
|
|
2737
|
+
},
|
|
2738
|
+
})
|
|
2739
|
+
.build(),
|
|
2740
|
+
{ stringInput: 'aaaa' },
|
|
2741
|
+
{
|
|
2742
|
+
stringInput: 'I am a custom error message',
|
|
2743
|
+
},
|
|
2744
|
+
],
|
|
2745
|
+
[
|
|
2746
|
+
'pattern',
|
|
2747
|
+
JSONSchemaBuilder()
|
|
2748
|
+
.addInput({
|
|
2749
|
+
stringInput: {
|
|
2750
|
+
...mockTextInput,
|
|
2751
|
+
pattern: '^(\\+|00)\\d*$',
|
|
2752
|
+
errorMessage: { pattern: 'I am a custom error message' },
|
|
2753
|
+
},
|
|
2754
|
+
})
|
|
2755
|
+
.build(),
|
|
2756
|
+
{ stringInput: 'aaaa' },
|
|
2757
|
+
{
|
|
2758
|
+
stringInput: 'I am a custom error message',
|
|
2759
|
+
},
|
|
2760
|
+
],
|
|
2761
|
+
[
|
|
2762
|
+
'maxFileSize',
|
|
2763
|
+
JSONSchemaBuilder()
|
|
2764
|
+
.addInput({
|
|
2765
|
+
fileInput: {
|
|
2766
|
+
...mockFileInput,
|
|
2767
|
+
'x-jsf-presentation': {
|
|
2768
|
+
...mockFileInput['x-jsf-presentation'],
|
|
2769
|
+
maxFileSize: 1000,
|
|
2770
|
+
},
|
|
2771
|
+
errorMessage: { maxFileSize: 'I am a custom error message' },
|
|
2772
|
+
},
|
|
2773
|
+
})
|
|
2774
|
+
.build(),
|
|
2775
|
+
{
|
|
2776
|
+
fileInput: [
|
|
2777
|
+
(() => {
|
|
2778
|
+
const file = new File([''], 'file.png');
|
|
2779
|
+
Object.defineProperty(file, 'size', { value: 1024 * 1024 * 1024 });
|
|
2780
|
+
return file;
|
|
2781
|
+
})(),
|
|
2782
|
+
],
|
|
2783
|
+
},
|
|
2784
|
+
{
|
|
2785
|
+
fileInput: 'I am a custom error message',
|
|
2786
|
+
},
|
|
2787
|
+
],
|
|
2788
|
+
[
|
|
2789
|
+
'accept',
|
|
2790
|
+
JSONSchemaBuilder()
|
|
2791
|
+
.addInput({
|
|
2792
|
+
fileInput: {
|
|
2793
|
+
...mockFileInput,
|
|
2794
|
+
accept: '.pdf',
|
|
2795
|
+
errorMessage: { accept: 'I am a custom error message' },
|
|
2796
|
+
},
|
|
2797
|
+
})
|
|
2798
|
+
.build(),
|
|
2799
|
+
{
|
|
2800
|
+
fileInput: [new File([''], 'file.docx')],
|
|
2801
|
+
},
|
|
2802
|
+
{
|
|
2803
|
+
fileInput: 'I am a custom error message',
|
|
2804
|
+
},
|
|
2805
|
+
],
|
|
2806
|
+
])('error message for property "%s"', (_, schema, input, errors) => {
|
|
2807
|
+
const { handleValidation } = createHeadlessForm(schema);
|
|
2808
|
+
const validateForm = (vals) => friendlyError(handleValidation(vals));
|
|
2809
|
+
|
|
2810
|
+
if (errors) {
|
|
2811
|
+
expect(validateForm(input)).toEqual(errors);
|
|
2812
|
+
} else {
|
|
2813
|
+
expect(validateForm(input)).toBeUndefined();
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
});
|
|
2817
|
+
|
|
2818
|
+
describe('Custom error messages', () => {
|
|
2819
|
+
it.each([
|
|
2820
|
+
[
|
|
2821
|
+
'type',
|
|
2822
|
+
JSONSchemaBuilder()
|
|
2823
|
+
.addInput({
|
|
2824
|
+
numberInput: {
|
|
2825
|
+
...mockNumberInput,
|
|
2826
|
+
'x-jsf-errorMessage': { type: 'It has to be a number.' },
|
|
2827
|
+
},
|
|
2828
|
+
})
|
|
2829
|
+
.build(),
|
|
2830
|
+
{ numberInput: 'Two' },
|
|
2831
|
+
{
|
|
2832
|
+
numberInput: 'It has to be a number.',
|
|
2833
|
+
},
|
|
2834
|
+
false,
|
|
2835
|
+
],
|
|
2836
|
+
[
|
|
2837
|
+
'minimum',
|
|
2838
|
+
JSONSchemaBuilder()
|
|
2839
|
+
.addInput({
|
|
2840
|
+
numberInput: {
|
|
2841
|
+
...mockNumberInput,
|
|
2842
|
+
'x-jsf-errorMessage': { minimum: 'I am a custom error message' },
|
|
2843
|
+
},
|
|
2844
|
+
})
|
|
2845
|
+
.build(),
|
|
2846
|
+
{ numberInput: -1 },
|
|
2847
|
+
{
|
|
2848
|
+
numberInput: 'I am a custom error message',
|
|
2849
|
+
},
|
|
2850
|
+
false,
|
|
2851
|
+
],
|
|
2852
|
+
[
|
|
2853
|
+
'required',
|
|
2854
|
+
JSONSchemaBuilder()
|
|
2855
|
+
.addInput({
|
|
2856
|
+
numberInput: {
|
|
2857
|
+
...mockNumberInput,
|
|
2858
|
+
'x-jsf-errorMessage': { required: 'I am a custom error message' },
|
|
2859
|
+
},
|
|
2860
|
+
})
|
|
2861
|
+
.setRequiredFields(['numberInput'])
|
|
2862
|
+
.build(),
|
|
2863
|
+
{},
|
|
2864
|
+
{
|
|
2865
|
+
numberInput: 'I am a custom error message',
|
|
2866
|
+
},
|
|
2867
|
+
],
|
|
2868
|
+
[
|
|
2869
|
+
'required (ignored because it is optional)',
|
|
2870
|
+
JSONSchemaBuilder()
|
|
2871
|
+
.addInput({
|
|
2872
|
+
numberInput: {
|
|
2873
|
+
...mockNumberInput,
|
|
2874
|
+
'x-jsf-errorMessage': { required: 'I am a custom error message' },
|
|
2875
|
+
},
|
|
2876
|
+
})
|
|
2877
|
+
.build(),
|
|
2878
|
+
{},
|
|
2879
|
+
undefined,
|
|
2880
|
+
],
|
|
2881
|
+
[
|
|
2882
|
+
'maximum',
|
|
2883
|
+
JSONSchemaBuilder()
|
|
2884
|
+
.addInput({
|
|
2885
|
+
numberInput: {
|
|
2886
|
+
...mockNumberInput,
|
|
2887
|
+
'x-jsf-errorMessage': { maximum: 'I am a custom error message' },
|
|
2888
|
+
},
|
|
2889
|
+
})
|
|
2890
|
+
.build(),
|
|
2891
|
+
{ numberInput: 11 },
|
|
2892
|
+
{
|
|
2893
|
+
numberInput: 'I am a custom error message',
|
|
2894
|
+
},
|
|
2895
|
+
],
|
|
2896
|
+
[
|
|
2897
|
+
'minLength',
|
|
2898
|
+
JSONSchemaBuilder()
|
|
2899
|
+
.addInput({
|
|
2900
|
+
stringInput: {
|
|
2901
|
+
...mockTextInput,
|
|
2902
|
+
minLength: 3,
|
|
2903
|
+
'x-jsf-errorMessage': { minLength: 'I am a custom error message' },
|
|
2904
|
+
},
|
|
2905
|
+
})
|
|
2906
|
+
.build(),
|
|
2907
|
+
{ stringInput: 'aa' },
|
|
2908
|
+
{
|
|
2909
|
+
stringInput: 'I am a custom error message',
|
|
2910
|
+
},
|
|
2911
|
+
],
|
|
2912
|
+
[
|
|
2913
|
+
'maxLength',
|
|
2914
|
+
JSONSchemaBuilder()
|
|
2915
|
+
.addInput({
|
|
2916
|
+
stringInput: {
|
|
2917
|
+
...mockTextInput,
|
|
2918
|
+
maxLength: 3,
|
|
2919
|
+
'x-jsf-errorMessage': { maxLength: 'I am a custom error message' },
|
|
2920
|
+
},
|
|
2921
|
+
})
|
|
2922
|
+
.build(),
|
|
2923
|
+
{ stringInput: 'aaaa' },
|
|
2924
|
+
{
|
|
2925
|
+
stringInput: 'I am a custom error message',
|
|
2926
|
+
},
|
|
2927
|
+
],
|
|
2928
|
+
[
|
|
2929
|
+
'pattern',
|
|
2930
|
+
JSONSchemaBuilder()
|
|
2931
|
+
.addInput({
|
|
2932
|
+
stringInput: {
|
|
2933
|
+
...mockTextInput,
|
|
2934
|
+
pattern: '^(\\+|00)\\d*$',
|
|
2935
|
+
'x-jsf-errorMessage': { pattern: 'I am a custom error message' },
|
|
2936
|
+
},
|
|
2937
|
+
})
|
|
2938
|
+
.build(),
|
|
2939
|
+
{ stringInput: 'aaaa' },
|
|
2940
|
+
{
|
|
2941
|
+
stringInput: 'I am a custom error message',
|
|
2942
|
+
},
|
|
2943
|
+
],
|
|
2944
|
+
[
|
|
2945
|
+
'maxFileSize',
|
|
2946
|
+
JSONSchemaBuilder()
|
|
2947
|
+
.addInput({
|
|
2948
|
+
fileInput: {
|
|
2949
|
+
...mockFileInput,
|
|
2950
|
+
'x-jsf-presentation': {
|
|
2951
|
+
...mockFileInput['x-jsf-presentation'],
|
|
2952
|
+
maxFileSize: 1000,
|
|
2953
|
+
},
|
|
2954
|
+
'x-jsf-errorMessage': { maxFileSize: 'I am a custom error message' },
|
|
2955
|
+
},
|
|
2956
|
+
})
|
|
2957
|
+
.build(),
|
|
2958
|
+
{
|
|
2959
|
+
fileInput: [
|
|
2960
|
+
(() => {
|
|
2961
|
+
const file = new File([''], 'file.png');
|
|
2962
|
+
Object.defineProperty(file, 'size', { value: 1024 * 1024 * 1024 });
|
|
2963
|
+
return file;
|
|
2964
|
+
})(),
|
|
2965
|
+
],
|
|
2966
|
+
},
|
|
2967
|
+
{
|
|
2968
|
+
fileInput: 'I am a custom error message',
|
|
2969
|
+
},
|
|
2970
|
+
],
|
|
2971
|
+
[
|
|
2972
|
+
'accept',
|
|
2973
|
+
JSONSchemaBuilder()
|
|
2974
|
+
.addInput({
|
|
2975
|
+
fileInput: {
|
|
2976
|
+
...mockFileInput,
|
|
2977
|
+
accept: '.pdf',
|
|
2978
|
+
'x-jsf-errorMessage': { accept: 'I am a custom error message' },
|
|
2979
|
+
},
|
|
2980
|
+
})
|
|
2981
|
+
.build(),
|
|
2982
|
+
{
|
|
2983
|
+
fileInput: [new File([''], 'file.docx')],
|
|
2984
|
+
},
|
|
2985
|
+
{
|
|
2986
|
+
fileInput: 'I am a custom error message',
|
|
2987
|
+
},
|
|
2988
|
+
],
|
|
2989
|
+
])('error message for property "%s"', (_, schema, input, errors) => {
|
|
2990
|
+
const { handleValidation } = createHeadlessForm(schema);
|
|
2991
|
+
const validateForm = (vals) => friendlyError(handleValidation(vals));
|
|
2992
|
+
|
|
2993
|
+
if (errors) {
|
|
2994
|
+
expect(validateForm(input)).toEqual(errors);
|
|
2995
|
+
} else {
|
|
2996
|
+
expect(validateForm(input)).toBeUndefined();
|
|
2997
|
+
}
|
|
2998
|
+
});
|
|
2999
|
+
|
|
3000
|
+
it('accepts with options.inputType[].errorMessage', () => {
|
|
3001
|
+
// Sanity-check the default error message
|
|
3002
|
+
const resultDefault = createHeadlessForm(schemaForErrorMessageSpecificity);
|
|
3003
|
+
expect(resultDefault.handleValidation({}).formErrors).toEqual({
|
|
3004
|
+
weekday: 'Required field',
|
|
3005
|
+
day: 'Required field',
|
|
3006
|
+
month: 'Required field',
|
|
3007
|
+
year: 'The year is mandatory.', // from x-jsf-errorMessage
|
|
3008
|
+
});
|
|
3009
|
+
|
|
3010
|
+
// Assert the custom error message
|
|
3011
|
+
const resultCustom = createHeadlessForm(schemaForErrorMessageSpecificity, {
|
|
3012
|
+
...jsfConfigForErrorMessageSpecificity,
|
|
3013
|
+
});
|
|
3014
|
+
expect(resultCustom.handleValidation({}).formErrors).toEqual({
|
|
3015
|
+
weekday: 'Required field', // sanity-check that a different inputType keeps the default error msg.
|
|
3016
|
+
day: 'This cannot be empty.',
|
|
3017
|
+
month: 'This cannot be empty.',
|
|
3018
|
+
year: 'The year is mandatory.', // error specificity: schema's msg is higher than options' msg.
|
|
3019
|
+
});
|
|
3020
|
+
});
|
|
3021
|
+
});
|
|
3022
|
+
|
|
3023
|
+
describe('when default values are provided', () => {
|
|
3024
|
+
describe('and "fieldset" has scoped conditionals', () => {
|
|
3025
|
+
it('should show conditionals fields when values fullfil conditions', () => {
|
|
3026
|
+
const result = createHeadlessForm(schemaFieldsetScopedCondition, {
|
|
3027
|
+
initialValues: { child: { has_child: 'yes' } },
|
|
3028
|
+
});
|
|
3029
|
+
|
|
3030
|
+
const fieldset = result.fields[0];
|
|
3031
|
+
|
|
3032
|
+
expect(fieldset).toMatchObject({
|
|
3033
|
+
fields: [
|
|
3034
|
+
{
|
|
3035
|
+
name: 'has_child',
|
|
3036
|
+
required: true,
|
|
3037
|
+
},
|
|
3038
|
+
{
|
|
3039
|
+
name: 'age',
|
|
3040
|
+
required: true,
|
|
3041
|
+
isVisible: true,
|
|
3042
|
+
},
|
|
3043
|
+
{
|
|
3044
|
+
name: 'passport_id',
|
|
3045
|
+
required: false,
|
|
3046
|
+
isVisible: true,
|
|
3047
|
+
},
|
|
3048
|
+
],
|
|
3049
|
+
});
|
|
3050
|
+
});
|
|
3051
|
+
|
|
3052
|
+
it('should hide conditionals fields when values do not fullfil conditions', () => {
|
|
3053
|
+
const result = createHeadlessForm(schemaFieldsetScopedCondition, {
|
|
3054
|
+
child: { has_child: 'no' },
|
|
3055
|
+
});
|
|
3056
|
+
|
|
3057
|
+
const fieldset = result.fields[0];
|
|
3058
|
+
|
|
3059
|
+
expect(fieldset).toMatchObject({
|
|
3060
|
+
fields: [
|
|
3061
|
+
{
|
|
3062
|
+
name: 'has_child',
|
|
3063
|
+
required: true,
|
|
3064
|
+
},
|
|
3065
|
+
{
|
|
3066
|
+
name: 'age',
|
|
3067
|
+
required: false,
|
|
3068
|
+
isVisible: false,
|
|
3069
|
+
},
|
|
3070
|
+
{
|
|
3071
|
+
name: 'passport_id',
|
|
3072
|
+
required: false,
|
|
3073
|
+
isVisible: false,
|
|
3074
|
+
},
|
|
3075
|
+
],
|
|
3076
|
+
});
|
|
3077
|
+
});
|
|
3078
|
+
});
|
|
3079
|
+
});
|
|
3080
|
+
|
|
3081
|
+
describe('parser options', () => {
|
|
3082
|
+
it('should support any custom field attribute', () => {
|
|
3083
|
+
const customAttrs = {
|
|
3084
|
+
something: 'foo', // a misc attribute
|
|
3085
|
+
inputType: 'super', // overrides "textarea"
|
|
3086
|
+
falsy: false, // accepts falsy attributes
|
|
3087
|
+
};
|
|
3088
|
+
const result = createHeadlessForm(
|
|
3089
|
+
{
|
|
3090
|
+
properties: {
|
|
3091
|
+
feedback: {
|
|
3092
|
+
title: 'Your feedback',
|
|
3093
|
+
type: 'string',
|
|
3094
|
+
presentation: {
|
|
3095
|
+
inputType: 'textarea',
|
|
3096
|
+
},
|
|
3097
|
+
},
|
|
3098
|
+
},
|
|
3099
|
+
},
|
|
3100
|
+
{
|
|
3101
|
+
customProperties: {
|
|
3102
|
+
feedback: {
|
|
3103
|
+
...customAttrs,
|
|
3104
|
+
},
|
|
3105
|
+
},
|
|
3106
|
+
}
|
|
3107
|
+
);
|
|
3108
|
+
|
|
3109
|
+
expect(result).toMatchObject({
|
|
3110
|
+
fields: [
|
|
3111
|
+
{
|
|
3112
|
+
name: 'feedback',
|
|
3113
|
+
label: 'Your feedback',
|
|
3114
|
+
jsonType: 'string',
|
|
3115
|
+
...customAttrs,
|
|
3116
|
+
},
|
|
3117
|
+
],
|
|
3118
|
+
});
|
|
3119
|
+
});
|
|
3120
|
+
|
|
3121
|
+
it('should support custom description (checkbox)', () => {
|
|
3122
|
+
const result = createHeadlessForm(
|
|
3123
|
+
{
|
|
3124
|
+
properties: {
|
|
3125
|
+
terms: {
|
|
3126
|
+
const: 'Agreed',
|
|
3127
|
+
title: 'Terms',
|
|
3128
|
+
description: 'Accept terms.',
|
|
3129
|
+
type: 'string',
|
|
3130
|
+
presentation: { inputType: 'checkbox' },
|
|
3131
|
+
},
|
|
3132
|
+
},
|
|
3133
|
+
},
|
|
3134
|
+
{
|
|
3135
|
+
customProperties: {
|
|
3136
|
+
terms: {
|
|
3137
|
+
description: (text) => `Extra text before. ${text}`,
|
|
3138
|
+
},
|
|
3139
|
+
},
|
|
3140
|
+
}
|
|
3141
|
+
);
|
|
3142
|
+
|
|
3143
|
+
expect(result).toMatchObject({
|
|
3144
|
+
fields: [
|
|
3145
|
+
{
|
|
3146
|
+
label: 'Terms',
|
|
3147
|
+
description: 'Extra text before. Accept terms.', // ensure custom description works
|
|
3148
|
+
name: 'terms',
|
|
3149
|
+
required: false,
|
|
3150
|
+
inputType: 'checkbox',
|
|
3151
|
+
type: 'checkbox',
|
|
3152
|
+
jsonType: 'string',
|
|
3153
|
+
checkboxValue: 'Agreed', // ensure _composeFieldCheckbox(). transformations are passed.
|
|
3154
|
+
},
|
|
3155
|
+
],
|
|
3156
|
+
});
|
|
3157
|
+
|
|
3158
|
+
// ensure _composeFieldCheckbox() "value" destructure happens.
|
|
3159
|
+
expect(result.fields[0]).not.toHaveProperty('value');
|
|
3160
|
+
});
|
|
3161
|
+
|
|
3162
|
+
it('should ignore fields that are not present in the schema', () => {
|
|
3163
|
+
const schemaBase = {
|
|
3164
|
+
properties: {
|
|
3165
|
+
feedback: {
|
|
3166
|
+
title: 'Your feedback',
|
|
3167
|
+
type: 'string',
|
|
3168
|
+
presentation: {
|
|
3169
|
+
inputType: 'textarea',
|
|
3170
|
+
},
|
|
3171
|
+
},
|
|
3172
|
+
},
|
|
3173
|
+
};
|
|
3174
|
+
|
|
3175
|
+
const resultWithoutCustomProperties = createHeadlessForm(schemaBase);
|
|
3176
|
+
const resultWithInvalidCustomProperty = createHeadlessForm(schemaBase, {
|
|
3177
|
+
customProperties: {
|
|
3178
|
+
unknown: {
|
|
3179
|
+
'data-foo': 'baz',
|
|
3180
|
+
},
|
|
3181
|
+
},
|
|
3182
|
+
});
|
|
3183
|
+
|
|
3184
|
+
function assertResultHasNoCustomizations(result) {
|
|
3185
|
+
expect(result.fields).toHaveLength(1); // The "unknown" is not present
|
|
3186
|
+
expect(result.fields[0].name).toBe('feedback');
|
|
3187
|
+
expect(result.fields[0]).not.toHaveProperty('data-foo');
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
assertResultHasNoCustomizations(resultWithoutCustomProperties);
|
|
3191
|
+
assertResultHasNoCustomizations(resultWithInvalidCustomProperty);
|
|
3192
|
+
});
|
|
3193
|
+
|
|
3194
|
+
it('should handle custom properties when inside fieldsets', () => {
|
|
3195
|
+
const result = createHeadlessForm(
|
|
3196
|
+
JSONSchemaBuilder()
|
|
3197
|
+
.addInput({
|
|
3198
|
+
id_number: mockNumberInput,
|
|
3199
|
+
})
|
|
3200
|
+
.addInput({
|
|
3201
|
+
fieldset: mockFieldset,
|
|
3202
|
+
})
|
|
3203
|
+
.addInput({ nestedFieldset: mockNestedFieldset })
|
|
3204
|
+
.build(),
|
|
3205
|
+
{
|
|
3206
|
+
customProperties: {
|
|
3207
|
+
id_number: { 'data-field': 'field' },
|
|
3208
|
+
fieldset: {
|
|
3209
|
+
id_number: { 'data-fieldset': 'fieldset' },
|
|
3210
|
+
},
|
|
3211
|
+
nestedFieldset: {
|
|
3212
|
+
innerFieldset: {
|
|
3213
|
+
id_number: { 'data-nested-fieldset': 'nested-fieldset' },
|
|
3214
|
+
},
|
|
3215
|
+
},
|
|
3216
|
+
},
|
|
3217
|
+
}
|
|
3218
|
+
);
|
|
3219
|
+
|
|
3220
|
+
expect(result).toMatchObject({
|
|
3221
|
+
fields: [
|
|
3222
|
+
{
|
|
3223
|
+
'data-field': 'field',
|
|
3224
|
+
name: 'id_number',
|
|
3225
|
+
},
|
|
3226
|
+
{
|
|
3227
|
+
name: 'fieldset',
|
|
3228
|
+
fields: [
|
|
3229
|
+
{
|
|
3230
|
+
name: 'id_number',
|
|
3231
|
+
'data-fieldset': 'fieldset',
|
|
3232
|
+
},
|
|
3233
|
+
{
|
|
3234
|
+
name: 'tabs',
|
|
3235
|
+
},
|
|
3236
|
+
],
|
|
3237
|
+
},
|
|
3238
|
+
{
|
|
3239
|
+
name: 'nestedFieldset',
|
|
3240
|
+
fields: [
|
|
3241
|
+
{
|
|
3242
|
+
name: 'innerFieldset',
|
|
3243
|
+
fields: [
|
|
3244
|
+
{
|
|
3245
|
+
name: 'id_number',
|
|
3246
|
+
'data-nested-fieldset': 'nested-fieldset',
|
|
3247
|
+
},
|
|
3248
|
+
{
|
|
3249
|
+
name: 'tabs',
|
|
3250
|
+
},
|
|
3251
|
+
],
|
|
3252
|
+
},
|
|
3253
|
+
],
|
|
3254
|
+
},
|
|
3255
|
+
],
|
|
3256
|
+
});
|
|
3257
|
+
|
|
3258
|
+
const [fieldResult, fildsetResult, nestedFieldsetResult] = result.fields;
|
|
3259
|
+
|
|
3260
|
+
// Sanity check that custom attrs are not "leaked" into other fields
|
|
3261
|
+
// $.id_number
|
|
3262
|
+
expect(fieldResult).toHaveProperty('name', 'id_number');
|
|
3263
|
+
expect(fieldResult).toHaveProperty('data-field', 'field');
|
|
3264
|
+
expect(fieldResult).not.toHaveProperty('data-fieldset');
|
|
3265
|
+
expect(fieldResult).not.toHaveProperty('data-nested-fieldset');
|
|
3266
|
+
|
|
3267
|
+
// $.fieldset.id_number
|
|
3268
|
+
expect(fildsetResult.fields[0]).toHaveProperty('name', 'id_number');
|
|
3269
|
+
expect(fildsetResult.fields[0]).toHaveProperty('data-fieldset', 'fieldset');
|
|
3270
|
+
expect(fildsetResult.fields[0]).not.toHaveProperty('data-field');
|
|
3271
|
+
expect(fildsetResult.fields[0]).not.toHaveProperty('data-nested-fieldset');
|
|
3272
|
+
expect(fildsetResult.fields[1]).not.toHaveProperty('data-field');
|
|
3273
|
+
expect(fildsetResult.fields[1]).not.toHaveProperty('data-nested-fieldset');
|
|
3274
|
+
|
|
3275
|
+
// $.nestedFieldset.innerFieldset.id_number
|
|
3276
|
+
expect(nestedFieldsetResult.fields[0].fields[0]).toHaveProperty('name', 'id_number');
|
|
3277
|
+
expect(nestedFieldsetResult.fields[0].fields[0]).toHaveProperty(
|
|
3278
|
+
'data-nested-fieldset',
|
|
3279
|
+
'nested-fieldset'
|
|
3280
|
+
);
|
|
3281
|
+
expect(nestedFieldsetResult.fields[0].fields[0]).not.toHaveProperty('data-field');
|
|
3282
|
+
expect(nestedFieldsetResult.fields[0].fields[0]).not.toHaveProperty('data-fieldset');
|
|
3283
|
+
expect(nestedFieldsetResult.fields[0].fields[1]).not.toHaveProperty('data-field');
|
|
3284
|
+
expect(nestedFieldsetResult.fields[0].fields[1]).not.toHaveProperty('data-fieldset');
|
|
3285
|
+
});
|
|
3286
|
+
});
|
|
3287
|
+
|
|
3288
|
+
describe('presentation (deprecated in favor of x-jsf-presentation)', () => {
|
|
3289
|
+
it('works well with position, description, inputType, and any other arbitrary attribute', () => {
|
|
3290
|
+
const { fields } = createHeadlessForm({
|
|
3291
|
+
properties: {
|
|
3292
|
+
day: {
|
|
3293
|
+
title: 'Date',
|
|
3294
|
+
presentation: {
|
|
3295
|
+
inputType: 'date',
|
|
3296
|
+
position: 1,
|
|
3297
|
+
foo: 'bar',
|
|
3298
|
+
statement: {
|
|
3299
|
+
description: 'ss',
|
|
3300
|
+
},
|
|
3301
|
+
},
|
|
3302
|
+
},
|
|
3303
|
+
time: {
|
|
3304
|
+
title: 'Time',
|
|
3305
|
+
presentation: {
|
|
3306
|
+
inputType: 'clock',
|
|
3307
|
+
description: 'Write in <b>hh:ss</b> format',
|
|
3308
|
+
position: 0,
|
|
3309
|
+
deprecated: {
|
|
3310
|
+
description: 'In favor of X',
|
|
3311
|
+
},
|
|
3312
|
+
},
|
|
3313
|
+
},
|
|
3314
|
+
},
|
|
3315
|
+
});
|
|
3316
|
+
|
|
3317
|
+
// Assert order from presentation.position
|
|
3318
|
+
expect(fields[0].name).toBe('time');
|
|
3319
|
+
expect(fields[1].name).toBe('day');
|
|
3320
|
+
|
|
3321
|
+
// Assert spreaded attributes
|
|
3322
|
+
expect(fields).toMatchObject([
|
|
3323
|
+
{
|
|
3324
|
+
name: 'time',
|
|
3325
|
+
description: '<span class="jsf-description">Write in <b>hh:ss</b> format</span>', // from presentation
|
|
3326
|
+
inputType: 'clock', // arbitrary type from presentation
|
|
3327
|
+
deprecated: {
|
|
3328
|
+
description: 'In favor of X', // from presentation
|
|
3329
|
+
},
|
|
3330
|
+
},
|
|
3331
|
+
{
|
|
3332
|
+
name: 'day',
|
|
3333
|
+
inputType: 'date', // arbitrary type from presentation
|
|
3334
|
+
foo: 'bar', // spread from presentation
|
|
3335
|
+
statement: {
|
|
3336
|
+
// from presentation
|
|
3337
|
+
description: 'ss',
|
|
3338
|
+
},
|
|
3339
|
+
},
|
|
3340
|
+
]);
|
|
3341
|
+
});
|
|
3342
|
+
});
|
|
3343
|
+
});
|