@remoteoss/json-schema-form 0.11.11-dev.20250220164730 → 1.0.0-alpha.1

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