UNPKG

@remoteoss/json-schema-form

Version:

Headless UI form powered by JSON Schemas

760 lines (698 loc) 22.3 kB
import { createHeadlessForm } from '@/createHeadlessForm'; describe('Conditional attributes updated', () => { it('Should allow check of a nested property in a conditional', () => { const { handleValidation } = createHeadlessForm( { additionalProperties: false, allOf: [ { if: { properties: { parent: { properties: { child: { const: 'yes', }, }, required: ['child'], }, }, required: ['parent'], }, then: { required: ['parent_sibling'] }, }, ], properties: { parent: { additionalProperties: false, properties: { child: { oneOf: [{ const: 'yes' }, { const: 'no' }], type: 'string', }, }, required: ['child'], type: 'object', }, parent_sibling: { type: 'integer', }, }, required: ['parent'], type: 'object', }, { strictInputType: false } ); expect(handleValidation({ parent: { child: 'no' } }).formErrors).toEqual(undefined); expect(handleValidation({ parent: { child: 'yes' } }).formErrors).toEqual({ parent_sibling: 'Required field', }); expect(handleValidation({ parent: { child: 'yes' }, parent_sibling: 1 }).formErrors).toEqual( undefined ); }); it('Update basic case with const, default, maximum', () => { const { fields, handleValidation } = createHeadlessForm( { properties: { is_full_time: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] }, hours: { type: 'number' }, }, allOf: [ { if: { properties: { is_full_time: { const: 'yes' } }, required: ['is_full_time'], }, then: { properties: { hours: { const: 8, default: 8, }, }, }, else: { properties: { hours: { maximum: 4, }, }, }, }, ], }, { strictInputType: false } ); // Given "Yes" it applies "const" and "default" expect(handleValidation({ is_full_time: 'yes', hours: 4 }).formErrors).toEqual({ hours: 'The only accepted value is 8.', }); expect(fields[1]).toMatchObject({ const: 8, default: 8 }); expect(fields[1].maximum).toBeUndefined(); // Changing to "No", applies the "maximum" and cleans "const" and "default" expect(handleValidation({ is_full_time: 'no', hours: 4 }).formErrors).toBeUndefined(); expect(fields[1]).toMatchObject({ maximum: 4 }); expect(fields[1].const).toBeUndefined(); expect(fields[1].default).toBeUndefined(); // Changing back to "Yes", it removes "maximum", and applies "const" and "default" expect(handleValidation({}).formErrors).toBeUndefined(); expect(handleValidation({ is_full_time: 'yes', hours: 8 }).formErrors).toBeUndefined(); expect(fields[1].maximum).toBeUndefined(); expect(fields[1]).toMatchObject({ const: 8, default: 8 }); }); it('Update a new attribute (eg description)', () => { const { fields, handleValidation } = createHeadlessForm( { properties: { is_full_time: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] }, hours: { type: 'number', }, }, allOf: [ { if: { properties: { is_full_time: { const: 'yes' } }, required: ['is_full_time'], }, then: { properties: { hours: { description: 'We recommend 8 hours.', }, }, }, }, ], }, { strictInputType: false } ); // By default the attribute is not set. expect(fields[1].description).toBeUndefined(); // Given "Yes" it applies the conditional attribute expect(handleValidation({ is_full_time: 'yes' }).formErrors).toBeUndefined(); expect(fields[1].description).toBe('We recommend 8 hours.'); // Changing to "No", removes the description expect(handleValidation({ is_full_time: 'no' }).formErrors).toBeUndefined(); expect(fields[1].description).toBeUndefined(); // Changing back to "Yes", it sets the attribute again expect(handleValidation({ is_full_time: 'yes' }).formErrors).toBeUndefined(); expect(fields[1].description).toBe('We recommend 8 hours.'); }); it('Update an existing attribute (eg description)', () => { const { fields, handleValidation } = createHeadlessForm( { properties: { is_full_time: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] }, hours: { type: 'number', description: 'Any value works.', }, }, allOf: [ { if: { properties: { is_full_time: { const: 'yes' } }, required: ['is_full_time'], }, then: { properties: { hours: { description: 'We recommend 8 hours.', }, }, }, }, ], }, { strictInputType: false } ); // By default the attribute is set the base value. expect(fields[1].description).toBe('Any value works.'); // Given "Yes" it applies the conditional attribute expect(handleValidation({ is_full_time: 'yes', hours: 4 }).formErrors).toBeUndefined(); expect(fields[1].description).toBe('We recommend 8 hours.'); // Changing to "No", it applies the base value again. expect(handleValidation({ is_full_time: 'no', hours: 4 }).formErrors).toBeUndefined(); expect(fields[1].description).toBe('Any value works.'); // Changing back to "Yes", it sets the attribute again expect(handleValidation({ is_full_time: 'yes', hours: 8 }).formErrors).toBeUndefined(); expect(fields[1].description).toBe('We recommend 8 hours.'); }); it('Update a nested attribute', () => { const { fields, handleValidation } = createHeadlessForm( { properties: { is_full_time: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] }, hours: { type: 'number', presentation: { inputType: 'number', anything: 'info', }, }, }, allOf: [ { if: { properties: { is_full_time: { const: 'yes' } }, required: ['is_full_time'], }, then: { properties: { hours: { presentation: { anything: 'danger', }, }, }, }, }, ], }, { strictInputType: false } ); // By default the attribute is set the base value. expect(fields[1].anything).toBe('info'); // Given "Yes" it applies the conditional attribute expect(handleValidation({ is_full_time: 'yes' }).formErrors).toBeUndefined(); expect(fields[1].anything).toBe('danger'); // Changing to "No", it applies the base value again. expect(handleValidation({ is_full_time: 'no' }).formErrors).toBeUndefined(); expect(fields[1].anything).toBe('info'); // Changing back to "Yes", it sets the attribute again expect(handleValidation({ is_full_time: 'yes' }).formErrors).toBeUndefined(); expect(fields[1].anything).toBe('danger'); }); it('Keeps existing attributes in matches that dont change the attr', () => { const { fields, handleValidation } = createHeadlessForm( { properties: { is_full_time: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] }, hours: { type: 'number', description: 'Any value works.', }, }, allOf: [ { if: { properties: { is_full_time: { const: 'yes' } }, required: ['is_full_time'], }, then: { required: ['hours'], }, else: { properties: { hours: false, }, }, }, ], }, { strictInputType: false } ); // By default the attribute is set the base value, even though the field is invisible. expect(fields[1].description).toBe('Any value works.'); expect(fields[1].isVisible).toBe(false); // Given "Yes" it keeps the base value expect(handleValidation({ is_full_time: 'yes' }).formErrors).toEqual({ hours: 'Required field', }); expect(fields[1].description).toBe('Any value works.'); expect(fields[1].isVisible).toBe(true); // Changing to "No" it keeps the base value expect(handleValidation({ is_full_time: 'no' }).formErrors).toBeUndefined(); expect(fields[1].description).toBe('Any value works.'); expect(fields[1].isVisible).toBe(false); }); it('Keeps internal attributes (dynamicInternalJsfAttrs)', () => { // This is necessary while we keep supporting "type", even if deprecated // otherwise our Remote app will break because it didn't migrate // from "type" to "inputType" yet. const { fields, handleValidation } = createHeadlessForm( { properties: { is_full_time: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] }, salary_period: { type: 'string', title: 'Salary period', oneOf: [ { title: 'Weekly', const: 'weekly' }, { title: 'Monthly', const: 'monthly' }, ], }, }, allOf: [ { if: { properties: { is_full_time: { const: 'yes' } }, required: ['is_full_time'], }, then: { properties: { salary_period: { description: 'We recommend montlhy.', }, }, }, }, ], }, { strictInputType: false } ); // Given "Yes" it keeps the "type" handleValidation({ is_full_time: 'yes' }); // All the following attrs are never removed // during conditionals because they are core. expect(fields[1]).toMatchObject({ name: 'salary_period', label: 'Salary period', required: false, type: 'radio', inputType: 'radio', jsonType: 'string', computedAttributes: {}, calculateConditionalProperties: expect.any(Function), schema: expect.any(Object), scopedJsonSchema: expect.any(Object), isVisible: true, options: [ { label: 'Weekly', value: 'weekly', }, { label: 'Monthly', value: 'monthly', }, ], }); }); it('Keeps custom attributes (dynamicInternalJsfAttrs) (hotfix temporary)', () => { // This is necessary as hotfix because we (Remote) use it internally. // Not cool, we'll need a better solution asap. const { fields, handleValidation } = createHeadlessForm( { properties: { is_full_time: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] }, salary_period: { type: 'string', title: 'Salary period', oneOf: [ { title: 'Weekly', const: 'weekly' }, { title: 'Monthly', const: 'monthly' }, ], }, }, allOf: [ { if: { properties: { is_full_time: { const: 'yes' } }, required: ['is_full_time'], }, then: { properties: { salary_period: { description: 'We recommend montlhy.', }, }, }, }, ], }, { strictInputType: false, customProperties: { salary_period: { Component: '<A React Component>', calculateDynamicProperties: () => true, }, }, } ); // It's there by default expect(fields[1].Component).toBe('<A React Component>'); expect(fields[1].calculateDynamicProperties).toEqual(expect.any(Function)); // Given "Yes", it stays there too. handleValidation({ is_full_time: 'yes' }); expect(fields[1].Component).toBe('<A React Component>'); expect(fields[1].calculateDynamicProperties).toEqual(expect.any(Function)); // Given "No", it stays there too. handleValidation({ is_full_time: 'no' }); expect(fields[1].Component).toBe('<A React Component>'); expect(fields[1].calculateDynamicProperties).toEqual(expect.any(Function)); expect(fields[1].visibilityCondition).toEqual(undefined); // visibilityCondition can be externally changed/updated/added, it stays there too; fields[1].visibilityCondition = () => false; handleValidation({ is_full_time: 'no' }); expect(fields[1].visibilityCondition).toEqual(expect.any(Function)); }); }); describe('Conditional with a minimum value check', () => { it('Should handle a maximum as a property field check', () => { const schema = { additionalProperties: false, allOf: [ { if: { properties: { salary: { maximum: 119999, }, }, required: ['salary'], }, then: { required: ['reason'], }, else: { properties: { reason: false, }, }, }, ], properties: { salary: { type: 'number', 'x-jsf-presentation': { inputType: 'money', }, }, reason: { oneOf: [ { const: 'reason_one', }, { const: 'reason_two', }, ], type: 'string', }, }, required: ['salary'], type: 'object', }; const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); expect(handleValidation({ salary: 120000 }).formErrors).toEqual(undefined); expect(handleValidation({ salary: 1000 }).formErrors).toEqual({ reason: 'Required field', }); expect(handleValidation({ salary: 1000, reason: 'reason_one' }).formErrors).toEqual(undefined); }); }); describe('Conditional with literal booleans', () => { it('handles true case', () => { const schema = { properties: { is_full_time: { type: 'boolean', }, salary: { type: 'number', }, }, required: [], if: true, then: { required: ['is_full_time'], }, else: { required: ['salary'], }, }; const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false }); handleValidation({}); expect(fields[0]).toMatchObject({ name: 'is_full_time', required: true, }); expect(fields[1]).toMatchObject({ name: 'salary', required: false, }); }); it('handles false case', () => { const schema = { properties: { is_full_time: { type: 'boolean', }, salary: { type: 'number', }, }, required: [], if: false, then: { required: ['is_full_time'], }, else: { required: ['salary'], }, }; const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false }); handleValidation({}); expect(fields[0]).toMatchObject({ name: 'is_full_time', required: false, }); expect(fields[1]).toMatchObject({ name: 'salary', required: true, }); }); }); describe('Conditional with anyOf', () => { const schema = { additionalProperties: false, type: 'object', properties: { field_a: { type: 'string' }, field_b: { type: 'string' }, field_c: { type: 'string' }, }, allOf: [ { if: { anyOf: [ { properties: { field_a: { const: '1' } }, required: ['field_a'] }, { properties: { field_b: { const: '2' } }, required: ['field_b'] }, ], }, then: { required: ['field_c'], }, else: { properties: { field_c: false, }, }, }, ], }; it('handles true case', () => { const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false }); expect(fields[2].isVisible).toBe(false); expect(handleValidation({ field_a: 'x', field_b: '2' }).formErrors).toEqual({ field_c: 'Required field', }); expect(fields[2].isVisible).toBe(true); }); it('handles false case', () => { const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false }); expect(fields[2].isVisible).toBe(false); expect(handleValidation({ field_a: 'x', field_b: 'x' }).formErrors).toBeUndefined(); expect(fields[2].isVisible).toBe(false); }); }); describe('Conditionals - bugs and code-smells', () => { // Why do we have these bugs? // To be honest we never realized it much later later. // We will fix them in the next major version. const schemaHasPet = { type: 'object', additionalProperties: false, properties: { has_pet: { title: 'Has Pet', description: 'Do you have a pet?', oneOf: [ { title: 'Yes', const: 'yes' }, { title: 'No', const: 'no' }, ], type: 'string', }, pet_name: { title: "Pet's name", type: 'string', }, }, required: ['has_pet'], allOf: [ { if: { properties: { has_pet: { const: 'yes' }, }, required: ['has_pet'], }, then: { required: ['pet_name'], }, else: { properties: { pet_name: false, }, }, }, ], }; it('Given values from hidden fields, it does not thrown an error (@bug)', () => { const { fields, handleValidation } = createHeadlessForm(schemaHasPet, { strictInputType: false, }); const petNameField = fields[1]; const validation = handleValidation({ has_pet: 'no', pet_name: 'Max' }); expect(petNameField.isVisible).toBe(false); // Bug: 🐛 It does not thrown an error, // but it should to be compliant with JSON Schema specs. expect(validation.formErrors).toBeUndefined(); // The error should be something like: // expect(validation.formErrors).toEqual({ pet_name: 'Not allowed.'}); }); it('Given values from hidden fields, it mutates the values (@bug)', () => { const { handleValidation } = createHeadlessForm(schemaHasPet, { strictInputType: false, }); const newValues = { has_pet: 'no', pet_name: 'Max' }; const validation = handleValidation(newValues); expect(newValues).toEqual({ has_pet: 'no', pet_name: null, // BUG! 🐛 Should still be "Max", should not be mutated. }); // Same bug as explained in the previous test. expect(validation.formErrors).toBeUndefined(); }); it('Given multiple conditionals to the same field, it only applies the last one (@bug) - case 1', () => { const { handleValidation } = createHeadlessForm( { additionalProperties: false, properties: { field_a: { type: 'string' }, field_b: { type: 'string' }, field_c: { type: 'number' }, }, allOf: [ { if: { properties: { field_a: { const: 'yes' } }, required: ['field_a'], }, then: { properties: { field_c: { minimum: 30 } } }, }, { if: { properties: { field_b: { const: 'yes' } }, required: ['field_b'], }, then: { properties: { field_c: { minimum: 10 } } }, }, ], }, { strictInputType: false, } ); const validation = handleValidation({ field_a: 'yes', field_b: 'yes', field_c: 5 }); expect(validation.formErrors).toEqual({ // BUG: 🐛 it should be "Must be greater or equal to 30" field_c: 'Must be greater or equal to 10', }); }); it('Given multiple conditionals to the same field, it only applies the last one (@bug) - case 2', () => { const { handleValidation } = createHeadlessForm( { additionalProperties: false, properties: { field_a: { type: 'string' }, field_b: { type: 'string' }, field_c: { type: 'number' }, }, allOf: [ { if: { properties: { field_a: { const: 'yes' } }, required: ['field_a'], }, then: { properties: { field_c: { minimum: 5 } } }, }, { if: { properties: { field_b: { const: 'yes' } }, required: ['field_b'], }, then: { properties: { field_c: { maximum: 10 } } }, }, ], }, { strictInputType: false, } ); const validation1 = handleValidation({ field_a: 'yes', field_b: 'yes', field_c: 12 }); expect(validation1.formErrors).toEqual({ field_c: 'Must be smaller or equal to 10', }); const validation2 = handleValidation({ field_a: 'yes', field_b: 'yes', field_c: 3 }); // BUG: 🐛 it should be "Must be greater or equal to 5" expect(validation2.formErrors).toBeUndefined(); // expect(validation1.formErrors).toEqual({ // field_c: 'Must be greater or equal to 5', // }); }); });