@remoteoss/json-schema-form 0.11.11-dev.20250220174843 → 1.0.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.
@@ -1,4324 +0,0 @@
1
- import isNil from 'lodash/isNil';
2
- import omitBy from 'lodash/omitBy';
3
- import { object } from 'yup';
4
-
5
- import {
6
- JSONSchemaBuilder,
7
- schemaInputTypeText,
8
- schemaInputTypeRadioDeprecated,
9
- schemaInputTypeRadioString,
10
- schemaInputTypeRadioStringY,
11
- schemaInputTypeRadioBoolean,
12
- schemaInputTypeRadioNumber,
13
- schemaInputTypeRadioRequiredAndOptional,
14
- schemaInputRadioOptionalNull,
15
- schemaInputRadioOptionalConventional,
16
- schemaInputTypeRadioOptionsWithDetails,
17
- schemaInputTypeRadioWithoutOptions,
18
- schemaInputTypeSelectSoloDeprecated,
19
- schemaInputTypeSelectSolo,
20
- schemaInputTypeSelectMultipleDeprecated,
21
- schemaInputTypeSelectMultiple,
22
- schemaInputTypeSelectMultipleOptional,
23
- schemaInputTypeFieldset,
24
- schemaInputTypeIntegerNumber,
25
- schemaInputTypeNumber,
26
- schemaInputTypeNumberZeroMaximum,
27
- schemaInputTypeDate,
28
- schemaInputTypeEmail,
29
- schemaInputWithStatement,
30
- schemaInputTypeCheckbox,
31
- schemaInputTypeCheckboxBooleans,
32
- schemaInputTypeCheckboxBooleanConditional,
33
- schemaInputTypeNull,
34
- schemaWithOrderKeyword,
35
- schemaWithPositionDeprecated,
36
- schemaDynamicValidationConst,
37
- schemaDynamicValidationMinimumMaximum,
38
- schemaDynamicValidationMinLengthMaxLength,
39
- schemaDynamicValidationContains,
40
- schemaAnyOfValidation,
41
- schemaWithoutInputTypes,
42
- schemaWithoutTypes,
43
- mockFileInput,
44
- mockRadioCardInput,
45
- mockRadioCardExpandableInput,
46
- mockTelWithPattern,
47
- mockTextInput,
48
- mockTextInputDeprecated,
49
- mockNumberInput,
50
- mockNumberInputWithPercentageAndCustomRange,
51
- mockTextPatternInput,
52
- mockTextMaxLengthInput,
53
- mockFieldset,
54
- mockNestedFieldset,
55
- mockGroupArrayInput,
56
- schemaFieldsetScopedCondition,
57
- schemaWithConditionalToFieldset,
58
- schemaWithConditionalPresentationProperties,
59
- schemaWithConditionalReadOnlyProperty,
60
- schemaWithWrongConditional,
61
- schemaWithConditionalAcknowledgementProperty,
62
- schemaInputTypeNumberWithPercentage,
63
- schemaForErrorMessageSpecificity,
64
- jsfConfigForErrorMessageSpecificity,
65
- schemaInputTypeFile,
66
- nestedGroupArrayForm,
67
- } from './helpers';
68
- import { mockConsole, restoreConsoleAndEnsureItWasNotCalled } from './testUtils';
69
- import { createHeadlessForm } from '@/createHeadlessForm';
70
-
71
- function buildJSONSchemaInput({ presentationFields, inputFields = {}, required }) {
72
- return {
73
- type: 'object',
74
- properties: {
75
- test: {
76
- description: 'Test description',
77
- presentation: {
78
- ...presentationFields,
79
- },
80
- title: 'Test title',
81
- type: 'number',
82
- ...inputFields,
83
- },
84
- },
85
- required: required ? ['test'] : [],
86
- };
87
- }
88
-
89
- function friendlyError({ formErrors }) {
90
- // destruct the formErrors directly
91
- return formErrors;
92
- }
93
-
94
- // Get a field by name recursively
95
- // eg getField(demo, "age") -> returns "age" field
96
- // eg getField(demo, child, name) -> returns "child.name" subfield
97
- const getField = (fields, name, ...subNames) => {
98
- const field = fields.find((f) => f.name === name);
99
- if (subNames.length > 0) {
100
- return getField(field.fields, ...subNames);
101
- }
102
- return field;
103
- };
104
-
105
- beforeEach(mockConsole);
106
- afterEach(restoreConsoleAndEnsureItWasNotCalled);
107
-
108
- describe('createHeadlessForm', () => {
109
- it('returns empty result given no schema', () => {
110
- const result = createHeadlessForm();
111
-
112
- expect(result).toMatchObject({
113
- fields: [],
114
- });
115
- expect(result.isError).toBe(false);
116
- expect(result.error).toBeFalsy();
117
- });
118
-
119
- it('returns an error given invalid schema', () => {
120
- const result = createHeadlessForm({ foo: 1 });
121
-
122
- expect(result.fields).toHaveLength(0);
123
- expect(result.isError).toBe(true);
124
-
125
- expect(console.error).toHaveBeenCalledWith(`JSON Schema invalid!`, expect.any(Error));
126
- console.error.mockClear();
127
-
128
- expect(result.error.message).toBe(`Cannot convert undefined or null to object`);
129
- });
130
-
131
- describe('field support fallback', () => {
132
- it('sets type from presentation.inputType', () => {
133
- const { fields } = createHeadlessForm({
134
- properties: {
135
- age: {
136
- title: 'Age',
137
- presentation: { inputType: 'number' },
138
- type: 'number',
139
- },
140
- starting_time: {
141
- title: 'Starting time',
142
- presentation: {
143
- inputType: 'hour', // Arbitrary types are accepted
144
- set: 'AM', // And even any arbitrary presentation keys
145
- },
146
- type: 'string',
147
- },
148
- },
149
- });
150
-
151
- const { schema: yupSchema1, ...fieldAge } = omitBy(fields[0], isNil);
152
- const { schema: yupSchema2, ...fieldTime } = omitBy(fields[1], isNil);
153
-
154
- expect(yupSchema1).toEqual(expect.any(Object));
155
- expect(fieldAge).toMatchObject({
156
- inputType: 'number',
157
- jsonType: 'number',
158
- type: 'number',
159
- });
160
-
161
- expect(yupSchema1).toEqual(expect.any(Object));
162
- expect(fieldTime).toMatchObject({
163
- inputType: 'hour',
164
- jsonType: 'string',
165
- name: 'starting_time',
166
- type: 'hour',
167
- set: 'AM',
168
- });
169
- });
170
-
171
- it('fails given a json schema without inputType', () => {
172
- const { fields, error } = createHeadlessForm({
173
- properties: {
174
- test: { type: 'string' },
175
- },
176
- });
177
-
178
- expect(fields).toHaveLength(0);
179
- expect(error.message).toContain('Strict error: Missing inputType to field "test"');
180
-
181
- expect(console.error).toHaveBeenCalledWith(`JSON Schema invalid!`, expect.any(Error));
182
- console.error.mockClear();
183
- });
184
-
185
- function extractTypeOnly(listOfFields) {
186
- const list = Array.isArray(listOfFields) ? listOfFields : listOfFields?.(); // handle fieldset + group-array
187
- return list?.map(
188
- ({ name, type, inputType, jsonType, label, options, fields: nestedFields }) => {
189
- return omitBy(
190
- {
191
- name,
192
- type, // @deprecated
193
- inputType,
194
- jsonType,
195
- label,
196
- options,
197
- fields: extractTypeOnly(nestedFields),
198
- },
199
- isNil
200
- );
201
- }
202
- );
203
- }
204
-
205
- it('given a json schema without inputType, sets type based on json type (when strictInputType:false)', () => {
206
- const { fields } = createHeadlessForm(schemaWithoutInputTypes, {
207
- strictInputType: false,
208
- });
209
-
210
- const fieldsByNameAndType = extractTypeOnly(fields);
211
- expect(fieldsByNameAndType).toMatchInlineSnapshot(`
212
- [
213
- {
214
- "inputType": "text",
215
- "jsonType": "string",
216
- "label": "A string -> text",
217
- "name": "a_string",
218
- "type": "text",
219
- },
220
- {
221
- "inputType": "radio",
222
- "jsonType": "string",
223
- "label": "A string with oneOf -> radio",
224
- "name": "a_string_oneOf",
225
- "options": [
226
- {
227
- "label": "Yes",
228
- "value": "yes",
229
- },
230
- {
231
- "label": "No",
232
- "value": "no",
233
- },
234
- ],
235
- "type": "radio",
236
- },
237
- {
238
- "inputType": "email",
239
- "jsonType": "string",
240
- "label": "A string with format:email -> email",
241
- "name": "a_string_email",
242
- "type": "email",
243
- },
244
- {
245
- "inputType": "date",
246
- "jsonType": "string",
247
- "label": "A string with format:email -> date",
248
- "name": "a_string_date",
249
- "type": "date",
250
- },
251
- {
252
- "inputType": "file",
253
- "jsonType": "string",
254
- "label": "A string with format:data-url -> file",
255
- "name": "a_string_file",
256
- "type": "file",
257
- },
258
- {
259
- "inputType": "number",
260
- "jsonType": "number",
261
- "label": "A number -> number",
262
- "name": "a_number",
263
- "type": "number",
264
- },
265
- {
266
- "inputType": "number",
267
- "jsonType": "integer",
268
- "label": "A integer -> number",
269
- "name": "a_integer",
270
- "type": "number",
271
- },
272
- {
273
- "inputType": "checkbox",
274
- "jsonType": "boolean",
275
- "label": "A boolean -> checkbox",
276
- "name": "a_boolean",
277
- "type": "checkbox",
278
- },
279
- {
280
- "fields": [
281
- {
282
- "inputType": "text",
283
- "jsonType": "string",
284
- "name": "foo",
285
- "type": "text",
286
- },
287
- {
288
- "inputType": "text",
289
- "jsonType": "string",
290
- "name": "bar",
291
- "type": "text",
292
- },
293
- ],
294
- "inputType": "fieldset",
295
- "jsonType": "object",
296
- "label": "An object -> fieldset",
297
- "name": "a_object",
298
- "type": "fieldset",
299
- },
300
- {
301
- "inputType": "select",
302
- "jsonType": "array",
303
- "label": "An array items.anyOf -> select",
304
- "name": "a_array_items",
305
- "options": [
306
- {
307
- "label": "Chrome",
308
- "value": "chr",
309
- },
310
- {
311
- "label": "Firefox",
312
- "value": "ff",
313
- },
314
- {
315
- "label": "Internet Explorer",
316
- "value": "ie",
317
- },
318
- ],
319
- "type": "select",
320
- },
321
- {
322
- "fields": [
323
- {
324
- "inputType": "text",
325
- "jsonType": "string",
326
- "label": "Role",
327
- "name": "role",
328
- "type": "text",
329
- },
330
- {
331
- "inputType": "number",
332
- "jsonType": "number",
333
- "label": "Years",
334
- "name": "years",
335
- "type": "number",
336
- },
337
- ],
338
- "inputType": "group-array",
339
- "jsonType": "array",
340
- "label": "An array items.properties -> group-array",
341
- "name": "a_array_properties",
342
- "type": "group-array",
343
- },
344
- {
345
- "inputType": "text",
346
- "label": "A void -> text",
347
- "name": "a_void",
348
- "type": "text",
349
- },
350
- ]
351
- `);
352
- });
353
-
354
- it('given a json schema without json type, sets type based on structure (when strictInputType:false)', () => {
355
- const { fields } = createHeadlessForm(schemaWithoutTypes, {
356
- strictInputType: false,
357
- });
358
-
359
- const fieldsByNameAndType = extractTypeOnly(fields);
360
- expect(fieldsByNameAndType).toMatchInlineSnapshot(`
361
- [
362
- {
363
- "inputType": "text",
364
- "label": "Default -> text",
365
- "name": "default",
366
- "type": "text",
367
- },
368
- {
369
- "inputType": "radio",
370
- "label": "With oneOf -> radio",
371
- "name": "with_oneOf",
372
- "options": [
373
- {
374
- "label": "Yes",
375
- "value": "yes",
376
- },
377
- {
378
- "label": "No",
379
- "value": "no",
380
- },
381
- ],
382
- "type": "radio",
383
- },
384
- {
385
- "inputType": "email",
386
- "label": "With format:email -> email",
387
- "name": "with_email",
388
- "type": "email",
389
- },
390
- {
391
- "inputType": "select",
392
- "label": "With properties -> fieldset",
393
- "name": "with_object",
394
- "type": "select",
395
- },
396
- {
397
- "inputType": "text",
398
- "label": "With items.anyOf -> select",
399
- "name": "with_items_anyOf",
400
- "options": [
401
- {
402
- "label": "Chrome",
403
- "value": "chr",
404
- },
405
- {
406
- "label": "Firefox",
407
- "value": "ff",
408
- },
409
- {
410
- "label": "Internet Explorer",
411
- "value": "ie",
412
- },
413
- ],
414
- "type": "text",
415
- },
416
- {
417
- "fields": [
418
- {
419
- "inputType": "text",
420
- "label": "Role",
421
- "name": "role",
422
- "type": "text",
423
- },
424
- {
425
- "inputType": "text",
426
- "label": "Years",
427
- "name": "years",
428
- "type": "text",
429
- },
430
- ],
431
- "inputType": "group-array",
432
- "label": "With items.properties -> group-array",
433
- "name": "with_items_properties",
434
- "type": "group-array",
435
- },
436
- ]
437
- `);
438
- });
439
- });
440
-
441
- describe('field support', () => {
442
- function assertOptionsAllowed({ handleValidation, fieldName, validOptions, type = 'string' }) {
443
- const validateForm = (vals) => friendlyError(handleValidation(vals));
444
-
445
- // All allowed options are valid
446
- validOptions.forEach((value) => {
447
- expect(validateForm({ [fieldName]: value })).toBeUndefined();
448
- });
449
-
450
- if (type === 'string') {
451
- // Any other arbitrary value is not valid.
452
- expect(validateForm({ [fieldName]: 'blah-blah' })).toEqual({
453
- [fieldName]: 'The option "blah-blah" is not valid.',
454
- });
455
-
456
- // As required field, empty string ("") is also considered empty. @BUG RMT-518
457
- // Expectation: The error to be "The option '' is not valid."
458
- expect(validateForm({ [fieldName]: '' })).toEqual({
459
- [fieldName]: 'Required field',
460
- });
461
- }
462
-
463
- // Given undefined, it says it's a required field.
464
- expect(validateForm({})).toEqual({
465
- [fieldName]: 'Required field',
466
- });
467
-
468
- // As required field, null is also considered empty @BUG RMT-518
469
- // Expectation: The error to be "The option null is not valid."
470
- expect(validateForm({ [fieldName]: null })).toEqual({
471
- [fieldName]: 'Required field',
472
- });
473
- }
474
-
475
- it('support "text" field type', () => {
476
- const { fields, handleValidation } = createHeadlessForm(schemaInputTypeText);
477
-
478
- expect(fields[0]).toMatchObject({
479
- description: 'Your username (max 10 characters)',
480
- label: 'Username',
481
- name: 'username',
482
- required: true,
483
- schema: expect.any(Object),
484
- inputType: 'text',
485
- jsonType: 'string',
486
- maskSecret: 2,
487
- maxLength: 10,
488
- isVisible: true,
489
- });
490
-
491
- const fieldValidator = fields[0].schema;
492
- expect(fieldValidator.isValidSync('CI007')).toBe(true);
493
- expect(fieldValidator.isValidSync(true)).toBe(false);
494
- expect(fieldValidator.isValidSync(1)).toBe(false);
495
- expect(fieldValidator.isValidSync(0)).toBe(false);
496
-
497
- expect(handleValidation({ username: 1 }).formErrors).toEqual({
498
- username: 'username must be a `string` type, but the final value was: `1`.',
499
- });
500
-
501
- expect(() => fieldValidator.validateSync('')).toThrowError('Required field');
502
- });
503
-
504
- describe('support "select" field type', () => {
505
- it('support "select" field type @deprecated', () => {
506
- const { fields, handleValidation } = createHeadlessForm(
507
- schemaInputTypeSelectSoloDeprecated
508
- );
509
-
510
- expect(fields).toMatchObject([
511
- {
512
- description: 'Life Insurance',
513
- label: 'Benefits (solo)',
514
- name: 'benefits',
515
- placeholder: 'Select...',
516
- type: 'select',
517
- options: [
518
- {
519
- label: 'Medical Insurance',
520
- value: 'Medical Insurance',
521
- },
522
- {
523
- label: 'Health Insurance',
524
- value: 'Health Insurance',
525
- },
526
- {
527
- label: 'Travel Bonus',
528
- value: 'Travel Bonus',
529
- },
530
- ],
531
- },
532
- ]);
533
-
534
- assertOptionsAllowed({
535
- handleValidation,
536
- fieldName: 'benefits',
537
- validOptions: ['Medical Insurance', 'Health Insurance', 'Travel Bonus'],
538
- });
539
- });
540
-
541
- it('support "select" field type', () => {
542
- const { fields, handleValidation } = createHeadlessForm(schemaInputTypeSelectSolo);
543
-
544
- const fieldSelect = fields[0];
545
- expect(fieldSelect).toMatchObject({
546
- name: 'browsers',
547
- label: 'Browsers (solo)',
548
- description: 'This solo select also includes a disabled option.',
549
- options: [
550
- {
551
- value: 'chr',
552
- label: 'Chrome',
553
- },
554
- {
555
- value: 'ff',
556
- label: 'Firefox',
557
- },
558
- {
559
- value: 'ie',
560
- label: 'Internet Explorer',
561
- disabled: true,
562
- },
563
- ],
564
- });
565
-
566
- expect(fieldSelect).not.toHaveProperty('multiple');
567
-
568
- assertOptionsAllowed({
569
- handleValidation,
570
- fieldName: 'browsers',
571
- validOptions: ['chr', 'ff', 'ie'],
572
- });
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
- describe('support "radio" field type', () => {
663
- it('support "radio" field type @deprecated', () => {
664
- const { fields, handleValidation } = createHeadlessForm(schemaInputTypeRadioDeprecated);
665
-
666
- expect(fields).toMatchObject([
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
- assertOptionsAllowed({
688
- handleValidation,
689
- fieldName: 'has_siblings',
690
- validOptions: ['yes', 'no'],
691
- });
692
- });
693
- it('support "radio" field string type', () => {
694
- const { fields, handleValidation } = createHeadlessForm(schemaInputTypeRadioString);
695
-
696
- expect(fields).toMatchObject([
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
- assertOptionsAllowed({
718
- handleValidation,
719
- fieldName: 'has_siblings',
720
- validOptions: ['yes', 'no'],
721
- });
722
- });
723
-
724
- it('support "radio" field boolean type', () => {
725
- const { fields, handleValidation } = createHeadlessForm(schemaInputTypeRadioBoolean);
726
-
727
- const validateForm = (vals) => friendlyError(handleValidation(vals));
728
-
729
- expect(fields).toMatchObject([
730
- {
731
- description: 'Are you over 18 years old?',
732
- label: 'Over 18',
733
- name: 'over_18',
734
- options: [
735
- {
736
- label: 'Yes',
737
- value: true,
738
- },
739
- {
740
- label: 'No',
741
- value: false,
742
- },
743
- ],
744
- required: true,
745
- schema: expect.any(Object),
746
- type: 'radio',
747
- },
748
- ]);
749
-
750
- assertOptionsAllowed({
751
- handleValidation,
752
- fieldName: 'over_18',
753
- validOptions: [true, false],
754
- type: schemaInputTypeRadioBoolean.properties.over_18.type,
755
- });
756
-
757
- expect(validateForm({ over_18: 'true' })).toEqual({
758
- over_18: 'The option "true" is not valid.',
759
- });
760
- });
761
-
762
- it('supports "radio" field type with its "card" and "card-expandable" variants', () => {
763
- const result = createHeadlessForm(
764
- JSONSchemaBuilder()
765
- .addInput({
766
- experience_level: mockRadioCardExpandableInput,
767
- payment_method: mockRadioCardInput,
768
- })
769
- .build()
770
- );
771
-
772
- expect(result).toMatchObject({
773
- fields: [
774
- {
775
- description:
776
- 'Please select the experience level that aligns with this role based on the job description (not the employees overall experience)',
777
- label: 'Experience level',
778
- name: 'experience_level',
779
- type: 'radio',
780
- required: false,
781
- variant: 'card-expandable',
782
- options: [
783
- {
784
- label: 'Junior level',
785
- value: 'junior',
786
- description:
787
- 'Entry level employees who perform tasks under the supervision of a more experienced employee.',
788
- },
789
- {
790
- label: 'Mid level',
791
- value: 'mid',
792
- description:
793
- 'Employees who perform tasks with a good degree of autonomy and/or with coordination and control functions.',
794
- },
795
- {
796
- label: 'Senior level',
797
- value: 'senior',
798
- description:
799
- 'Employees who perform tasks with a high degree of autonomy and/or with coordination and control functions.',
800
- },
801
- ],
802
- },
803
- {
804
- description: 'Chose how you want to be paid',
805
- label: 'Payment method',
806
- name: 'payment_method',
807
- type: 'radio',
808
- variant: 'card',
809
- required: false,
810
- options: [
811
- {
812
- label: 'Credit Card',
813
- value: 'cc',
814
- description: 'Plastic money, which is still money',
815
- },
816
- {
817
- label: 'Cash',
818
- value: 'cash',
819
- description: 'Rules Everything Around Me',
820
- },
821
- ],
822
- },
823
- ],
824
- });
825
- });
826
-
827
- // @BUG COD-1859
828
- // it should validate when type is string but value is not a boolean
829
- it('support "radio" field string-y type', () => {
830
- const { fields, handleValidation } = createHeadlessForm(schemaInputTypeRadioStringY);
831
-
832
- const validateForm = (vals) => friendlyError(handleValidation(vals));
833
-
834
- expect(fields).toMatchObject([
835
- {
836
- description: 'Do you have any siblings?',
837
- label: 'Has siblings',
838
- name: 'has_siblings',
839
- options: [
840
- {
841
- label: 'Yes',
842
- value: 'true',
843
- },
844
- {
845
- label: 'No',
846
- value: 'false',
847
- },
848
- ],
849
- required: true,
850
- schema: expect.any(Object),
851
- type: 'radio',
852
- },
853
- ]);
854
-
855
- assertOptionsAllowed({
856
- handleValidation,
857
- fieldName: 'has_siblings',
858
- validOptions: ['true', 'false'],
859
- });
860
-
861
- expect(validateForm({ has_siblings: false })).toEqual({
862
- has_siblings: 'The option "false" is not valid.',
863
- });
864
- });
865
-
866
- it('support "radio" field number type', () => {
867
- const { fields, handleValidation } = createHeadlessForm(schemaInputTypeRadioNumber);
868
-
869
- const validateForm = (vals) => friendlyError(handleValidation(vals));
870
-
871
- expect(fields).toMatchObject([
872
- {
873
- description: 'How many siblings do you have?',
874
- label: 'Number of siblings',
875
- name: 'siblings_count',
876
- options: [
877
- {
878
- label: 'One',
879
- value: 1,
880
- },
881
- {
882
- label: 'Two',
883
- value: 2,
884
- },
885
- {
886
- label: 'Three',
887
- value: 3,
888
- },
889
- ],
890
- required: true,
891
- schema: expect.any(Object),
892
- type: 'radio',
893
- },
894
- ]);
895
-
896
- assertOptionsAllowed({
897
- handleValidation,
898
- fieldName: 'siblings_count',
899
- validOptions: [1, 2, 3],
900
- type: schemaInputTypeRadioNumber.properties.siblings_count.type,
901
- });
902
-
903
- expect(validateForm({ siblings_count: '3' })).toEqual({
904
- siblings_count: 'The option "3" is not valid.',
905
- });
906
- });
907
-
908
- it('support "radio" optional field', () => {
909
- const { fields, handleValidation } = createHeadlessForm(
910
- schemaInputTypeRadioRequiredAndOptional
911
- );
912
- const validateForm = (vals) => friendlyError(handleValidation(vals));
913
-
914
- expect(fields).toMatchObject([
915
- {},
916
- {
917
- name: 'has_car',
918
- label: 'Has car',
919
- description: 'Do you have a car? (optional field, check oneOf)',
920
- options: [
921
- {
922
- label: 'Yes',
923
- value: 'yes',
924
- },
925
- {
926
- label: 'No',
927
- value: 'no',
928
- },
929
- ],
930
- required: false,
931
- schema: expect.any(Object),
932
- type: 'radio',
933
- },
934
- ]);
935
-
936
- expect(
937
- validateForm({
938
- has_siblings: 'yes',
939
- has_car: 'yes',
940
- })
941
- ).toBeUndefined();
942
-
943
- expect(validateForm({})).toEqual({
944
- has_siblings: 'Required field',
945
- });
946
- });
947
-
948
- function assertCommonBehavior(validateForm) {
949
- // Note: Very similar to assertOptionsAllowed()
950
- // We could reuse it in a next iteration.
951
-
952
- // Happy path
953
- expect(validateForm({ has_car: 'yes' })).toBeUndefined();
954
-
955
- // Accepts undefined field
956
- expect(validateForm({})).toBeUndefined();
957
-
958
- // Does not accept other values
959
- expect(validateForm({ has_car: 'blah-blah' })).toEqual({
960
- has_car: 'The option "blah-blah" is not valid.',
961
- });
962
-
963
- // Does not accept "null" as string
964
- expect(validateForm({ has_car: 'null' })).toEqual({
965
- has_car: 'The option "null" is not valid.',
966
- });
967
-
968
- // Accepts empty string ("") — @BUG RMT-518
969
- // Expectation: Does not accept empty string ("")
970
- expect(validateForm({ has_car: '' })).toBeUndefined();
971
- }
972
-
973
- it('support "radio" optional field - optional (conventional way) - @BUG RMT-518', () => {
974
- const { handleValidation } = createHeadlessForm(schemaInputRadioOptionalConventional);
975
- const validateForm = (vals) => friendlyError(handleValidation(vals));
976
-
977
- assertCommonBehavior(validateForm);
978
-
979
- // Accepts null, even though it shouldn't @BUG RMT-518
980
- // This is for cases where we (Remote) still have incorrect
981
- // JSON Schemas in our Platform.
982
- expect(validateForm({ has_car: null })).toBeUndefined();
983
- // Expected:
984
- // // Does NOT accept null value
985
- // expect(validateForm({ has_car: null })).toEqual({
986
- // has_car: 'The option null is not valid.',
987
- // });
988
- });
989
-
990
- it('support "radio" optional field - optional with null option (as Remote does) - @BUG RMT-518', () => {
991
- const { handleValidation } = createHeadlessForm(schemaInputRadioOptionalNull);
992
- const validateForm = (vals) => friendlyError(handleValidation(vals));
993
-
994
- assertCommonBehavior(validateForm);
995
-
996
- // Accepts null value
997
- expect(validateForm({ has_car: null })).toBeUndefined();
998
- });
999
-
1000
- it('support "radio" field type with extra info inside each option', () => {
1001
- const result = createHeadlessForm(schemaInputTypeRadioOptionsWithDetails);
1002
-
1003
- expect(result.fields).toHaveLength(1);
1004
-
1005
- const fieldOptions = result.fields[0].options;
1006
-
1007
- // The x-jsf-presentation content was spread to the root:
1008
- expect(fieldOptions[0]).not.toHaveProperty('x-jsf-presentation');
1009
- expect(fieldOptions).toEqual([
1010
- {
1011
- label: 'Basic',
1012
- value: 'basic',
1013
- meta: {
1014
- displayCost: '$30.00/mo',
1015
- },
1016
- // Other x-* keywords are kept as it is.
1017
- 'x-another': 'extra-thing',
1018
- },
1019
- {
1020
- label: 'Standard',
1021
- value: 'standard',
1022
- meta: {
1023
- displayCost: '$50.00/mo',
1024
- },
1025
- },
1026
- ]);
1027
- });
1028
-
1029
- it('supports oneOf pattern validation', () => {
1030
- const result = createHeadlessForm(mockTelWithPattern);
1031
-
1032
- expect(result).toMatchObject({
1033
- fields: [
1034
- {
1035
- label: 'Phone number',
1036
- name: 'phone_number',
1037
- type: 'tel',
1038
- required: false,
1039
- options: [
1040
- {
1041
- label: 'Portugal',
1042
- pattern: '^(\\+351)[0-9]{9,}$',
1043
- },
1044
- {
1045
- label: 'United Kingdom (UK)',
1046
- pattern: '^(\\+44)[0-9]{1,}$',
1047
- },
1048
- {
1049
- label: 'Bolivia',
1050
- pattern: '^(\\+591)[0-9]{9,}$',
1051
- },
1052
- {
1053
- label: 'Canada',
1054
- pattern: '^(\\+1)(206|224)[0-9]{1,}$',
1055
- },
1056
- {
1057
- label: 'United States',
1058
- pattern: '^(\\+1)[0-9]{1,}$',
1059
- },
1060
- ],
1061
- },
1062
- ],
1063
- });
1064
-
1065
- const fieldValidator = result.fields[0].schema;
1066
-
1067
- expect(fieldValidator.isValidSync('+351123123123')).toBe(true);
1068
- expect(() => fieldValidator.validateSync('+35100')).toThrowError(
1069
- 'The option "+35100" is not valid.'
1070
- );
1071
- expect(fieldValidator.isValidSync(undefined)).toBe(true);
1072
- });
1073
-
1074
- it('support "radio" field type without oneOf options', () => {
1075
- const result = createHeadlessForm(schemaInputTypeRadioWithoutOptions);
1076
-
1077
- expect(result.fields).toHaveLength(1);
1078
-
1079
- const fieldOptions = result.fields[0].options;
1080
- expect(fieldOptions).toEqual([]);
1081
- });
1082
- });
1083
-
1084
- it('support "integer" field type', () => {
1085
- const result = createHeadlessForm(schemaInputTypeIntegerNumber);
1086
- expect(result).toMatchObject({
1087
- fields: [
1088
- {
1089
- description: 'How many open tabs do you have?',
1090
- label: 'Tabs',
1091
- name: 'tabs',
1092
- required: false,
1093
- schema: expect.any(Object),
1094
- type: 'number',
1095
- jsonType: 'integer',
1096
- inputType: 'number',
1097
- minimum: 1,
1098
- maximum: 10,
1099
- },
1100
- ],
1101
- });
1102
-
1103
- const fieldValidator = result.fields[0].schema;
1104
- expect(fieldValidator.isValidSync('0')).toBe(false);
1105
- expect(fieldValidator.isValidSync('10')).toBe(true);
1106
- expect(fieldValidator.isValidSync('11')).toBe(false);
1107
- expect(fieldValidator.isValidSync('5.5')).toBe(false);
1108
- expect(fieldValidator.isValidSync('1.0')).toBe(true);
1109
- expect(fieldValidator.isValidSync('this is text with a number 1')).toBe(false);
1110
- expect(() => fieldValidator.validateSync('5.5')).toThrowError(
1111
- 'Must not contain decimal points. E.g. 5 instead of 5.5'
1112
- );
1113
- expect(() => fieldValidator.validateSync('some text')).toThrowError(
1114
- 'The value must be a number'
1115
- );
1116
- expect(() => fieldValidator.validateSync('')).toThrowError('The value must be a number');
1117
- });
1118
-
1119
- it('support "number" field type', () => {
1120
- const result = createHeadlessForm(schemaInputTypeNumber);
1121
- expect(result).toMatchObject({
1122
- fields: [
1123
- {
1124
- description: 'How many open tabs do you have?',
1125
- label: 'Tabs',
1126
- name: 'tabs',
1127
- required: true,
1128
- schema: expect.any(Object),
1129
- type: 'number',
1130
- minimum: 1,
1131
- maximum: 10,
1132
- },
1133
- ],
1134
- });
1135
-
1136
- const fieldValidator = result.fields[0].schema;
1137
- expect(fieldValidator.isValidSync('0')).toBe(false);
1138
- expect(fieldValidator.isValidSync('10')).toBe(true);
1139
- expect(fieldValidator.isValidSync('11')).toBe(false);
1140
- expect(fieldValidator.isValidSync('this is text with a number 1')).toBe(false);
1141
- expect(() => fieldValidator.validateSync('some text')).toThrowError(
1142
- 'The value must be a number'
1143
- );
1144
- expect(() => fieldValidator.validateSync('')).toThrowError('The value must be a number');
1145
- });
1146
-
1147
- it('support "number" field type with the percentage attribute', () => {
1148
- const result = createHeadlessForm(schemaInputTypeNumberWithPercentage);
1149
- expect(result).toMatchObject({
1150
- fields: [
1151
- {
1152
- description: 'What % of shares do you own?',
1153
- label: 'Shares',
1154
- name: 'shares',
1155
- percentage: true,
1156
- required: true,
1157
- schema: expect.any(Object),
1158
- type: 'number',
1159
- minimum: 1,
1160
- maximum: 100,
1161
- },
1162
- ],
1163
- });
1164
-
1165
- const fieldValidator = result.fields[0].schema;
1166
- const { percentage } = result.fields[0];
1167
- expect(fieldValidator.isValidSync('0')).toBe(false);
1168
- expect(fieldValidator.isValidSync('10')).toBe(true);
1169
- expect(fieldValidator.isValidSync('101')).toBe(false);
1170
- expect(fieldValidator.isValidSync('this is text with a number 1')).toBe(false);
1171
- expect(() => fieldValidator.validateSync('some text')).toThrowError(
1172
- 'The value must be a number'
1173
- );
1174
- expect(() => fieldValidator.validateSync('')).toThrowError('The value must be a number');
1175
- expect(percentage).toBe(true);
1176
- });
1177
-
1178
- it('support "number" field type with the percentage attribute and custom range values', () => {
1179
- const result = createHeadlessForm(
1180
- JSONSchemaBuilder()
1181
- .addInput({
1182
- shares: {
1183
- ...mockNumberInputWithPercentageAndCustomRange,
1184
- },
1185
- })
1186
- .setRequiredFields(['shares'])
1187
- .build()
1188
- );
1189
-
1190
- expect(result).toMatchObject({
1191
- fields: [
1192
- {
1193
- description: 'What % of shares do you own?',
1194
- label: 'Shares',
1195
- name: 'shares',
1196
- percentage: true,
1197
- required: true,
1198
- schema: expect.any(Object),
1199
- type: 'number',
1200
- minimum: 50,
1201
- maximum: 70,
1202
- },
1203
- ],
1204
- });
1205
-
1206
- const fieldValidatorCustom = result.fields[0].schema;
1207
- const { percentage: percentageCustom } = result.fields[0];
1208
- expect(fieldValidatorCustom.isValidSync('0')).toBe(false);
1209
- expect(fieldValidatorCustom.isValidSync('49')).toBe(false);
1210
- expect(fieldValidatorCustom.isValidSync('55')).toBe(true);
1211
- expect(fieldValidatorCustom.isValidSync('70')).toBe(true);
1212
- expect(fieldValidatorCustom.isValidSync('101')).toBe(false);
1213
- expect(fieldValidatorCustom.isValidSync('this is text with a number 1')).toBe(false);
1214
- expect(() => fieldValidatorCustom.validateSync('some text')).toThrowError(
1215
- 'The value must be a number'
1216
- );
1217
- expect(() => fieldValidatorCustom.validateSync('')).toThrowError(
1218
- 'The value must be a number'
1219
- );
1220
- expect(percentageCustom).toBe(true);
1221
- });
1222
-
1223
- it('support "date" field type', () => {
1224
- const { fields, handleValidation } = createHeadlessForm(schemaInputTypeDate);
1225
-
1226
- const validateForm = (vals) => friendlyError(handleValidation(vals));
1227
-
1228
- expect(fields[0]).toMatchObject({
1229
- label: 'Birthdate',
1230
- name: 'birthdate',
1231
- required: true,
1232
- schema: expect.any(Object),
1233
- type: 'date',
1234
- minDate: '1922-03-01',
1235
- maxDate: '2022-03-17',
1236
- });
1237
-
1238
- const todayDateHint = new Date().toISOString().substring(0, 10);
1239
-
1240
- expect(validateForm({})).toEqual({
1241
- birthdate: 'Required field',
1242
- });
1243
-
1244
- expect(validateForm({ birthdate: '2020-10-10' })).toBeUndefined();
1245
- expect(validateForm({ birthdate: '2020-13-10' })).toEqual({
1246
- birthdate: `Must be a valid date in yyyy-mm-dd format. e.g. ${todayDateHint}`,
1247
- });
1248
- });
1249
-
1250
- describe('support "date" field type', () => {
1251
- it('support "date" field type with a minDate', () => {
1252
- const { fields, handleValidation } = createHeadlessForm(schemaInputTypeDate);
1253
-
1254
- const validateForm = (vals) => friendlyError(handleValidation(vals));
1255
-
1256
- expect(fields[0]).toMatchObject({
1257
- label: 'Birthdate',
1258
- name: 'birthdate',
1259
- required: true,
1260
- schema: expect.any(Object),
1261
- type: 'date',
1262
- minDate: '1922-03-01',
1263
- maxDate: '2022-03-17',
1264
- });
1265
-
1266
- expect(validateForm({})).toEqual({
1267
- birthdate: 'Required field',
1268
- });
1269
-
1270
- expect(validateForm({ birthdate: '' })).toEqual({
1271
- birthdate: `Required field`,
1272
- });
1273
-
1274
- expect(validateForm({ birthdate: '1922-02-01' })).toEqual({
1275
- birthdate: 'The date must be 1922-03-01 or after.',
1276
- });
1277
-
1278
- expect(validateForm({ birthdate: '1922-03-01' })).toBeUndefined();
1279
-
1280
- expect(validateForm({ birthdate: '2021-03-01' })).toBeUndefined();
1281
- });
1282
-
1283
- it('support "date" field type with a maxDate', () => {
1284
- const { fields, handleValidation } = createHeadlessForm(schemaInputTypeDate);
1285
-
1286
- const validateForm = (vals) => friendlyError(handleValidation(vals));
1287
-
1288
- expect(fields[0]).toMatchObject({
1289
- label: 'Birthdate',
1290
- name: 'birthdate',
1291
- required: true,
1292
- schema: expect.any(Object),
1293
- type: 'date',
1294
- minDate: '1922-03-01',
1295
- maxDate: '2022-03-17',
1296
- });
1297
-
1298
- expect(validateForm({ birthdate: '' })).toEqual({
1299
- birthdate: `Required field`,
1300
- });
1301
-
1302
- expect(validateForm({ birthdate: '2022-02-01' })).toBeUndefined();
1303
- expect(validateForm({ birthdate: '2022-03-01' })).toBeUndefined();
1304
- expect(validateForm({ birthdate: '2022-04-01' })).toEqual({
1305
- birthdate: 'The date must be 2022-03-17 or before.',
1306
- });
1307
- });
1308
-
1309
- it('support format date with minDate and maxDate', () => {
1310
- const schemaFormatDate = {
1311
- properties: {
1312
- birthdate: {
1313
- title: 'Birthdate',
1314
- type: 'string',
1315
- format: 'date',
1316
- 'x-jsf-presentation': {
1317
- inputType: 'myDateType',
1318
- maxDate: '2022-03-01',
1319
- minDate: '1922-03-01',
1320
- },
1321
- },
1322
- },
1323
- };
1324
-
1325
- const { handleValidation } = createHeadlessForm(schemaFormatDate);
1326
- const validateForm = (vals) => friendlyError(handleValidation(vals));
1327
-
1328
- expect(validateForm({ birthdate: '1922-02-01' })).toEqual({
1329
- birthdate: 'The date must be 1922-03-01 or after.',
1330
- });
1331
- });
1332
- });
1333
-
1334
- describe('supports "file" field type', () => {
1335
- it('supports "file" field type', () => {
1336
- const result = createHeadlessForm(
1337
- JSONSchemaBuilder()
1338
- .addInput({
1339
- fileInput: mockFileInput,
1340
- })
1341
- .build()
1342
- );
1343
-
1344
- expect(result).toMatchObject({
1345
- fields: [
1346
- {
1347
- type: 'file',
1348
- fileDownload: 'http://some.domain.com/file-name.pdf',
1349
- description: 'File Input Description',
1350
- fileName: 'My File',
1351
- label: 'File Input',
1352
- name: 'fileInput',
1353
- required: false,
1354
- accept: '.png,.jpg,.jpeg,.pdf',
1355
- },
1356
- ],
1357
- });
1358
- });
1359
-
1360
- describe('when a field has accepted extensions', () => {
1361
- let fields;
1362
- beforeEach(() => {
1363
- const result = createHeadlessForm(
1364
- JSONSchemaBuilder().addInput({ fileInput: mockFileInput }).build()
1365
- );
1366
- fields = result.fields;
1367
- });
1368
-
1369
- describe('and file is of incorrect format', () => {
1370
- const file = new File(['foo'], 'file.txt', {
1371
- type: 'text/plain',
1372
- });
1373
-
1374
- it('should throw an error', async () =>
1375
- expect(
1376
- object()
1377
- .shape({
1378
- fileInput: fields[0].schema,
1379
- })
1380
- .validate({ fileInput: [file] })
1381
- ).rejects.toMatchObject({
1382
- errors: ['Unsupported file format. The acceptable formats are .png,.jpg,.jpeg,.pdf.'],
1383
- }));
1384
- });
1385
-
1386
- describe('and file is of correct format', () => {
1387
- const file = new File(['foo'], 'file.png', {
1388
- type: 'image/png',
1389
- });
1390
- Object.defineProperty(file, 'size', { value: 1024 * 1024 });
1391
-
1392
- const assertObj = { fileInput: [file] };
1393
- it('should validate field', async () =>
1394
- expect(
1395
- object()
1396
- .shape({
1397
- fileInput: fields[0].schema,
1398
- })
1399
- .validate({ fileInput: [file] })
1400
- ).resolves.toEqual(assertObj));
1401
- });
1402
-
1403
- describe('and file is of correct but uppercase format ', () => {
1404
- const file = new File(['foo'], 'file.PNG', {
1405
- type: 'image/png',
1406
- });
1407
- Object.defineProperty(file, 'size', { value: 1024 * 1024 });
1408
-
1409
- const assertObj = { fileInput: [file] };
1410
- it('should validate field', async () =>
1411
- expect(
1412
- object()
1413
- .shape({
1414
- fileInput: fields[0].schema,
1415
- })
1416
- .validate({ fileInput: [file] })
1417
- ).resolves.toEqual(assertObj));
1418
- });
1419
-
1420
- describe('and file is not instance of a File', () => {
1421
- it('accepts if file object has name property', async () => {
1422
- expect(
1423
- object()
1424
- .shape({
1425
- fileInput: fields[0].schema,
1426
- })
1427
- .validate({ fileInput: [{ name: 'foo.pdf' }] })
1428
- ).resolves.toEqual({ fileInput: [{ name: 'foo.pdf' }] });
1429
- });
1430
-
1431
- it('should validate format', async () =>
1432
- expect(
1433
- object()
1434
- .shape({
1435
- fileInput: fields[0].schema,
1436
- })
1437
- .validate({ fileInput: [{ name: 'foo.txt' }] })
1438
- ).rejects.toMatchObject({
1439
- errors: ['Unsupported file format. The acceptable formats are .png,.jpg,.jpeg,.pdf.'],
1440
- }));
1441
-
1442
- it('should validate max size', async () =>
1443
- expect(
1444
- object()
1445
- .shape({
1446
- fileInput: fields[0].schema,
1447
- })
1448
- .validate({ fileInput: [{ name: 'foo.txt', size: 1024 * 1024 * 1024 }] })
1449
- ).rejects.toMatchObject({ errors: ['File size too large. The limit is 20 MB.'] }));
1450
-
1451
- it('throw an error if invalid file object', async () =>
1452
- expect(
1453
- object()
1454
- .shape({
1455
- fileInput: fields[0].schema,
1456
- })
1457
- .validate({ fileInput: [{ path: 'foo.txt' }] })
1458
- ).rejects.toMatchObject({ errors: ['Not a valid file.'] }));
1459
- });
1460
- });
1461
-
1462
- describe('when a field has max file size', () => {
1463
- let fields;
1464
- beforeEach(() => {
1465
- const result = createHeadlessForm(
1466
- JSONSchemaBuilder().addInput({ fileInput: mockFileInput }).build()
1467
- );
1468
- fields = result.fields;
1469
- });
1470
- describe('and file is greater than that', () => {
1471
- const file = new File([''], 'file.png');
1472
- Object.defineProperty(file, 'size', { value: 1024 * 1024 * 1024 });
1473
-
1474
- it('should throw an error', async () =>
1475
- expect(
1476
- object()
1477
- .shape({
1478
- fileInput: fields[0].schema,
1479
- })
1480
- .validate({ fileInput: [file] })
1481
- ).rejects.toMatchObject({ errors: ['File size too large. The limit is 20 MB.'] }));
1482
- });
1483
- describe('and file is smaller than that', () => {
1484
- const file = new File([''], 'file.png');
1485
- Object.defineProperty(file, 'size', { value: 1024 * 1024 });
1486
-
1487
- const assertObj = { fileInput: [file] };
1488
- it('should validate field', async () =>
1489
- expect(
1490
- object()
1491
- .shape({
1492
- fileInput: fields[0].schema,
1493
- })
1494
- .validate({ fileInput: [file] })
1495
- ).resolves.toEqual(assertObj));
1496
- });
1497
- });
1498
-
1499
- describe('when a field file is optional', () => {
1500
- it('it accepts an empty array', () => {
1501
- const result = createHeadlessForm(
1502
- JSONSchemaBuilder().addInput({ fileInput: mockFileInput }).build()
1503
- );
1504
- const emptyFile = { fileInput: [] };
1505
- expect(
1506
- object()
1507
- .shape({
1508
- fileInput: result.fields[0].schema,
1509
- })
1510
- .validate(emptyFile)
1511
- ).resolves.toEqual(emptyFile);
1512
- });
1513
-
1514
- it('it validates missing file correctly', () => {
1515
- const { handleValidation } = createHeadlessForm(
1516
- JSONSchemaBuilder().addInput({ fileInput: mockFileInput }).build()
1517
- );
1518
- const validateForm = (vals) => friendlyError(handleValidation(vals));
1519
-
1520
- expect(validateForm({})).toBeUndefined();
1521
- expect(validateForm({ fileInput: null })).toBeUndefined();
1522
- });
1523
- });
1524
-
1525
- describe('when a field file is required', () => {
1526
- it('it validates missing file correctly', () => {
1527
- const { handleValidation } = createHeadlessForm(schemaInputTypeFile);
1528
- const validateForm = (vals) => friendlyError(handleValidation(vals));
1529
-
1530
- expect(validateForm({})).toEqual({
1531
- a_file: 'Required field',
1532
- });
1533
-
1534
- expect(
1535
- validateForm({
1536
- a_file: null,
1537
- })
1538
- ).toEqual({
1539
- a_file: 'Required field',
1540
- });
1541
- });
1542
- });
1543
- });
1544
-
1545
- describe('supports "group-array" field type', () => {
1546
- it('basic test', () => {
1547
- const result = createHeadlessForm(
1548
- JSONSchemaBuilder()
1549
- .addInput({
1550
- dependent_details: mockGroupArrayInput,
1551
- })
1552
- .build()
1553
- );
1554
-
1555
- expect(result).toMatchObject({
1556
- fields: [
1557
- {
1558
- type: 'group-array',
1559
- description: 'Add the dependents you claim below',
1560
- label: 'Child details',
1561
- name: 'dependent_details',
1562
- required: false,
1563
- fields: expect.any(Function),
1564
- addFieldText: 'Add new field',
1565
- },
1566
- ],
1567
- });
1568
-
1569
- // Validations
1570
- const fieldValidator = result.fields[0].schema;
1571
- // nthfields are required
1572
- expect(
1573
- fieldValidator.isValidSync([
1574
- {
1575
- birthdate: '',
1576
- full_name: '',
1577
- sex: '',
1578
- },
1579
- ])
1580
- ).toBe(false);
1581
- // date is invalid
1582
- expect(
1583
- fieldValidator.isValidSync([
1584
- {
1585
- birthdate: 'invalidate date',
1586
- full_name: 'John Doe',
1587
- sex: 'male',
1588
- },
1589
- ])
1590
- ).toBe(false);
1591
- // all good
1592
- expect(
1593
- fieldValidator.isValidSync([
1594
- {
1595
- birthdate: '2021-12-04',
1596
- full_name: 'John Doe',
1597
- sex: 'male',
1598
- },
1599
- ])
1600
- ).toBe(true);
1601
-
1602
- const nestedFieldsFromResult = result.fields[0].fields();
1603
- expect(nestedFieldsFromResult).toMatchObject([
1604
- {
1605
- type: 'text',
1606
- description: 'Enter your child’s full name',
1607
- maxLength: 255,
1608
- label: 'Child Full Name',
1609
- name: 'full_name',
1610
- required: true,
1611
- },
1612
- {
1613
- type: 'date',
1614
- name: 'birthdate',
1615
- label: 'Child Birthdate',
1616
- required: true,
1617
- description: 'Enter your child’s date of birth',
1618
- maxLength: 255,
1619
- },
1620
- {
1621
- type: 'radio',
1622
- name: 'sex',
1623
- label: 'Child Sex',
1624
- options: [
1625
- {
1626
- label: 'Male',
1627
- value: 'male',
1628
- },
1629
- {
1630
- label: 'Female',
1631
- value: 'female',
1632
- },
1633
- ],
1634
- required: true,
1635
- description:
1636
- 'We know sex is non-binary but for insurance and payroll purposes, we need to collect this information.',
1637
- },
1638
- ]);
1639
- });
1640
-
1641
- it('nested fields (native, core and custom) has correct validations', () => {
1642
- const { handleValidation } = createHeadlessForm({
1643
- properties: {
1644
- break_schedule: {
1645
- title: 'Work schedule',
1646
- type: 'array',
1647
- presentation: {
1648
- inputType: 'group-array',
1649
- },
1650
- items: {
1651
- properties: {
1652
- minutes_native: {
1653
- title: 'Minutes of break (native)',
1654
- type: 'integer',
1655
- minimum: 60,
1656
- // without presentation.inputType
1657
- },
1658
- minutes_core: {
1659
- title: 'Minutes of break (core)',
1660
- type: 'integer',
1661
- minimum: 60,
1662
- presentation: {
1663
- inputType: 'number', // a core inputType
1664
- },
1665
- },
1666
- minutes_custom: {
1667
- title: 'Minutes of break (custom)',
1668
- type: 'integer',
1669
- minimum: 60,
1670
- presentation: {
1671
- inputType: 'hour', // a custom inputType
1672
- },
1673
- },
1674
- },
1675
- required: ['weekday', 'minutes_native', 'minutes_core', 'minutes_custom'],
1676
- },
1677
- },
1678
- },
1679
- required: ['break_schedule'],
1680
- });
1681
- const validateForm = (vals) => friendlyError(handleValidation(vals));
1682
-
1683
- // Given empty, it says it's required
1684
- expect(validateForm({})).toEqual({
1685
- break_schedule: 'Required field',
1686
- });
1687
-
1688
- // Given empty fields, it mentions nested required fields
1689
- expect(
1690
- validateForm({
1691
- break_schedule: [{}],
1692
- })
1693
- ).toEqual({
1694
- break_schedule: [
1695
- {
1696
- minutes_native: 'Required field',
1697
- minutes_core: 'Required field',
1698
- minutes_custom: 'Required field',
1699
- },
1700
- ],
1701
- });
1702
-
1703
- // Given correct values, it's all valid.
1704
- expect(
1705
- validateForm({
1706
- break_schedule: [
1707
- {
1708
- minutes_native: 60,
1709
- minutes_core: 60,
1710
- minutes_custom: 60,
1711
- },
1712
- ],
1713
- })
1714
- ).toBeUndefined();
1715
-
1716
- // Given invalid values, the validation is triggered.
1717
- expect(
1718
- validateForm({
1719
- break_schedule: [
1720
- {
1721
- minutes_native: 50,
1722
- minutes_core: 50,
1723
- minutes_custom: 50,
1724
- },
1725
- ],
1726
- })
1727
- ).toEqual({
1728
- break_schedule: [
1729
- {
1730
- minutes_core: 'Must be greater or equal to 60',
1731
- minutes_native: 'Must be greater or equal to 60',
1732
- minutes_custom: 'Must be greater or equal to 60',
1733
- },
1734
- ],
1735
- });
1736
- });
1737
-
1738
- it('nested "group-array" fields', () => {
1739
- const result = createHeadlessForm({
1740
- properties: {
1741
- nestedGroupArray: nestedGroupArrayForm,
1742
- },
1743
- });
1744
-
1745
- expect(result.fields[0]).toMatchObject({
1746
- label: 'Parent object',
1747
- name: 'nestedGroupArray',
1748
- required: false,
1749
- type: 'group-array',
1750
- inputType: 'group-array',
1751
- jsonType: 'array',
1752
- fields: expect.any(Function),
1753
- });
1754
-
1755
- expect(result.fields[0].fields()).toMatchObject([
1756
- {
1757
- type: 'text',
1758
- description: 'Simple text field',
1759
- maxLength: 255,
1760
- label: 'Outer Field',
1761
- name: 'notNested',
1762
- required: true,
1763
- },
1764
- {
1765
- label: 'Nested group-array',
1766
- name: 'nested',
1767
- required: true,
1768
- type: 'group-array',
1769
- inputType: 'group-array',
1770
- jsonType: 'array',
1771
- fields: expect.any(Function),
1772
- },
1773
- ]);
1774
-
1775
- expect(result.fields[0].fields()[1].fields()).toMatchObject([
1776
- {
1777
- type: 'text',
1778
- description: 'First nested text field',
1779
- maxLength: 255,
1780
- label: 'Inner Field 1',
1781
- name: 'nestedField1',
1782
- required: true,
1783
- },
1784
- {
1785
- type: 'text',
1786
- description: 'Second nested text field',
1787
- maxLength: 255,
1788
- label: 'Inner Field 2',
1789
- name: 'nestedField2',
1790
- required: true,
1791
- },
1792
- ]);
1793
- });
1794
-
1795
- it('can pass custom field attributes', () => {
1796
- const result = createHeadlessForm(
1797
- {
1798
- properties: {
1799
- children_basic: mockGroupArrayInput,
1800
- children_custom: mockGroupArrayInput,
1801
- },
1802
- },
1803
- {
1804
- customProperties: {
1805
- children_custom: {
1806
- 'data-foo': 'baz',
1807
- },
1808
- },
1809
- }
1810
- );
1811
-
1812
- expect(result).toMatchObject({
1813
- fields: [
1814
- {
1815
- label: 'Child details',
1816
- name: 'children_basic',
1817
- required: false,
1818
- type: 'group-array',
1819
- inputType: 'group-array',
1820
- jsonType: 'array',
1821
- fields: expect.any(Function), // This is what makes the field work
1822
- },
1823
- {
1824
- label: 'Child details',
1825
- name: 'children_custom',
1826
- type: 'group-array',
1827
- inputType: 'group-array',
1828
- jsonType: 'array',
1829
- required: false,
1830
- 'data-foo': 'baz', // check that custom property is properly propagated
1831
- fields: expect.any(Function), // This is what makes the field work
1832
- },
1833
- ],
1834
- });
1835
- });
1836
-
1837
- it('can be a conditional field', () => {
1838
- const { fields, handleValidation } = createHeadlessForm({
1839
- properties: {
1840
- yes_or_no: {
1841
- title: 'Show the dependents or not?',
1842
- oneOf: [{ const: 'yes' }, { const: 'no' }],
1843
- 'x-jsf-presentation': { inputType: 'radio' },
1844
- },
1845
- dependent_details: mockGroupArrayInput,
1846
- },
1847
- allOf: [
1848
- {
1849
- if: {
1850
- properties: {
1851
- yes_or_no: { const: 'yes' },
1852
- },
1853
- required: ['yes_or_no'],
1854
- },
1855
- then: {
1856
- required: ['dependent_details'],
1857
- },
1858
- else: {
1859
- properties: {
1860
- dependent_details: false,
1861
- },
1862
- },
1863
- },
1864
- ],
1865
- });
1866
-
1867
- // By default is hidden but the fields are accessible
1868
- expect(getField(fields, 'dependent_details').isVisible).toBe(false);
1869
- expect(getField(fields, 'dependent_details').fields).toEqual(expect.any(Function));
1870
-
1871
- // When the condition matches...
1872
- const { formErrors } = handleValidation({ yes_or_no: 'yes' });
1873
- expect(formErrors).toEqual({
1874
- dependent_details: 'Required field',
1875
- });
1876
- // it gets visible with its inner fields.
1877
- expect(getField(fields, 'dependent_details').isVisible).toBe(true);
1878
- expect(getField(fields, 'dependent_details').fields).toEqual(expect.any(Function));
1879
- });
1880
- });
1881
-
1882
- it('supports "null" field type', () => {
1883
- const { handleValidation, fields } = createHeadlessForm(schemaInputTypeNull, {
1884
- strictInputType: false,
1885
- });
1886
-
1887
- expect(fields).toMatchObject([
1888
- {
1889
- name: 'name',
1890
- label: '(Optional) Name',
1891
- type: undefined,
1892
- jsonType: 'null',
1893
- schema: expect.any(Object),
1894
- },
1895
- {
1896
- name: 'username',
1897
- label: 'Username',
1898
- type: 'text',
1899
- jsonType: 'string',
1900
- inputType: 'text',
1901
- maxLength: 4,
1902
- schema: expect.any(Object),
1903
- },
1904
- ]);
1905
-
1906
- // jsonType `null` fields do not have a corresponding inputType
1907
- expect(fields[0].inputType).toBeUndefined();
1908
-
1909
- const validateForm = (vals) => friendlyError(handleValidation(vals));
1910
-
1911
- expect(validateForm({})).toEqual({
1912
- username: 'Required field',
1913
- });
1914
-
1915
- expect(validateForm({ username: 'hello', name: 'John' })).toEqual({
1916
- username: 'Please insert up to 4 characters',
1917
- name: 'The value "John" is not valid.',
1918
- });
1919
-
1920
- expect(validateForm({ username: 'john' })).toBeUndefined();
1921
- expect(validateForm({ username: 'john', name: null })).toBeUndefined();
1922
- });
1923
-
1924
- describe('supports "fieldset" field type', () => {
1925
- it('supports basic case', () => {
1926
- const result = createHeadlessForm({
1927
- properties: {
1928
- fieldset: mockFieldset,
1929
- },
1930
- });
1931
-
1932
- expect(result).toMatchObject({
1933
- fields: [
1934
- {
1935
- description: 'Fieldset description',
1936
- label: 'Fieldset title',
1937
- name: 'fieldset',
1938
- type: 'fieldset',
1939
- required: false,
1940
- fields: [
1941
- {
1942
- description: 'Your username (max 10 characters)',
1943
- label: 'Username',
1944
- name: 'username',
1945
- type: 'text',
1946
- required: true,
1947
- },
1948
- {
1949
- description: 'How many open tabs do you have?',
1950
- label: 'Tabs',
1951
- maximum: 10,
1952
- minimum: 1,
1953
- name: 'tabs',
1954
- type: 'number',
1955
- required: false,
1956
- },
1957
- ],
1958
- },
1959
- ],
1960
- });
1961
- });
1962
-
1963
- it('supports nested fieldset (fieldset inside fieldset)', () => {
1964
- const result = createHeadlessForm(
1965
- JSONSchemaBuilder()
1966
- .addInput({
1967
- nestedFieldset: mockNestedFieldset,
1968
- })
1969
- .build()
1970
- );
1971
-
1972
- expect(result).toMatchObject({
1973
- fields: [
1974
- {
1975
- label: 'Nested fieldset title',
1976
- description: 'Nested fieldset description',
1977
- name: 'nestedFieldset',
1978
- type: 'fieldset',
1979
- required: false,
1980
- fields: [
1981
- {
1982
- description: 'Fieldset description',
1983
- label: 'Fieldset title',
1984
- name: 'innerFieldset',
1985
- type: 'fieldset',
1986
- required: false,
1987
- fields: [
1988
- {
1989
- description: 'Your username (max 10 characters)',
1990
- label: 'Username',
1991
- name: 'username',
1992
- type: 'text',
1993
- required: true,
1994
- },
1995
- {
1996
- description: 'How many open tabs do you have?',
1997
- label: 'Tabs',
1998
- maximum: 10,
1999
- minimum: 1,
2000
- name: 'tabs',
2001
- type: 'number',
2002
- required: false,
2003
- },
2004
- ],
2005
- },
2006
- ],
2007
- },
2008
- ],
2009
- });
2010
- });
2011
-
2012
- it('supported "fieldset" with scoped conditionals', () => {
2013
- const { handleValidation } = createHeadlessForm(schemaFieldsetScopedCondition, {});
2014
- const validateForm = (vals) => friendlyError(handleValidation(vals));
2015
-
2016
- // The "child.has_child" is required
2017
- expect(validateForm({})).toEqual({
2018
- child: {
2019
- has_child: 'Required field',
2020
- },
2021
- });
2022
-
2023
- // The "child.no" is valid
2024
- expect(
2025
- validateForm({
2026
- child: {
2027
- has_child: 'no',
2028
- },
2029
- })
2030
- ).toBeUndefined();
2031
-
2032
- // Invalid because it expect child.age too
2033
- expect(
2034
- validateForm({
2035
- child: {
2036
- has_child: 'yes',
2037
- },
2038
- })
2039
- ).toEqual({
2040
- child: {
2041
- age: 'Required field',
2042
- },
2043
- });
2044
-
2045
- // Valid without optional child.passport_id
2046
- expect(
2047
- validateForm({
2048
- child: {
2049
- has_child: 'yes',
2050
- age: 15,
2051
- },
2052
- })
2053
- ).toBeUndefined();
2054
-
2055
- // Valid with optional child.passport_id
2056
- expect(
2057
- validateForm({
2058
- child: {
2059
- has_child: 'yes',
2060
- age: 15,
2061
- passport_id: 'asdf',
2062
- },
2063
- })
2064
- ).toBeUndefined();
2065
- });
2066
-
2067
- it('should set any nested "fieldset" form values to null when they are invisible', async () => {
2068
- const { handleValidation } = createHeadlessForm(schemaFieldsetScopedCondition, {});
2069
- const validateForm = (vals) => friendlyError(handleValidation(vals));
2070
-
2071
- const formValues = {
2072
- child: {
2073
- has_child: 'yes',
2074
- age: 15,
2075
- },
2076
- };
2077
-
2078
- await expect(validateForm(formValues)).toBeUndefined();
2079
- expect(formValues.child.age).toBe(15);
2080
-
2081
- formValues.child.has_child = 'no';
2082
- // form value updates re-validate; see computeYupSchema()
2083
- await expect(validateForm(formValues)).toBeUndefined();
2084
-
2085
- // when child.has_child is 'no' child.age is invisible
2086
- expect(formValues.child.age).toBe(null);
2087
- });
2088
-
2089
- describe('supports conditionals to fieldsets', () => {
2090
- // To not mix the concepts:
2091
- // - Scoped conditionals: Conditionals written inside a fieldset
2092
- // - Conditionals to fieldsets: Root conditionals that affect a fieldset
2093
-
2094
- // This describe has sequential tests, covering the following:
2095
- // If the working_hours > 30,
2096
- // Then the fieldset perks.food changes (the "no" option gets removed)
2097
-
2098
- // Setup (arrange)
2099
- let validateForm;
2100
- let fields;
2101
- let originalFood;
2102
- let perksForLowWorkHours;
2103
-
2104
- beforeAll(() => {
2105
- const form = createHeadlessForm(schemaWithConditionalToFieldset);
2106
- fields = form.fields;
2107
- validateForm = (vals) => friendlyError(form.handleValidation(vals));
2108
- originalFood = getField(fields, 'perks', 'food');
2109
-
2110
- perksForLowWorkHours = {
2111
- food: 'no', // this option will be removed when the condition happens.
2112
- retirement: 'basic',
2113
- };
2114
- });
2115
-
2116
- it('by default, the Perks.food has 4 options', () => {
2117
- expect(originalFood.options).toHaveLength(4);
2118
- expect(originalFood.description).toBeUndefined();
2119
-
2120
- // Ensure the perks are required
2121
- expect(validateForm({})).toEqual({
2122
- work_hours_per_week: 'Required field',
2123
- perks: {
2124
- food: 'Required field',
2125
- retirement: 'Required field',
2126
- },
2127
- });
2128
-
2129
- // Given low work hours, the form is valid.
2130
- expect(
2131
- validateForm({
2132
- work_hours_per_week: 5,
2133
- perks: perksForLowWorkHours,
2134
- })
2135
- ).toBeUndefined();
2136
- });
2137
-
2138
- it('Given a lot work hours, the perks.food options change', () => {
2139
- expect(
2140
- validateForm({
2141
- work_hours_per_week: 35,
2142
- })
2143
- ).toEqual({
2144
- pto: 'Required field', // Sanity-check - this field gets required too.
2145
- perks: {
2146
- food: 'Required field',
2147
- retirement: 'Required field',
2148
- },
2149
- });
2150
-
2151
- // The fieldset changed!
2152
- const foodField = getField(fields, 'perks', 'food');
2153
- // perks.food options changed ("No" was removed)
2154
- expect(foodField.options).toHaveLength(3);
2155
-
2156
- // Ensure the "no" option is no longer accepted:
2157
- // This is a very important test in case the UI fails for some reason.
2158
- expect(
2159
- validateForm({
2160
- work_hours_per_week: 35,
2161
- pto: 20,
2162
- perks: perksForLowWorkHours,
2163
- })
2164
- ).toEqual({
2165
- perks: {
2166
- food: 'The option "no" is not valid.',
2167
- },
2168
- });
2169
-
2170
- // perks.food has a new description
2171
- expect(foodField.description).toBe("Above 30 hours, the 'no' option disappears.");
2172
- // pto has a new description
2173
- expect(getField(fields, 'pto').description).toBe(
2174
- 'Above 30 hours, the PTO needs to be at least 20 days.'
2175
- );
2176
-
2177
- // Sanity-check: Now the PTO also has a minimum value
2178
- expect(
2179
- validateForm({
2180
- work_hours_per_week: 35,
2181
- pto: 5, // too low
2182
- perks: { food: 'lunch', retirement: 'basic' },
2183
- })
2184
- ).toEqual({
2185
- pto: 'Must be greater or equal to 20',
2186
- });
2187
- });
2188
-
2189
- it('When changing back to low work hours, the perks.food goes back to the original state', () => {
2190
- expect(
2191
- validateForm({
2192
- work_hours_per_week: 10,
2193
- pto: 5,
2194
- })
2195
- ).toEqual({
2196
- perks: {
2197
- food: 'Required field',
2198
- retirement: 'Required field',
2199
- },
2200
- // ...pto is minimum error is gone! (sanity-check)
2201
- });
2202
-
2203
- const foodField = getField(fields, 'perks', 'food');
2204
- // ...Number of perks.food options was back to the original (4)
2205
- expect(foodField.options).toHaveLength(4);
2206
- // ...Food description was back to the original
2207
- expect(foodField.description).toBeUndefined();
2208
- // ...PTO Description is removed too.
2209
- expect(getField(fields, 'pto').description).toBeUndefined();
2210
-
2211
- // Given again "low perks", the form valid.
2212
- expect(
2213
- validateForm({
2214
- work_hours_per_week: 10,
2215
- perks: perksForLowWorkHours,
2216
- })
2217
- ).toBeUndefined();
2218
- });
2219
- });
2220
- });
2221
-
2222
- it('support "email" field type', () => {
2223
- const result = createHeadlessForm(schemaInputTypeEmail);
2224
-
2225
- expect(result).toMatchObject({
2226
- fields: [
2227
- {
2228
- description: 'Enter your email address',
2229
- label: 'Email address',
2230
- name: 'email_address',
2231
- required: true,
2232
- schema: expect.any(Object),
2233
- type: 'email',
2234
- maxLength: 255,
2235
- },
2236
- ],
2237
- });
2238
-
2239
- const fieldValidator = result.fields[0].schema;
2240
- expect(fieldValidator.isValidSync('test@gmail.com')).toBe(true);
2241
- expect(() => fieldValidator.validateSync('ffsdf')).toThrowError(
2242
- 'Please enter a valid email address'
2243
- );
2244
- expect(() => fieldValidator.validateSync(undefined)).toThrowError('Required field');
2245
- });
2246
-
2247
- describe('supports "checkbox" field type', () => {
2248
- describe('checkbox as string', () => {
2249
- it('required: only accept the value in "checkboxValue"', () => {
2250
- const result = createHeadlessForm(schemaInputTypeCheckbox);
2251
- const checkboxField = result.fields.find((field) => field.name === 'contract_duration');
2252
-
2253
- expect(checkboxField).toMatchObject({
2254
- description:
2255
- 'I acknowledge that all employees in France will be hired on indefinite contracts.',
2256
- label: 'Contract duration',
2257
- name: 'contract_duration',
2258
- type: 'checkbox',
2259
- checkboxValue: 'Permanent',
2260
- });
2261
- expect(checkboxField).not.toHaveProperty('default'); // ensure it's not checked by default.
2262
-
2263
- const fieldValidator = checkboxField.schema;
2264
- expect(fieldValidator.isValidSync('Permanent')).toBe(true);
2265
- expect(() => fieldValidator.validateSync(undefined)).toThrowError(
2266
- 'Please acknowledge this field'
2267
- );
2268
- });
2269
-
2270
- it('required checked: returns a default value', () => {
2271
- const result = createHeadlessForm(schemaInputTypeCheckbox);
2272
- const checkboxField = result.fields.find(
2273
- (field) => field.name === 'contract_duration_checked'
2274
- );
2275
-
2276
- expect(checkboxField).toMatchObject({
2277
- default: 'Permanent',
2278
- checkboxValue: 'Permanent',
2279
- });
2280
- });
2281
- });
2282
-
2283
- describe('checkbox as boolean', () => {
2284
- it('optional: Accepts true or false', () => {
2285
- const result = createHeadlessForm(schemaInputTypeCheckboxBooleans);
2286
- const checkboxField = result.fields.find((field) => field.name === 'boolean_empty');
2287
-
2288
- expect(checkboxField).toMatchObject({
2289
- checkboxValue: true,
2290
- });
2291
- expect(checkboxField).not.toHaveProperty('default'); // ensure it's not checked by default.
2292
-
2293
- const fieldValidator = checkboxField.schema;
2294
- expect(fieldValidator.isValidSync(true)).toBe(true);
2295
- expect(fieldValidator.isValidSync(false)).toBe(true);
2296
- expect(fieldValidator.isValidSync(undefined)).toBe(true);
2297
- expect(() => fieldValidator.validateSync('foo')).toThrowError(
2298
- 'The value must be a boolean, but received "foo"'
2299
- );
2300
- });
2301
-
2302
- it('required: Only accepts true', () => {
2303
- const result = createHeadlessForm(schemaInputTypeCheckboxBooleans);
2304
- const checkboxField = result.fields.find((field) => field.name === 'boolean_required');
2305
-
2306
- expect(checkboxField).toMatchObject({
2307
- checkboxValue: true,
2308
- });
2309
-
2310
- const fieldValidator = checkboxField.schema;
2311
- expect(fieldValidator.isValidSync(true)).toBe(true);
2312
- expect(() => fieldValidator.validateSync(false)).toThrowError(
2313
- 'Please acknowledge this field'
2314
- );
2315
- });
2316
-
2317
- it('checked: returns default: true', () => {
2318
- const result = createHeadlessForm(schemaInputTypeCheckboxBooleans);
2319
- const checkboxField = result.fields.find((field) => field.name === 'boolean_checked');
2320
-
2321
- expect(checkboxField).toMatchObject({
2322
- checkboxValue: true,
2323
- default: true,
2324
- });
2325
- });
2326
-
2327
- it('conditional: it works as undefined value', () => {
2328
- const { fields, handleValidation } = createHeadlessForm(
2329
- schemaInputTypeCheckboxBooleanConditional
2330
- );
2331
- const checkboxField = fields.find((field) => field.name === 'pet_is_cat');
2332
-
2333
- expect(handleValidation({ has_pet: false }).formErrors).toBeUndefined();
2334
- expect(checkboxField.isVisible).toBe(false);
2335
-
2336
- expect(handleValidation({ has_pet: true, pet_is_cat: true }).formErrors).toBeUndefined();
2337
- expect(checkboxField.isVisible).toBe(true);
2338
-
2339
- expect(handleValidation({ has_pet: true, pet_is_cat: 'foo' }).formErrors).toEqual({
2340
- pet_is_cat: 'The value must be a boolean, but received "foo"',
2341
- });
2342
-
2343
- // Bug: It should throw an error saying pet_is_cat is not allowed, but it doesn't.
2344
- // Explained at "Given values from hidden fields, it does not thrown an error"
2345
- expect(handleValidation({ has_pet: false, pet_is_cat: true }).formErrors).toBeUndefined();
2346
- });
2347
- });
2348
- });
2349
-
2350
- describe('supports custom inputType (eg "hour")', () => {
2351
- it('as required, optional, and mixed types', () => {
2352
- const { fields, handleValidation } = createHeadlessForm(
2353
- {
2354
- properties: {
2355
- start_time: {
2356
- title: 'Starting time',
2357
- type: 'string',
2358
- presentation: {
2359
- inputType: 'hour',
2360
- },
2361
- },
2362
- pause: {
2363
- title: 'Pause time (optional)',
2364
- type: 'string',
2365
- presentation: {
2366
- inputType: 'hour',
2367
- },
2368
- },
2369
- end_time: {
2370
- title: 'Finishing time (optional)',
2371
- type: ['null', 'string'], // ensure it supports mix types (array) (optional/null)
2372
- presentation: {
2373
- inputType: 'hour',
2374
- },
2375
- },
2376
- },
2377
- required: ['start_time'],
2378
- },
2379
- {
2380
- strictInputType: false,
2381
- }
2382
- );
2383
- const validateForm = (vals) => friendlyError(handleValidation(vals));
2384
-
2385
- const commonAttrs = {
2386
- type: 'hour',
2387
- inputType: 'hour',
2388
- jsonType: 'string',
2389
- schema: expect.any(Object),
2390
- };
2391
- expect(fields).toMatchObject([
2392
- {
2393
- name: 'start_time',
2394
- label: 'Starting time',
2395
- ...commonAttrs,
2396
- },
2397
- {
2398
- name: 'pause',
2399
- label: 'Pause time (optional)',
2400
- ...commonAttrs,
2401
- },
2402
- {
2403
- name: 'end_time',
2404
- label: 'Finishing time (optional)',
2405
- ...commonAttrs,
2406
- jsonType: ['null', 'string'],
2407
- },
2408
- ]);
2409
-
2410
- expect(validateForm({})).toEqual({
2411
- start_time: 'Required field',
2412
- });
2413
-
2414
- expect(validateForm({ start_time: '08:30' })).toBeUndefined();
2415
- });
2416
- });
2417
- });
2418
-
2419
- describe('validation options', () => {
2420
- it('given invalid values it returns both yupError and formErrors', () => {
2421
- const { handleValidation } = createHeadlessForm(schemaInputTypeText);
2422
-
2423
- const { formErrors, yupError } = handleValidation({});
2424
-
2425
- // Assert the yupError shape is really a YupError
2426
- expect(yupError).toEqual(expect.any(Error));
2427
- expect(yupError.inner[0].path).toBe('username');
2428
- expect(yupError.inner[0].message).toBe('Required field');
2429
-
2430
- // Assert the converted YupError to formErrors
2431
- expect(formErrors).toEqual({
2432
- username: 'Required field',
2433
- });
2434
- });
2435
- });
2436
-
2437
- it('supports oneOf number const', () => {
2438
- const result = createHeadlessForm({
2439
- type: 'object',
2440
- additionalProperties: false,
2441
- properties: {
2442
- pets: {
2443
- title: 'How many pets?',
2444
- oneOf: [
2445
- {
2446
- title: 'One',
2447
- const: 0,
2448
- },
2449
- {
2450
- title: 'Two',
2451
- const: 2,
2452
- },
2453
- {
2454
- title: 'null',
2455
- const: 1,
2456
- },
2457
- ],
2458
- 'x-jsf-presentation': {
2459
- inputType: 'select',
2460
- },
2461
- type: ['number', 'null'],
2462
- },
2463
- },
2464
- required: [],
2465
- 'x-jsf-order': ['pets'],
2466
- });
2467
-
2468
- const fieldValidator = result.fields[0].schema;
2469
-
2470
- expect(fieldValidator.isValidSync(0)).toBe(true);
2471
- expect(fieldValidator.isValidSync(1)).toBe(true);
2472
- expect(() => fieldValidator.validateSync('2')).toThrowError('The option "2" is not valid.');
2473
- expect(fieldValidator.isValidSync(null)).toBe(true);
2474
- });
2475
-
2476
- describe('property misc attributes', () => {
2477
- it('pass readOnly to field', () => {
2478
- const result = createHeadlessForm({
2479
- properties: {
2480
- secret: {
2481
- title: 'Secret code',
2482
- readOnly: true,
2483
- type: 'string',
2484
- presentation: {
2485
- inputType: 'text',
2486
- },
2487
- },
2488
- },
2489
- });
2490
-
2491
- expect(result).toMatchObject({
2492
- fields: [
2493
- {
2494
- name: 'secret',
2495
- label: 'Secret code',
2496
- schema: expect.any(Object),
2497
- readOnly: true,
2498
- },
2499
- ],
2500
- });
2501
- });
2502
-
2503
- it('pass "deprecated" attributes to field', () => {
2504
- const result = createHeadlessForm({
2505
- properties: {
2506
- secret: {
2507
- title: 'Age',
2508
- type: 'number',
2509
- deprecated: true,
2510
- presentation: {
2511
- inputType: 'number',
2512
- deprecated: {
2513
- description: 'Deprecated in favor of "birthdate".',
2514
- },
2515
- },
2516
- },
2517
- },
2518
- });
2519
-
2520
- expect(result).toMatchObject({
2521
- fields: [
2522
- {
2523
- type: 'number',
2524
- name: 'secret',
2525
- label: 'Age',
2526
- schema: expect.any(Object),
2527
- deprecated: {
2528
- description: 'Deprecated in favor of "birthdate".',
2529
- },
2530
- },
2531
- ],
2532
- });
2533
- });
2534
-
2535
- it('pass both root level "description" and "x-jsf-presentation.description"', () => {
2536
- const resultsWithRootDescription = createHeadlessForm({
2537
- properties: {
2538
- username: mockTextInput,
2539
- },
2540
- required: ['username'],
2541
- });
2542
-
2543
- expect(resultsWithRootDescription.fields[0].description).toMatch(/your username/i);
2544
-
2545
- const resultsWithPresentationDescription = createHeadlessForm({
2546
- properties: {
2547
- username: {
2548
- ...mockTextInput,
2549
- 'x-jsf-presentation': {
2550
- inputType: 'text',
2551
- // should override the root level description
2552
- description: 'a different description with <span>markup</span>',
2553
- },
2554
- },
2555
- },
2556
- required: ['username'],
2557
- });
2558
-
2559
- expect(resultsWithPresentationDescription.fields[0].description).toMatch(
2560
- /a different description /i
2561
- );
2562
- });
2563
-
2564
- it('pass both root level "description" and "presentation.description" (deprecated)', () => {
2565
- const resultsWithRootDescription = createHeadlessForm({
2566
- properties: {
2567
- username: mockTextInputDeprecated,
2568
- },
2569
- required: ['username'],
2570
- });
2571
-
2572
- expect(resultsWithRootDescription.fields[0].description).toMatch(/your username/i);
2573
-
2574
- const resultsWithPresentationDescription = createHeadlessForm(
2575
- JSONSchemaBuilder()
2576
- .addInput({
2577
- username: {
2578
- ...mockTextInputDeprecated,
2579
- presentation: {
2580
- inputType: 'text',
2581
- maskSecret: 2,
2582
- // should override the root level description
2583
- description: 'a different description with <span>markup</span>',
2584
- },
2585
- },
2586
- })
2587
- .setRequiredFields(['username'])
2588
- .build()
2589
- );
2590
-
2591
- expect(resultsWithPresentationDescription.fields[0].description).toMatch(
2592
- /a different description /i
2593
- );
2594
- });
2595
-
2596
- it('support field with "x-jsf-presentation.statement"', () => {
2597
- const result = createHeadlessForm(schemaInputWithStatement);
2598
-
2599
- expect(result).toMatchObject({
2600
- fields: [
2601
- {
2602
- name: 'bonus',
2603
- label: 'Bonus',
2604
- type: 'text',
2605
- statement: {
2606
- description: 'This is a custom statement message.',
2607
- inputType: 'statement',
2608
- severity: 'info',
2609
- },
2610
- },
2611
- {
2612
- name: 'role',
2613
- label: 'Role',
2614
- type: 'text',
2615
- statement: {
2616
- description: 'This is another statement message, but more severe.',
2617
- inputType: 'statement',
2618
- severity: 'warning',
2619
- },
2620
- },
2621
- ],
2622
- });
2623
- });
2624
-
2625
- it('pass custom attributes as function', () => {
2626
- function FakeComponent(props) {
2627
- const { label, description } = props;
2628
- return `A React component with ${label} and ${description}`;
2629
- }
2630
- // Any custom attributes must be inside "x-jsf-presentation"
2631
- const { fields, handleValidation } = createHeadlessForm({
2632
- properties: {
2633
- field_a: {
2634
- title: 'Field A',
2635
- 'x-jsf-presentation': {
2636
- inputType: 'text',
2637
- MyComponent: FakeComponent,
2638
- },
2639
- },
2640
- field_b: {
2641
- title: 'Field B',
2642
- 'x-jsf-presentation': {
2643
- inputType: 'text',
2644
- MyComponent: FakeComponent,
2645
- },
2646
- },
2647
- },
2648
- allOf: [
2649
- {
2650
- if: {
2651
- properties: {
2652
- field_a: { const: 'yes' },
2653
- },
2654
- required: ['field_a'],
2655
- },
2656
- then: {
2657
- required: ['field_b'],
2658
- },
2659
- },
2660
- ],
2661
- });
2662
-
2663
- const fieldA = getField(fields, 'field_a');
2664
- expect(fieldA).toMatchObject({
2665
- label: 'Field A',
2666
- MyComponent: expect.any(Function),
2667
- });
2668
-
2669
- const fieldB = getField(fields, 'field_b');
2670
- expect(fieldB).toMatchObject({
2671
- label: 'Field B',
2672
- required: false,
2673
- MyComponent: expect.any(Function),
2674
- });
2675
-
2676
- const fakeProps = { label: 'Field B', description: 'fake description' };
2677
- expect(fieldB.MyComponent(fakeProps)).toBe(
2678
- 'A React component with Field B and fake description'
2679
- );
2680
-
2681
- // Ensure "MyComponent" attribute still exsits after a validation cycle.
2682
- // This covers the updateField(). Check PR for more context.
2683
- handleValidation({ field_a: 'yes' });
2684
-
2685
- expect(getField(fields, 'field_a')).toMatchObject({
2686
- MyComponent: expect.any(Function),
2687
- });
2688
- expect(getField(fields, 'field_b')).toMatchObject({
2689
- required: true,
2690
- MyComponent: expect.any(Function),
2691
- });
2692
- });
2693
-
2694
- it('pass scopedJsonSchema to each field', () => {
2695
- const { fields } = createHeadlessForm(schemaWithoutInputTypes, {
2696
- strictInputType: false,
2697
- });
2698
-
2699
- const aFieldInRoot = getField(fields, 'a_string');
2700
- // It's the entire json schema
2701
- expect(aFieldInRoot.scopedJsonSchema).toEqual(schemaWithoutInputTypes);
2702
-
2703
- const aFieldset = getField(fields, 'a_object');
2704
- const aFieldInTheFieldset = getField(aFieldset.fields, 'foo');
2705
-
2706
- // It's only the json schema of that fieldset
2707
- expect(aFieldInTheFieldset.scopedJsonSchema).toEqual(
2708
- schemaWithoutInputTypes.properties.a_object
2709
- );
2710
- });
2711
-
2712
- describe('Order of fields', () => {
2713
- it('sorts fields based on presentation.position keyword (deprecated)', () => {
2714
- const { fields } = createHeadlessForm(schemaWithPositionDeprecated);
2715
-
2716
- // Assert the order from the original schema object
2717
- expect(Object.keys(schemaWithPositionDeprecated.properties)).toEqual([
2718
- 'age',
2719
- 'street',
2720
- 'username',
2721
- ]);
2722
- expect(Object.keys(schemaWithPositionDeprecated.properties.street.properties)).toEqual([
2723
- 'line_one',
2724
- 'postal_code',
2725
- 'number',
2726
- ]);
2727
-
2728
- // Assert the Fields order
2729
- const fieldsByName = fields.map((f) => f.name);
2730
- expect(fieldsByName).toEqual(['username', 'age', 'street']);
2731
-
2732
- const fieldsetByName = fields[2].fields.map((f) => f.name);
2733
- expect(fieldsetByName).toEqual(['line_one', 'number', 'postal_code']);
2734
- });
2735
-
2736
- it('sorts fields based on x-jsf-order keyword', () => {
2737
- const { fields } = createHeadlessForm(schemaWithOrderKeyword);
2738
-
2739
- // Assert the order from the original schema object
2740
- expect(Object.keys(schemaWithOrderKeyword.properties)).toEqual([
2741
- 'age',
2742
- 'street',
2743
- 'username',
2744
- ]);
2745
- expect(Object.keys(schemaWithOrderKeyword.properties.street.properties)).toEqual([
2746
- 'line_one',
2747
- 'postal_code',
2748
- 'number',
2749
- ]);
2750
-
2751
- // Assert the Fields order
2752
- const fieldsByName = fields.map((f) => f.name);
2753
- expect(fieldsByName).toEqual(['username', 'age', 'street']);
2754
-
2755
- const fieldsetByName = fields[2].fields.map((f) => f.name);
2756
- expect(fieldsetByName).toEqual(['line_one', 'number', 'postal_code']);
2757
- });
2758
-
2759
- it('sorts fields based on original properties (without x-jsf-order)', () => {
2760
- // Assert the sample schema has x-jsf-order
2761
- expect(schemaWithOrderKeyword['x-jsf-order']).toBeDefined();
2762
-
2763
- const schemaWithoutOrder = {
2764
- ...schemaWithOrderKeyword,
2765
- 'x-jsf-order': undefined,
2766
- };
2767
- const { fields } = createHeadlessForm(schemaWithoutOrder);
2768
-
2769
- const originalOrder = ['age', 'street', 'username'];
2770
- // Assert the order from the original schema object
2771
- expect(Object.keys(schemaWithoutOrder.properties)).toEqual(originalOrder);
2772
-
2773
- // Assert the order of fields is the same as the original object
2774
- const fieldsByName = fields.map((f) => f.name);
2775
- expect(fieldsByName).toEqual(originalOrder);
2776
- });
2777
- });
2778
- });
2779
-
2780
- describe('more validations', () => {
2781
- describe('when a field is required', () => {
2782
- let fields;
2783
- beforeEach(() => {
2784
- const result = createHeadlessForm(
2785
- buildJSONSchemaInput({ presentationFields: { inputType: 'text' }, required: true })
2786
- );
2787
- fields = result.fields;
2788
- });
2789
- describe('and value is empty', () => {
2790
- it('should throw an error', async () =>
2791
- expect(
2792
- object()
2793
- .shape({
2794
- test: fields[0].schema,
2795
- })
2796
- .validate({ test: '' })
2797
- ).rejects.toMatchObject({ errors: ['Required field'] }));
2798
- });
2799
- describe('and value is defined', () => {
2800
- it('should validate field', async () => {
2801
- const assertObj = { test: 'Hello' };
2802
- return expect(
2803
- object()
2804
- .shape({
2805
- test: fields[0].schema,
2806
- })
2807
- .validate(assertObj)
2808
- ).resolves.toEqual(assertObj);
2809
- });
2810
- });
2811
- });
2812
-
2813
- describe('when a field is number', () => {
2814
- let fields;
2815
- beforeEach(() => {
2816
- const result = createHeadlessForm(
2817
- buildJSONSchemaInput({ presentationFields: { inputType: 'number' } })
2818
- );
2819
- fields = result.fields;
2820
- });
2821
- describe('and value is a string', () => {
2822
- it('should throw an error', async () =>
2823
- expect(
2824
- object()
2825
- .shape({
2826
- test: fields[0].schema,
2827
- })
2828
- .validate({ test: 'Hello' })
2829
- ).rejects.toThrow());
2830
- });
2831
- describe('and value is a number', () => {
2832
- it('should validate field', async () => {
2833
- const assertObj = { test: 3 };
2834
- return expect(
2835
- object()
2836
- .shape({
2837
- test: fields[0].schema,
2838
- })
2839
- .validate(assertObj)
2840
- ).resolves.toEqual(assertObj);
2841
- });
2842
- });
2843
- describe('and maximum is set to zero', () => {
2844
- it('shows the correct validation', () => {
2845
- const { handleValidation } = createHeadlessForm(schemaInputTypeNumberZeroMaximum);
2846
- const validateForm = (vals) => friendlyError(handleValidation(vals));
2847
-
2848
- expect(validateForm({ tabs: '0' })).toBeUndefined();
2849
- expect(validateForm({ tabs: '-10' })).toBeUndefined();
2850
-
2851
- expect(validateForm({ tabs: 1 })).toEqual({
2852
- tabs: 'Must be smaller or equal to 0',
2853
- });
2854
- });
2855
- });
2856
- });
2857
-
2858
- describe('when a field has a maxLength of 10', () => {
2859
- let fields;
2860
- beforeEach(() => {
2861
- const result = createHeadlessForm(
2862
- buildJSONSchemaInput({
2863
- presentationFields: { inputType: 'text' },
2864
- inputFields: { maxLength: 10 },
2865
- })
2866
- );
2867
- fields = result.fields;
2868
- });
2869
- describe('and value is greater than that', () => {
2870
- it('should throw an error', async () =>
2871
- expect(
2872
- object()
2873
- .shape({
2874
- test: fields[0].schema,
2875
- })
2876
- .validate({ test: 'Hello Mr John Doe' })
2877
- ).rejects.toMatchObject({ errors: ['Please insert up to 10 characters'] }));
2878
- });
2879
- describe('and value is less than that', () => {
2880
- it('should validate field', async () => {
2881
- const assertObj = { test: 'Hello John' };
2882
- return expect(
2883
- object()
2884
- .shape({
2885
- test: fields[0].schema,
2886
- })
2887
- .validate(assertObj)
2888
- ).resolves.toEqual(assertObj);
2889
- });
2890
- });
2891
- });
2892
-
2893
- describe('when a field has a minLength of 2', () => {
2894
- let fields;
2895
- beforeEach(() => {
2896
- const result = createHeadlessForm(
2897
- buildJSONSchemaInput({
2898
- presentationFields: { inputType: 'text' },
2899
- inputFields: { minLength: 2 },
2900
- })
2901
- );
2902
- fields = result.fields;
2903
- });
2904
- describe('and value is smaller than that', () => {
2905
- it('should throw an error', async () =>
2906
- expect(
2907
- object()
2908
- .shape({
2909
- test: fields[0].schema,
2910
- })
2911
- .validate({ test: 'H' })
2912
- ).rejects.toMatchObject({ errors: ['Please insert at least 2 characters'] }));
2913
- });
2914
- describe('and value is greater than that', () => {
2915
- it('should validate field', async () => {
2916
- const assertObj = { test: 'Hello John' };
2917
- return expect(
2918
- object()
2919
- .shape({
2920
- test: fields[0].schema,
2921
- })
2922
- .validate(assertObj)
2923
- ).resolves.toEqual(assertObj);
2924
- });
2925
- });
2926
- });
2927
-
2928
- describe('when a field has a minimum of 0', () => {
2929
- let fields;
2930
- beforeEach(() => {
2931
- const result = createHeadlessForm(
2932
- buildJSONSchemaInput({
2933
- presentationFields: { inputType: 'number' },
2934
- inputFields: { minimum: 0 },
2935
- })
2936
- );
2937
- fields = result.fields;
2938
- });
2939
-
2940
- describe('and value is less than that', () => {
2941
- it('should throw an error', async () =>
2942
- expect(
2943
- object()
2944
- .shape({
2945
- test: fields[0].schema,
2946
- })
2947
- .validate({ test: -1 })
2948
- ).rejects.toMatchObject({ errors: ['Must be greater or equal to 0'] }));
2949
- });
2950
-
2951
- describe('and value is greater than that', () => {
2952
- it('should validate field', async () => {
2953
- const assertObj = { test: 4 };
2954
- return expect(
2955
- object()
2956
- .shape({
2957
- test: fields[0].schema,
2958
- })
2959
- .validate(assertObj)
2960
- ).resolves.toEqual(assertObj);
2961
- });
2962
- });
2963
- });
2964
-
2965
- describe('when a field has a maximum of 10', () => {
2966
- let fields;
2967
- beforeEach(() => {
2968
- const result = createHeadlessForm(
2969
- buildJSONSchemaInput({
2970
- presentationFields: { inputType: 'number' },
2971
- inputFields: { maximum: 10 },
2972
- })
2973
- );
2974
- fields = result.fields;
2975
- });
2976
-
2977
- describe('and value is greater than that', () => {
2978
- it('should throw an error', async () =>
2979
- expect(
2980
- object()
2981
- .shape({
2982
- test: fields[0].schema,
2983
- })
2984
- .validate({ test: 11 })
2985
- ).rejects.toMatchObject({ errors: ['Must be smaller or equal to 10'] }));
2986
- });
2987
-
2988
- describe('and value is greater than that', () => {
2989
- it('should validate field', async () => {
2990
- const assertObj = { test: 4 };
2991
- return expect(
2992
- object()
2993
- .shape({
2994
- test: fields[0].schema,
2995
- })
2996
- .validate(assertObj)
2997
- ).resolves.toEqual(assertObj);
2998
- });
2999
- });
3000
- });
3001
-
3002
- describe('when a field has a pattern', () => {
3003
- let fields;
3004
- beforeEach(() => {
3005
- const result = createHeadlessForm(
3006
- buildJSONSchemaInput({
3007
- presentationFields: { inputType: 'text' },
3008
- inputFields: { pattern: '^[0-9]{3}-[0-9]{2}-(?!0{4})[0-9]{4}$' },
3009
- })
3010
- );
3011
- fields = result.fields;
3012
- });
3013
- describe('and value does not match the pattern', () => {
3014
- it('should throw an error', async () =>
3015
- expect(
3016
- object()
3017
- .shape({
3018
- test: fields[0].schema,
3019
- })
3020
- .validate({ test: 'Hello' })
3021
- ).rejects.toMatchObject({ errors: [expect.any(String)] }));
3022
- });
3023
- describe('and value matches the pattern', () => {
3024
- it('should validate field', async () => {
3025
- const assertObj = { test: '401-85-1950' };
3026
- return expect(
3027
- object()
3028
- .shape({
3029
- test: fields[0].schema,
3030
- })
3031
- .validate(assertObj)
3032
- ).resolves.toEqual(assertObj);
3033
- });
3034
- });
3035
- });
3036
- });
3037
-
3038
- describe('even more validations', () => {
3039
- describe('and all fields are optional', () => {
3040
- let handleValidation;
3041
- const validateForm = (vals) => friendlyError(handleValidation(vals));
3042
-
3043
- beforeEach(() => {
3044
- const result = {
3045
- properties: {
3046
- textInput: mockTextInput,
3047
- numberInput: mockNumberInput,
3048
- },
3049
- };
3050
- const { handleValidation: handleValidationEach } = createHeadlessForm(result);
3051
- handleValidation = handleValidationEach;
3052
- });
3053
-
3054
- it.each([
3055
- [
3056
- 'validation should return true when the object has empty values',
3057
- { textInput: '' },
3058
- undefined,
3059
- ],
3060
- [
3061
- 'validation should return true when object is valid',
3062
- { textInput: 'abcde', numberInput: 9 },
3063
- undefined,
3064
- ],
3065
- ])('%s', (_, value, errors) => {
3066
- const testValue = validateForm(value);
3067
- if (errors) {
3068
- expect(testValue).toEqual(errors);
3069
- } else {
3070
- expect(testValue).toBeUndefined();
3071
- }
3072
- });
3073
- });
3074
-
3075
- describe('and all fields are mandatory', () => {
3076
- let handleValidation;
3077
- const validateForm = (vals) => friendlyError(handleValidation(vals));
3078
-
3079
- beforeEach(() => {
3080
- const result = {
3081
- properties: {
3082
- textInput: mockTextInput,
3083
- numberInput: mockNumberInput,
3084
- },
3085
- required: ['numberInput', 'textInput'],
3086
- };
3087
- const { handleValidation: handleValidationEach } = createHeadlessForm(result);
3088
- handleValidation = handleValidationEach;
3089
- });
3090
-
3091
- it.each([
3092
- [
3093
- 'validation should return false when value is an empty object',
3094
- {},
3095
- {
3096
- numberInput: 'Required field',
3097
- textInput: 'Required field',
3098
- },
3099
- ],
3100
- [
3101
- 'validation should return false when value is an object with null values',
3102
- { textInput: null, numberInput: null },
3103
- { numberInput: 'Required field', textInput: 'Required field' },
3104
- ],
3105
- [
3106
- 'validation should return false when value is an object with empty values',
3107
- { textInput: '', numberInput: '' },
3108
- { numberInput: 'The value must be a number', textInput: 'Required field' },
3109
- ],
3110
- [
3111
- 'validation should return false when one value is empty',
3112
- { textInput: '986-39-076', numberInput: '' },
3113
- { numberInput: 'The value must be a number' },
3114
- ],
3115
- [
3116
- 'validation should return false a numeric field is not a number',
3117
- { textInput: '986-39-076', numberInput: 'not a number' },
3118
- { numberInput: 'The value must be a number' },
3119
- ],
3120
- [
3121
- 'validation should return true when object is valid',
3122
- { textInput: 'abc-xy-asd', numberInput: 9 },
3123
- undefined,
3124
- ],
3125
- ])('%s', (_, values, errors) => {
3126
- const testValue = validateForm(values);
3127
- if (errors) {
3128
- expect(testValue).toEqual(errors);
3129
- } else {
3130
- expect(testValue).toBeUndefined();
3131
- }
3132
- });
3133
-
3134
- describe('and one field has pattern validation', () => {
3135
- beforeEach(() => {
3136
- const result = JSONSchemaBuilder()
3137
- .addInput({ patternTextInput: mockTextPatternInput })
3138
- .setRequiredFields(['patternTextInput'])
3139
- .build();
3140
- const { handleValidation: handleValidationEach } = createHeadlessForm(result);
3141
- handleValidation = handleValidationEach;
3142
- });
3143
-
3144
- it.each([
3145
- [
3146
- 'validation should return false when a value does not match a pattern',
3147
- { patternTextInput: 'abc-xy-asd' },
3148
- { patternTextInput: expect.stringMatching(/Must have a valid format. E.g./i) },
3149
- ],
3150
- [
3151
- 'validation should return true when value matches the pattern',
3152
- { patternTextInput: '986-39-0716' },
3153
- undefined,
3154
- ],
3155
- ])('%s', (_, values, errors) => {
3156
- const testValue = validateForm(values);
3157
- if (errors) {
3158
- expect(testValue).toEqual(errors);
3159
- } else {
3160
- expect(testValue).toBeUndefined();
3161
- }
3162
- });
3163
- });
3164
-
3165
- describe('and one field has max length validation', () => {
3166
- beforeEach(() => {
3167
- const result = JSONSchemaBuilder()
3168
- .addInput({ maxLengthTextInput: mockTextMaxLengthInput })
3169
- .setRequiredFields(['maxLengthTextInput'])
3170
- .build();
3171
- const { handleValidation: handleValidationEach } = createHeadlessForm(result);
3172
- handleValidation = handleValidationEach;
3173
- });
3174
-
3175
- it.each([
3176
- [
3177
- 'validation should return false when a value is greater than the limit',
3178
- { maxLengthTextInput: 'Hello John Dow' },
3179
- { maxLengthTextInput: 'Please insert up to 10 characters' },
3180
- ],
3181
- [
3182
- 'validation should return true when value is within the limit',
3183
- { maxLengthTextInput: 'Hello John' },
3184
- undefined,
3185
- ],
3186
- ])('%s', (_, values, errors) => {
3187
- const testValue = validateForm(values);
3188
- if (errors) {
3189
- expect(testValue).toEqual(errors);
3190
- } else {
3191
- expect(testValue).toBeUndefined();
3192
- }
3193
- });
3194
- });
3195
- });
3196
-
3197
- describe('and fields are dynamically required/optional', () => {
3198
- it('applies correct validation for single-value based conditionals', async () => {
3199
- const { fields, handleValidation } = createHeadlessForm(schemaDynamicValidationConst);
3200
- const validateForm = (vals) => friendlyError(handleValidation(vals));
3201
-
3202
- expect(
3203
- validateForm({
3204
- validate_tabs: 'no',
3205
- a_fieldset: {
3206
- username: 'abc',
3207
- },
3208
- mandatory_group_array: 'no',
3209
- })
3210
- ).toBeUndefined();
3211
-
3212
- const getTabsField = () =>
3213
- fields.find((f) => f.name === 'a_fieldset').fields.find((f) => f.name === 'tabs');
3214
-
3215
- expect(getTabsField().required).toBeFalsy();
3216
-
3217
- expect(
3218
- validateForm({
3219
- validate_tabs: 'yes',
3220
- a_fieldset: {
3221
- username: 'abc',
3222
- },
3223
- mandatory_group_array: 'no',
3224
- })
3225
- ).toEqual({
3226
- a_fieldset: {
3227
- tabs: 'Required field',
3228
- },
3229
- });
3230
-
3231
- expect(getTabsField().required).toBeTruthy();
3232
-
3233
- expect(
3234
- validateForm({
3235
- validate_tabs: 'yes',
3236
- a_fieldset: {
3237
- username: 'abc',
3238
- },
3239
- mandatory_group_array: 'yes',
3240
- a_group_array: [{ full_name: 'adfs' }],
3241
- })
3242
- ).toEqual({ a_fieldset: { tabs: 'Required field' } });
3243
-
3244
- expect(
3245
- validateForm({
3246
- validate_tabs: 'yes',
3247
- a_fieldset: {
3248
- username: 'abc',
3249
- tabs: 2,
3250
- },
3251
- mandatory_group_array: 'no',
3252
- })
3253
- ).toBeUndefined();
3254
- });
3255
-
3256
- it('applies correct validation for minimum/maximum conditionals', async () => {
3257
- const { handleValidation } = createHeadlessForm(schemaDynamicValidationMinimumMaximum);
3258
- const validateForm = (vals) => friendlyError(handleValidation(vals));
3259
-
3260
- // Check for minimum condition
3261
- expect(
3262
- validateForm({
3263
- a_number: 0,
3264
- })
3265
- ).toEqual({
3266
- a_conditional_text: 'Required field',
3267
- a_number: 'Must be greater or equal to 1',
3268
- });
3269
-
3270
- // Check for maximum condition
3271
- expect(
3272
- validateForm({
3273
- a_number: 11,
3274
- })
3275
- ).toEqual({
3276
- a_conditional_text: 'Required field',
3277
- a_number: 'Must be smaller or equal to 10',
3278
- });
3279
-
3280
- // Check for absence of a_number
3281
- expect(validateForm({})).toEqual({
3282
- a_conditional_text: 'Required field',
3283
- });
3284
-
3285
- // Check for number within range
3286
- expect(
3287
- validateForm({
3288
- a_number: 5,
3289
- })
3290
- ).toBeUndefined();
3291
- });
3292
-
3293
- it('applies correct validation for minLength/maxLength conditionals', async () => {
3294
- const { handleValidation } = createHeadlessForm(schemaDynamicValidationMinLengthMaxLength);
3295
- const validateForm = (vals) => friendlyError(handleValidation(vals));
3296
- const formError = {
3297
- a_conditional_text: 'Required field',
3298
- };
3299
- // By default a_conditional_text is required.
3300
- expect(validateForm({})).toEqual(formError);
3301
-
3302
- // Check for minimum length condition - a_text >= 3 chars
3303
- expect(
3304
- validateForm({
3305
- a_text: 'Foo',
3306
- })
3307
- ).toBeUndefined();
3308
-
3309
- // Check for maximum length condition - a_text <= 5 chars
3310
- expect(
3311
- validateForm({
3312
- a_text: 'Fooba',
3313
- })
3314
- ).toBeUndefined();
3315
-
3316
- // Check for text out of length range (7 chars)
3317
- expect(
3318
- validateForm({
3319
- a_text: 'Foobaaz',
3320
- })
3321
- ).toEqual(formError);
3322
-
3323
- // Check for text out of length range (2 chars)
3324
- expect(
3325
- validateForm({
3326
- a_text: 'Fe',
3327
- })
3328
- ).toEqual(formError);
3329
- });
3330
-
3331
- it('applies correct validation for array-contain based conditionals', async () => {
3332
- const { handleValidation } = createHeadlessForm(schemaDynamicValidationContains);
3333
- const validateForm = (vals) => friendlyError(handleValidation(vals));
3334
-
3335
- expect(
3336
- validateForm({
3337
- validate_fieldset: ['username'],
3338
- a_fieldset: {
3339
- username: 'abc',
3340
- },
3341
- })
3342
- ).toBeUndefined();
3343
-
3344
- expect(
3345
- validateForm({
3346
- validate_fieldset: ['username', 'all'],
3347
- a_fieldset: {
3348
- username: 'abc',
3349
- },
3350
- })
3351
- ).toEqual({
3352
- a_fieldset: {
3353
- tabs: 'Required field',
3354
- },
3355
- });
3356
-
3357
- expect(
3358
- validateForm({
3359
- validate_fieldset: ['username', 'all'],
3360
- a_fieldset: {
3361
- username: 'abc',
3362
- tabs: 2,
3363
- },
3364
- })
3365
- ).toBeUndefined();
3366
- });
3367
-
3368
- it('applies correct validation for fieldset fields', async () => {
3369
- const { handleValidation } = createHeadlessForm(schemaDynamicValidationContains);
3370
- const validateForm = (vals) => friendlyError(handleValidation(vals));
3371
-
3372
- expect(
3373
- validateForm({
3374
- validate_fieldset: ['username'],
3375
- a_fieldset: {
3376
- username: 'abc',
3377
- },
3378
- })
3379
- ).toBeUndefined();
3380
-
3381
- expect(
3382
- validateForm({
3383
- validate_fieldset: ['username', 'all'],
3384
- a_fieldset: {
3385
- username: 'abc',
3386
- },
3387
- })
3388
- ).toEqual({
3389
- a_fieldset: {
3390
- tabs: 'Required field',
3391
- },
3392
- });
3393
-
3394
- expect(
3395
- validateForm({
3396
- validate_fieldset: ['username', 'all'],
3397
- a_fieldset: {
3398
- username: 'abc',
3399
- tabs: 2,
3400
- },
3401
- })
3402
- ).toBeUndefined();
3403
- });
3404
-
3405
- it('applies any of the validation alternatives in a anyOf branch', async () => {
3406
- const { handleValidation } = createHeadlessForm(schemaAnyOfValidation);
3407
- const validateForm = (vals) => friendlyError(handleValidation(vals));
3408
-
3409
- expect(
3410
- validateForm({
3411
- field_a: '123',
3412
- })
3413
- ).toBeUndefined();
3414
-
3415
- expect(
3416
- validateForm({
3417
- field_b: '456',
3418
- })
3419
- ).toEqual({ field_c: 'Required field' });
3420
-
3421
- expect(
3422
- validateForm({
3423
- field_b: '456',
3424
- field_c: '789',
3425
- })
3426
- ).toBeUndefined();
3427
-
3428
- expect(
3429
- validateForm({
3430
- field_a: '123',
3431
- field_c: '789',
3432
- })
3433
- ).toBeUndefined();
3434
-
3435
- expect(
3436
- validateForm({
3437
- field_a: '123',
3438
- field_b: '456',
3439
- field_c: '789',
3440
- })
3441
- ).toBeUndefined();
3442
- });
3443
-
3444
- describe('nested conditionals', () => {
3445
- it('given empty values, runs "else" (gets hidden)', () => {
3446
- const { fields } = createHeadlessForm(schemaWithConditionalReadOnlyProperty, {
3447
- field_a: null,
3448
- });
3449
- expect(getField(fields, 'field_b').isVisible).toBe(false);
3450
- });
3451
-
3452
- it('given a match, runs "then" (turns visible and editable)', () => {
3453
- const { fields } = createHeadlessForm(schemaWithConditionalReadOnlyProperty, {
3454
- initialValues: { field_a: 'yes' },
3455
- });
3456
- expect(getField(fields, 'field_b').isVisible).toBe(true);
3457
- expect(getField(fields, 'field_b').readOnly).toBe(false);
3458
- });
3459
-
3460
- it('given a nested match, runs "else-then" (turns visible but readOnly)', () => {
3461
- const { fields } = createHeadlessForm(schemaWithConditionalReadOnlyProperty, {
3462
- initialValues: { field_a: 'no' },
3463
- });
3464
- expect(getField(fields, 'field_b').isVisible).toBe(true);
3465
- expect(getField(fields, 'field_b').readOnly).toBe(true);
3466
- });
3467
- });
3468
-
3469
- describe('conditional fields (incorrectly done)', () => {
3470
- // this catches the typical scenario where developers forget to set the if.required[]
3471
-
3472
- it('given empty values, the incorrect conditional runs "then" instead of "else"', () => {
3473
- const { fields: fieldsEmpty } = createHeadlessForm(schemaWithWrongConditional, {
3474
- initialValues: { field_a: null, field_a_wrong: null },
3475
- });
3476
- // The dependent correct field gets hidden, but...
3477
- expect(getField(fieldsEmpty, 'field_b').isVisible).toBe(false);
3478
- // ...the dependent wrong field stays visible because the
3479
- // conditional is wrong (it's missing the if.required[])
3480
- expect(getField(fieldsEmpty, 'field_b_wrong').isVisible).toBe(true);
3481
- });
3482
-
3483
- it('given a match ("yes"), both runs "then" (turn visible)', () => {
3484
- const { fields: fieldsVisible } = createHeadlessForm(schemaWithWrongConditional, {
3485
- initialValues: { field_a: 'yes', field_a_wrong: 'yes' },
3486
- });
3487
- expect(getField(fieldsVisible, 'field_b').isVisible).toBe(true);
3488
- expect(getField(fieldsVisible, 'field_b_wrong').isVisible).toBe(true);
3489
- });
3490
-
3491
- it('not given a match ("no"), both run else (stay hidden)', () => {
3492
- const { fields: fieldsHidden } = createHeadlessForm(schemaWithWrongConditional, {
3493
- initialValues: { field_a: 'no', field_a_wrong: 'no' },
3494
- });
3495
- expect(getField(fieldsHidden, 'field_b').isVisible).toBe(false);
3496
- expect(getField(fieldsHidden, 'field_b_wrong').isVisible).toBe(false);
3497
- });
3498
- });
3499
-
3500
- it('checkbox should have no initial value when its dynamically shown and invisible', () => {
3501
- const { fields } = createHeadlessForm(schemaWithConditionalAcknowledgementProperty, {
3502
- initialValues: {
3503
- field_a: 'no',
3504
- },
3505
- });
3506
- const dependentField = getField(fields, 'field_b');
3507
- expect(dependentField.isVisible).toBe(false);
3508
- expect(dependentField.value).toBe(undefined);
3509
- });
3510
-
3511
- it('checkbox should have no initial value when its dynamically shown and visible', () => {
3512
- const { fields } = createHeadlessForm(schemaWithConditionalAcknowledgementProperty, {
3513
- initialValues: {
3514
- field_a: 'yes',
3515
- },
3516
- });
3517
- const dependentField = getField(fields, 'field_b');
3518
- expect(dependentField.isVisible).toBe(true);
3519
- expect(dependentField.value).toBe(undefined);
3520
- });
3521
- });
3522
- });
3523
-
3524
- // TODO: delete after migration to x-jsf-errorMessage is completed
3525
- describe('Throwing custom error messages using errorMessage (deprecated)', () => {
3526
- it.each([
3527
- [
3528
- 'type',
3529
- JSONSchemaBuilder()
3530
- .addInput({
3531
- numberInput: {
3532
- ...mockNumberInput,
3533
- errorMessage: { type: 'It has to be a number.' },
3534
- },
3535
- })
3536
- .build(),
3537
- { numberInput: 'Two' },
3538
- {
3539
- numberInput: 'It has to be a number.',
3540
- },
3541
- false,
3542
- ],
3543
- [
3544
- 'minimum',
3545
- JSONSchemaBuilder()
3546
- .addInput({
3547
- numberInput: {
3548
- ...mockNumberInput,
3549
- errorMessage: { minimum: 'I am a custom error message' },
3550
- },
3551
- })
3552
- .build(),
3553
- { numberInput: -1 },
3554
- {
3555
- numberInput: 'I am a custom error message',
3556
- },
3557
- false,
3558
- ],
3559
- [
3560
- 'required',
3561
- JSONSchemaBuilder()
3562
- .addInput({
3563
- numberInput: {
3564
- ...mockNumberInput,
3565
- errorMessage: { required: 'I am a custom error message' },
3566
- },
3567
- })
3568
- .setRequiredFields(['numberInput'])
3569
- .build(),
3570
- {},
3571
- {
3572
- numberInput: 'I am a custom error message',
3573
- },
3574
- ],
3575
- [
3576
- 'required (ignored because it is optional)',
3577
- JSONSchemaBuilder()
3578
- .addInput({
3579
- numberInput: {
3580
- ...mockNumberInput,
3581
- errorMessage: { required: 'I am a custom error message' },
3582
- },
3583
- })
3584
- .build(),
3585
- {},
3586
- undefined,
3587
- ],
3588
- [
3589
- 'maximum',
3590
- JSONSchemaBuilder()
3591
- .addInput({
3592
- numberInput: {
3593
- ...mockNumberInput,
3594
- errorMessage: { maximum: 'I am a custom error message' },
3595
- },
3596
- })
3597
- .build(),
3598
- { numberInput: 11 },
3599
- {
3600
- numberInput: 'I am a custom error message',
3601
- },
3602
- ],
3603
- [
3604
- 'minLength',
3605
- JSONSchemaBuilder()
3606
- .addInput({
3607
- stringInput: {
3608
- ...mockTextInput,
3609
- minLength: 3,
3610
- errorMessage: { minLength: 'I am a custom error message' },
3611
- },
3612
- })
3613
- .build(),
3614
- { stringInput: 'aa' },
3615
- {
3616
- stringInput: 'I am a custom error message',
3617
- },
3618
- ],
3619
- [
3620
- 'maxLength',
3621
- JSONSchemaBuilder()
3622
- .addInput({
3623
- stringInput: {
3624
- ...mockTextInput,
3625
- maxLength: 3,
3626
- errorMessage: { maxLength: 'I am a custom error message' },
3627
- },
3628
- })
3629
- .build(),
3630
- { stringInput: 'aaaa' },
3631
- {
3632
- stringInput: 'I am a custom error message',
3633
- },
3634
- ],
3635
- [
3636
- 'pattern',
3637
- JSONSchemaBuilder()
3638
- .addInput({
3639
- stringInput: {
3640
- ...mockTextInput,
3641
- pattern: '^(\\+|00)\\d*$',
3642
- errorMessage: { pattern: 'I am a custom error message' },
3643
- },
3644
- })
3645
- .build(),
3646
- { stringInput: 'aaaa' },
3647
- {
3648
- stringInput: 'I am a custom error message',
3649
- },
3650
- ],
3651
- [
3652
- 'maxFileSize',
3653
- JSONSchemaBuilder()
3654
- .addInput({
3655
- fileInput: {
3656
- ...mockFileInput,
3657
- 'x-jsf-presentation': {
3658
- ...mockFileInput['x-jsf-presentation'],
3659
- maxFileSize: 1000,
3660
- },
3661
- errorMessage: { maxFileSize: 'I am a custom error message' },
3662
- },
3663
- })
3664
- .build(),
3665
- {
3666
- fileInput: [
3667
- (() => {
3668
- const file = new File([''], 'file.png');
3669
- Object.defineProperty(file, 'size', { value: 1024 * 1024 * 1024 });
3670
- return file;
3671
- })(),
3672
- ],
3673
- },
3674
- {
3675
- fileInput: 'I am a custom error message',
3676
- },
3677
- ],
3678
- [
3679
- 'accept',
3680
- JSONSchemaBuilder()
3681
- .addInput({
3682
- fileInput: {
3683
- ...mockFileInput,
3684
- accept: '.pdf',
3685
- errorMessage: { accept: 'I am a custom error message' },
3686
- },
3687
- })
3688
- .build(),
3689
- {
3690
- fileInput: [new File([''], 'file.docx')],
3691
- },
3692
- {
3693
- fileInput: 'I am a custom error message',
3694
- },
3695
- ],
3696
- ])('error message for property "%s"', (_, schema, input, errors) => {
3697
- const { handleValidation } = createHeadlessForm(schema);
3698
- const validateForm = (vals) => friendlyError(handleValidation(vals));
3699
-
3700
- if (errors) {
3701
- expect(validateForm(input)).toEqual(errors);
3702
- } else {
3703
- expect(validateForm(input)).toBeUndefined();
3704
- }
3705
- });
3706
- });
3707
-
3708
- describe('Custom error messages', () => {
3709
- it.each([
3710
- [
3711
- 'type',
3712
- JSONSchemaBuilder()
3713
- .addInput({
3714
- numberInput: {
3715
- ...mockNumberInput,
3716
- 'x-jsf-errorMessage': { type: 'It has to be a number.' },
3717
- },
3718
- })
3719
- .build(),
3720
- { numberInput: 'Two' },
3721
- {
3722
- numberInput: 'It has to be a number.',
3723
- },
3724
- false,
3725
- ],
3726
- [
3727
- 'minimum',
3728
- JSONSchemaBuilder()
3729
- .addInput({
3730
- numberInput: {
3731
- ...mockNumberInput,
3732
- 'x-jsf-errorMessage': { minimum: 'I am a custom error message' },
3733
- },
3734
- })
3735
- .build(),
3736
- { numberInput: -1 },
3737
- {
3738
- numberInput: 'I am a custom error message',
3739
- },
3740
- false,
3741
- ],
3742
- [
3743
- 'required',
3744
- JSONSchemaBuilder()
3745
- .addInput({
3746
- numberInput: {
3747
- ...mockNumberInput,
3748
- 'x-jsf-errorMessage': { required: 'I am a custom error message' },
3749
- },
3750
- })
3751
- .setRequiredFields(['numberInput'])
3752
- .build(),
3753
- {},
3754
- {
3755
- numberInput: 'I am a custom error message',
3756
- },
3757
- ],
3758
- [
3759
- 'required (ignored because it is optional)',
3760
- JSONSchemaBuilder()
3761
- .addInput({
3762
- numberInput: {
3763
- ...mockNumberInput,
3764
- 'x-jsf-errorMessage': { required: 'I am a custom error message' },
3765
- },
3766
- })
3767
- .build(),
3768
- {},
3769
- undefined,
3770
- ],
3771
- [
3772
- 'maximum',
3773
- JSONSchemaBuilder()
3774
- .addInput({
3775
- numberInput: {
3776
- ...mockNumberInput,
3777
- 'x-jsf-errorMessage': { maximum: 'I am a custom error message' },
3778
- },
3779
- })
3780
- .build(),
3781
- { numberInput: 11 },
3782
- {
3783
- numberInput: 'I am a custom error message',
3784
- },
3785
- ],
3786
- [
3787
- 'minLength',
3788
- JSONSchemaBuilder()
3789
- .addInput({
3790
- stringInput: {
3791
- ...mockTextInput,
3792
- minLength: 3,
3793
- 'x-jsf-errorMessage': { minLength: 'I am a custom error message' },
3794
- },
3795
- })
3796
- .build(),
3797
- { stringInput: 'aa' },
3798
- {
3799
- stringInput: 'I am a custom error message',
3800
- },
3801
- ],
3802
- [
3803
- 'maxLength',
3804
- JSONSchemaBuilder()
3805
- .addInput({
3806
- stringInput: {
3807
- ...mockTextInput,
3808
- maxLength: 3,
3809
- 'x-jsf-errorMessage': { maxLength: 'I am a custom error message' },
3810
- },
3811
- })
3812
- .build(),
3813
- { stringInput: 'aaaa' },
3814
- {
3815
- stringInput: 'I am a custom error message',
3816
- },
3817
- ],
3818
- [
3819
- 'pattern',
3820
- JSONSchemaBuilder()
3821
- .addInput({
3822
- stringInput: {
3823
- ...mockTextInput,
3824
- pattern: '^(\\+|00)\\d*$',
3825
- 'x-jsf-errorMessage': { pattern: 'I am a custom error message' },
3826
- },
3827
- })
3828
- .build(),
3829
- { stringInput: 'aaaa' },
3830
- {
3831
- stringInput: 'I am a custom error message',
3832
- },
3833
- ],
3834
- [
3835
- 'maxFileSize',
3836
- JSONSchemaBuilder()
3837
- .addInput({
3838
- fileInput: {
3839
- ...mockFileInput,
3840
- 'x-jsf-presentation': {
3841
- ...mockFileInput['x-jsf-presentation'],
3842
- maxFileSize: 1000,
3843
- },
3844
- 'x-jsf-errorMessage': { maxFileSize: 'I am a custom error message' },
3845
- },
3846
- })
3847
- .build(),
3848
- {
3849
- fileInput: [
3850
- (() => {
3851
- const file = new File([''], 'file.png');
3852
- Object.defineProperty(file, 'size', { value: 1024 * 1024 * 1024 });
3853
- return file;
3854
- })(),
3855
- ],
3856
- },
3857
- {
3858
- fileInput: 'I am a custom error message',
3859
- },
3860
- ],
3861
- [
3862
- 'accept',
3863
- JSONSchemaBuilder()
3864
- .addInput({
3865
- fileInput: {
3866
- ...mockFileInput,
3867
- accept: '.pdf',
3868
- 'x-jsf-errorMessage': { accept: 'I am a custom error message' },
3869
- },
3870
- })
3871
- .build(),
3872
- {
3873
- fileInput: [new File([''], 'file.docx')],
3874
- },
3875
- {
3876
- fileInput: 'I am a custom error message',
3877
- },
3878
- ],
3879
- ])('error message for property "%s"', (_, schema, input, errors) => {
3880
- const { handleValidation } = createHeadlessForm(schema);
3881
- const validateForm = (vals) => friendlyError(handleValidation(vals));
3882
-
3883
- if (errors) {
3884
- expect(validateForm(input)).toEqual(errors);
3885
- } else {
3886
- expect(validateForm(input)).toBeUndefined();
3887
- }
3888
- });
3889
-
3890
- it('accepts with options.inputType[].errorMessage', () => {
3891
- // Sanity-check the default error message
3892
- const resultDefault = createHeadlessForm(schemaForErrorMessageSpecificity);
3893
- expect(resultDefault.handleValidation({}).formErrors).toEqual({
3894
- weekday: 'Required field',
3895
- day: 'Required field',
3896
- month: 'Required field',
3897
- year: 'The year is mandatory.', // from x-jsf-errorMessage
3898
- });
3899
-
3900
- // Assert the custom error message
3901
- const resultCustom = createHeadlessForm(schemaForErrorMessageSpecificity, {
3902
- ...jsfConfigForErrorMessageSpecificity,
3903
- });
3904
- expect(resultCustom.handleValidation({}).formErrors).toEqual({
3905
- weekday: 'Required field', // sanity-check that a different inputType keeps the default error msg.
3906
- day: 'This cannot be empty.',
3907
- month: 'This cannot be empty.',
3908
- year: 'The year is mandatory.', // error specificity: schema's msg is higher than options' msg.
3909
- });
3910
- });
3911
- });
3912
-
3913
- describe('given default values', () => {
3914
- describe('and a field with conditional presentation properties', () => {
3915
- it('returns the nested properties when the conditional matches', () => {
3916
- const { fields } = createHeadlessForm(schemaWithConditionalPresentationProperties, {
3917
- initialValues: {
3918
- // show the hidden statement
3919
- mock_radio: 'no',
3920
- },
3921
- });
3922
-
3923
- expect(fields[0].statement.description).toBe(`<a href="">conditional statement markup</a>`);
3924
- });
3925
- });
3926
- describe('and "fieldset" has scoped conditionals', () => {
3927
- it('should show conditionals fields when values fullfil conditions', () => {
3928
- const result = createHeadlessForm(schemaFieldsetScopedCondition, {
3929
- initialValues: { child: { has_child: 'yes' } },
3930
- });
3931
-
3932
- const fieldset = result.fields[0];
3933
-
3934
- expect(fieldset).toMatchObject({
3935
- fields: [
3936
- {
3937
- name: 'has_child',
3938
- required: true,
3939
- },
3940
- {
3941
- name: 'age',
3942
- required: true,
3943
- isVisible: true,
3944
- },
3945
- {
3946
- name: 'passport_id',
3947
- required: false,
3948
- isVisible: true,
3949
- },
3950
- ],
3951
- });
3952
- });
3953
-
3954
- it('should hide conditionals fields when values do not fullfil conditions', () => {
3955
- const result = createHeadlessForm(schemaFieldsetScopedCondition, {
3956
- child: { has_child: 'no' },
3957
- });
3958
-
3959
- const fieldset = result.fields[0];
3960
-
3961
- expect(fieldset).toMatchObject({
3962
- fields: [
3963
- {
3964
- name: 'has_child',
3965
- required: true,
3966
- },
3967
- {
3968
- name: 'age',
3969
- required: false,
3970
- isVisible: false,
3971
- },
3972
- {
3973
- name: 'passport_id',
3974
- required: false,
3975
- isVisible: false,
3976
- },
3977
- ],
3978
- });
3979
- });
3980
-
3981
- it('should ignore initial values that do not match the field type (eg string vs object)', () => {
3982
- const result = createHeadlessForm(schemaInputTypeFieldset, {
3983
- initialValues: {
3984
- a_fieldset: 'foo', // should be an object instead of string
3985
- },
3986
- });
3987
-
3988
- // It returns fields without errors
3989
- expect(result.fields).toBeDefined();
3990
- expect(result.fields[0].fields[0].name).toBe('username');
3991
- expect(result.fields[0].fields[1].name).toBe('tabs');
3992
-
3993
- // Warn about those missmatched values
3994
- expect(console.warn).toHaveBeenCalledWith(
3995
- `Field "a_fieldset"'s value is "foo", but should be type object.`
3996
- );
3997
- console.warn.mockClear();
3998
-
3999
- expect(console.error).not.toHaveBeenCalled();
4000
- });
4001
- });
4002
- });
4003
-
4004
- describe('parser options', () => {
4005
- it('should support any custom field attribute', () => {
4006
- const customAttrs = {
4007
- something: 'foo', // a misc attribute
4008
- inputType: 'super', // overrides "textarea"
4009
- falsy: false, // accepts falsy attributes
4010
- };
4011
- const result = createHeadlessForm(
4012
- {
4013
- properties: {
4014
- feedback: {
4015
- title: 'Your feedback',
4016
- type: 'string',
4017
- presentation: {
4018
- inputType: 'textarea',
4019
- },
4020
- },
4021
- },
4022
- },
4023
- {
4024
- customProperties: {
4025
- feedback: {
4026
- ...customAttrs,
4027
- },
4028
- },
4029
- }
4030
- );
4031
-
4032
- expect(result).toMatchObject({
4033
- fields: [
4034
- {
4035
- name: 'feedback',
4036
- label: 'Your feedback',
4037
- jsonType: 'string',
4038
- ...customAttrs,
4039
- },
4040
- ],
4041
- });
4042
- });
4043
-
4044
- it('should support custom description (checkbox)', () => {
4045
- const result = createHeadlessForm(
4046
- {
4047
- properties: {
4048
- terms: {
4049
- const: 'Agreed',
4050
- title: 'Terms',
4051
- description: 'Accept terms.',
4052
- type: 'string',
4053
- presentation: { inputType: 'checkbox' },
4054
- },
4055
- },
4056
- },
4057
- {
4058
- customProperties: {
4059
- terms: {
4060
- description: (text) => `Extra text before. ${text}`,
4061
- },
4062
- },
4063
- }
4064
- );
4065
-
4066
- expect(result).toMatchObject({
4067
- fields: [
4068
- {
4069
- label: 'Terms',
4070
- description: 'Extra text before. Accept terms.', // ensure custom description works
4071
- name: 'terms',
4072
- required: false,
4073
- inputType: 'checkbox',
4074
- type: 'checkbox',
4075
- jsonType: 'string',
4076
- checkboxValue: 'Agreed', // ensure _composeFieldCheckbox(). transformations are passed.
4077
- },
4078
- ],
4079
- });
4080
-
4081
- // ensure _composeFieldCheckbox() "value" destructure happens.
4082
- expect(result.fields[0]).not.toHaveProperty('value');
4083
- });
4084
-
4085
- it('should ignore fields that are not present in the schema', () => {
4086
- const schemaBase = {
4087
- properties: {
4088
- feedback: {
4089
- title: 'Your feedback',
4090
- type: 'string',
4091
- presentation: {
4092
- inputType: 'textarea',
4093
- },
4094
- },
4095
- },
4096
- };
4097
-
4098
- const resultWithoutCustomProperties = createHeadlessForm(schemaBase);
4099
- const resultWithInvalidCustomProperty = createHeadlessForm(schemaBase, {
4100
- customProperties: {
4101
- unknown: {
4102
- 'data-foo': 'baz',
4103
- },
4104
- },
4105
- });
4106
-
4107
- function assertResultHasNoCustomizations(result) {
4108
- expect(result.fields).toHaveLength(1); // The "unknown" is not present
4109
- expect(result.fields[0].name).toBe('feedback');
4110
- expect(result.fields[0]).not.toHaveProperty('data-foo');
4111
- }
4112
-
4113
- assertResultHasNoCustomizations(resultWithoutCustomProperties);
4114
- assertResultHasNoCustomizations(resultWithInvalidCustomProperty);
4115
- });
4116
-
4117
- it('should handle custom properties when inside fieldsets', () => {
4118
- const result = createHeadlessForm(
4119
- JSONSchemaBuilder()
4120
- .addInput({
4121
- id_number: mockNumberInput,
4122
- })
4123
- .addInput({
4124
- fieldset: mockFieldset,
4125
- })
4126
- .addInput({ nestedFieldset: mockNestedFieldset })
4127
- .build(),
4128
- {
4129
- customProperties: {
4130
- id_number: { 'data-field': 'field' },
4131
- fieldset: {
4132
- customProperties: {
4133
- username: { 'data-fieldset': 'fieldset' },
4134
- },
4135
- },
4136
- nestedFieldset: {
4137
- customProperties: {
4138
- innerFieldset: {
4139
- customProperties: {
4140
- username: { 'data-nested-fieldset': 'nested-fieldset' },
4141
- },
4142
- },
4143
- },
4144
- },
4145
- },
4146
- }
4147
- );
4148
-
4149
- expect(result).toMatchObject({
4150
- fields: [
4151
- {
4152
- 'data-field': 'field',
4153
- name: 'id_number',
4154
- },
4155
- {
4156
- name: 'fieldset',
4157
- fields: [
4158
- {
4159
- name: 'username',
4160
- 'data-fieldset': 'fieldset',
4161
- },
4162
- {
4163
- name: 'tabs',
4164
- },
4165
- ],
4166
- },
4167
- {
4168
- name: 'nestedFieldset',
4169
- fields: [
4170
- {
4171
- name: 'innerFieldset',
4172
- fields: [
4173
- {
4174
- name: 'username',
4175
- 'data-nested-fieldset': 'nested-fieldset',
4176
- },
4177
- {
4178
- name: 'tabs',
4179
- },
4180
- ],
4181
- },
4182
- ],
4183
- },
4184
- ],
4185
- });
4186
-
4187
- const [fieldResult, fildsetResult, nestedFieldsetResult] = result.fields;
4188
-
4189
- // Sanity check that custom attrs are not "leaked" into other fields
4190
- // $.id_number
4191
- expect(fieldResult).toHaveProperty('name', 'id_number');
4192
- expect(fieldResult).toHaveProperty('data-field', 'field');
4193
- expect(fieldResult).not.toHaveProperty('data-fieldset');
4194
- expect(fieldResult).not.toHaveProperty('data-nested-fieldset');
4195
-
4196
- // $.fieldset.username
4197
- expect(fildsetResult.fields[0]).toHaveProperty('name', 'username');
4198
- expect(fildsetResult.fields[0]).toHaveProperty('data-fieldset', 'fieldset');
4199
- expect(fildsetResult.fields[0]).not.toHaveProperty('data-field');
4200
- expect(fildsetResult.fields[0]).not.toHaveProperty('data-nested-fieldset');
4201
- expect(fildsetResult.fields[1]).not.toHaveProperty('data-field');
4202
- expect(fildsetResult.fields[1]).not.toHaveProperty('data-nested-fieldset');
4203
-
4204
- // $.nestedFieldset.innerFieldset.id_number
4205
- expect(nestedFieldsetResult.fields[0].fields[0]).toHaveProperty('name', 'username');
4206
- expect(nestedFieldsetResult.fields[0].fields[0]).toHaveProperty(
4207
- 'data-nested-fieldset',
4208
- 'nested-fieldset'
4209
- );
4210
- expect(nestedFieldsetResult.fields[0].fields[0]).not.toHaveProperty('data-field');
4211
- expect(nestedFieldsetResult.fields[0].fields[0]).not.toHaveProperty('data-fieldset');
4212
- expect(nestedFieldsetResult.fields[0].fields[1]).not.toHaveProperty('data-field');
4213
- expect(nestedFieldsetResult.fields[0].fields[1]).not.toHaveProperty('data-fieldset');
4214
- });
4215
- it('should handle custom properties when inside fieldsets for fields name clashing with reserved words', () => {
4216
- const { fields } = createHeadlessForm(
4217
- {
4218
- properties: {
4219
- dog: {
4220
- title: 'Dog details',
4221
- description: 'Fieldset description',
4222
- 'x-jsf-presentation': {
4223
- inputType: 'fieldset',
4224
- },
4225
- properties: {
4226
- name: {
4227
- // This fieldName (name) clashs with the field specs "name"
4228
- title: 'Dogs name',
4229
- 'x-jsf-presentation': {
4230
- inputType: 'text',
4231
- },
4232
- type: 'string',
4233
- },
4234
- type: {
4235
- // This field name (type) clashs with the field specs "type"
4236
- title: 'Breed type',
4237
- 'x-jsf-presentation': {
4238
- inputType: 'number',
4239
- },
4240
- type: 'string',
4241
- },
4242
- },
4243
- required: ['name'],
4244
- type: 'object',
4245
- },
4246
- },
4247
- required: ['dog'],
4248
- },
4249
- {
4250
- customProperties: {
4251
- dog: {
4252
- customProperties: {
4253
- name: {
4254
- description: "What's your dogs name",
4255
- },
4256
- },
4257
- },
4258
- },
4259
- }
4260
- );
4261
-
4262
- expect(fields.length).toBe(1);
4263
- expect(fields[0].fields.length).toBe(2);
4264
- expect(fields[0].fields[0].name).toBe('name');
4265
- expect(fields[0].fields[0].description).toBe("What's your dogs name");
4266
- });
4267
- });
4268
-
4269
- describe('presentation (deprecated in favor of x-jsf-presentation)', () => {
4270
- it('works well with position, description, inputType, and any other arbitrary attribute', () => {
4271
- const { fields } = createHeadlessForm({
4272
- properties: {
4273
- day: {
4274
- title: 'Date',
4275
- presentation: {
4276
- inputType: 'date',
4277
- position: 1,
4278
- foo: 'bar',
4279
- statement: {
4280
- description: 'ss',
4281
- },
4282
- },
4283
- },
4284
- time: {
4285
- title: 'Time',
4286
- presentation: {
4287
- inputType: 'clock',
4288
- description: 'Write in <b>hh:ss</b> format',
4289
- position: 0,
4290
- deprecated: {
4291
- description: 'In favor of X',
4292
- },
4293
- },
4294
- },
4295
- },
4296
- });
4297
-
4298
- // Assert order from presentation.position
4299
- expect(fields[0].name).toBe('time');
4300
- expect(fields[1].name).toBe('day');
4301
-
4302
- // Assert spreaded attributes
4303
- expect(fields).toMatchObject([
4304
- {
4305
- name: 'time',
4306
- description: 'Write in <b>hh:ss</b> format', // from presentation
4307
- inputType: 'clock', // arbitrary type from presentation
4308
- deprecated: {
4309
- description: 'In favor of X', // from presentation
4310
- },
4311
- },
4312
- {
4313
- name: 'day',
4314
- inputType: 'date', // arbitrary type from presentation
4315
- foo: 'bar', // spread from presentation
4316
- statement: {
4317
- // from presentation
4318
- description: 'ss',
4319
- },
4320
- },
4321
- ]);
4322
- });
4323
- });
4324
- });