UNPKG

@remoteoss/json-schema-form

Version:

Headless UI form powered by JSON Schemas

804 lines (729 loc) 22.8 kB
import merge from 'lodash/fp/merge'; import { JSONSchemaBuilder, mockFieldset, mockRadioInputString } from './helpers'; import { mockMoneyInput } from './helpers.custom'; import { createHeadlessForm } from '@/createHeadlessForm'; function friendlyError({ formErrors }) { // destruct the formErrors directly return formErrors; } export const mockNumberInput = { title: 'Tabs', description: 'How many open tabs do you have?', 'x-jsf-presentation': { inputType: 'number', }, minimum: 5, maximum: 30, type: 'number', }; export const mockNumberInputDeprecatedPresentation = { title: 'Tabs', description: 'How many open tabs do you have?', presentation: { inputType: 'number', }, minimum: 5, maximum: 30, type: 'number', }; const schemaBasic = ({ newProperties, allOf } = {}) => JSONSchemaBuilder() .addInput( merge( { parent_age: { ...mockNumberInput, maximum: 100 }, child_age: mockNumberInput, }, newProperties ) ) .setRequiredFields(['parent_age']) .addAllOf(allOf || []) .build(); const schemaWithConditional = ({ newProperties } = {}) => JSONSchemaBuilder() .addInput( merge( { is_employee: mockRadioInputString, salary: { ...mockMoneyInput, minimum: 0 }, bonus: { ...mockMoneyInput, minimum: 0 }, }, newProperties ) ) .setRequiredFields(['is_employee', 'salary']) .addAllOf([ { if: { properties: { is_employee: { const: 'yes', }, }, required: ['is_employee'], }, then: { properties: { salary: { minimum: 100000, // 1000.00€ }, }, required: ['bonus'], }, else: { properties: { salary: { minimum: 0, // 0.00€ }, bonus: false, }, }, }, ]) .build(); function validateFieldParams(fieldParams, newFieldParams) { expect(newFieldParams).toHaveProperty('name', fieldParams.name); expect(newFieldParams).toHaveProperty('label', fieldParams.title); expect(newFieldParams).toHaveProperty('description', fieldParams.description); if (fieldParams.minimum) { expect(newFieldParams).toHaveProperty('minimum', fieldParams.minimum); } if (fieldParams.maximum) { expect(newFieldParams).toHaveProperty('maximum', fieldParams.maximum); } } function validateNumberParams(fieldParams, newFieldParams) { validateFieldParams(fieldParams, newFieldParams); expect(newFieldParams).toHaveProperty('inputType', 'number'); expect(newFieldParams).toHaveProperty('jsonType', 'number'); } function validateMoneyParams(fieldParams, newFieldParams) { validateFieldParams(fieldParams, newFieldParams); expect(newFieldParams).toHaveProperty('inputType', 'money'); expect(newFieldParams).toHaveProperty('jsonType', 'integer'); } function createScenario({ schema, config }) { const form = createHeadlessForm(schema, config); const validateForm = (vals) => friendlyError(form.handleValidation(vals)); return { ...form, validateForm, }; } beforeAll(() => { jest.spyOn(console, 'warn').mockImplementation(() => {}); }); afterEach(() => { // safety-check that every mocked validation is within the range // eslint-disable-next-line no-console expect(console.warn).not.toHaveBeenCalled(); }); afterAll(() => { // eslint-disable-next-line no-console console.warn.mockRestore(); }); // @deprecated - customProperties won't be supported in v2. describe('createHeadlessForm() - custom validations (deprecated)', () => { describe('simple validation (eg maximum)', () => { it('works as a number', () => { const { fields, validateForm } = createScenario({ schema: schemaBasic(), config: { customProperties: { child_age: { maximum: 14, }, }, }, }); validateNumberParams({ ...mockNumberInput, name: 'child_age', maximum: 14 }, fields[1]); expect(validateForm({})).toEqual({ parent_age: 'Required field', }); expect(validateForm({ parent_age: 30, child_age: 15 })).toEqual({ child_age: 'Must be smaller or equal to 14', }); expect(validateForm({ parent_age: 30, child_age: 10 })).toBeUndefined(); }); it('works as a function', () => { // Friendly Scenario: child_age must be smaller than parent_age. const { fields, validateForm } = createScenario({ schema: schemaBasic(), config: { customProperties: { child_age: { maximum: (values, { maximum }) => values.parent_age || maximum, }, }, }, }); validateNumberParams( { ...mockNumberInput, name: 'child_age', maximum: undefined }, fields[1] ); expect(validateForm({})).toEqual({ parent_age: 'Required field', }); expect(validateForm({ parent_age: 25, child_age: 26 })).toEqual({ child_age: 'Must be smaller or equal to 25', }); expect(validateForm({ parent_age: 25, child_age: 20 })).toBeUndefined(); }); it('works with minimum and maximum together', () => { const { fields, validateForm } = createScenario({ schema: schemaBasic(), config: { customProperties: { child_age: { // dumb example: parents that are less than double the child age, // the child must be between 20 and 29yo. minimum: (values, { minimum }) => values.parent_age < values.child_age * 3 ? 20 : minimum, maximum: (values, { maximum }) => values.parent_age < values.child_age * 3 ? 29 : maximum, }, }, }, }); validateNumberParams( { ...mockNumberInput, name: 'child_age', minimum: 5, maximum: 30 }, fields[1] ); // Test the default validations expect(validateForm({ parent_age: 50, child_age: 1 })).toEqual({ child_age: 'Must be greater or equal to 5', }); expect(validateForm({ parent_age: 100, child_age: 31 })).toEqual({ child_age: 'Must be smaller or equal to 30', }); // Test the custom validations expect(validateForm({ parent_age: 35, child_age: 19 })).toEqual({ child_age: 'Must be greater or equal to 20', }); expect(validateForm({ parent_age: 40, child_age: 31 })).toEqual({ child_age: 'Must be smaller or equal to 29', }); }); it('works with negative values', () => { const { fields, validateForm } = createScenario({ schema: schemaBasic({ newProperties: { parent_age: { minimum: -20, maximum: -1, }, }, }), config: { customProperties: { parent_age: { minimum: -15, maximum: -5, }, }, }, }); validateNumberParams( { ...mockNumberInput, name: 'parent_age', minimum: -15, maximum: -5 }, fields[0] ); expect(validateForm({})).toEqual({ parent_age: 'Required field', }); expect(validateForm({ parent_age: -20 })).toEqual({ parent_age: 'Must be greater or equal to -15', }); expect(validateForm({ parent_age: -4 })).toEqual({ parent_age: 'Must be smaller or equal to -5', }); expect(validateForm({ parent_age: -10 })).toBeUndefined(); }); it('keeps original validation, given an empty validation', () => { const { fields, validateForm } = createScenario({ schema: schemaBasic(), config: { customProperties: { parent_age: {}, }, }, }); validateNumberParams({ ...mockNumberInput, name: 'parent_age', maximum: 100 }, fields[0]); expect(validateForm({})).toEqual({ parent_age: 'Required field', }); expect(validateForm({ parent_age: 0 })).toEqual({ parent_age: 'Must be greater or equal to 5', }); }); it('applies validation, when original does not exist', () => { const { fields, validateForm } = createScenario({ schema: schemaBasic({ newProperties: { parent_age: { minimum: null, maximum: null }, }, }), config: { customProperties: { parent_age: { minimum: 1, maximum: 20, }, }, }, }); validateNumberParams( { ...mockNumberInput, minimum: 1, maximum: 20, name: 'parent_age' }, fields[0] ); expect(validateForm({})).toEqual({ parent_age: 'Required field', }); expect(validateForm({ parent_age: 0 })).toEqual({ parent_age: 'Must be greater or equal to 1', }); expect(validateForm({ parent_age: 21 })).toEqual({ parent_age: 'Must be smaller or equal to 20', }); }); }); describe('in fieldsets', () => { it('applies custom validation in nested fields', () => { const { fields, validateForm } = createScenario({ schema: JSONSchemaBuilder() .addInput({ animal_age: mockNumberInput, second_gen: { ...mockFieldset, properties: { cub_age: mockNumberInput, third_gen: { ...mockFieldset, properties: { grandcub_age: mockNumberInput, }, }, }, }, }) .build(), config: { customProperties: { animal_age: { minimum: 24, maximum: 28, }, second_gen: { customProperties: { cub_age: { minimum: 18, maximum: 21, }, third_gen: { customProperties: { grandcub_age: { minimum: 10, maximum: 15, }, }, }, }, }, }, }, }); const [animalField, secondGenField] = fields; // Assert custom validations validateNumberParams( { ...mockNumberInput, name: 'animal_age', minimum: 24, maximum: 28, required: false, }, animalField ); validateNumberParams( { ...mockNumberInput, name: 'cub_age', minimum: 18, maximum: 21, required: false, }, secondGenField.fields[0] ); validateNumberParams( { ...mockNumberInput, name: 'grandcub_age', minimum: 10, maximum: 15, required: false, }, secondGenField.fields[1].fields[0] ); // Assert minimum values expect( validateForm({ animal_age: 1, second_gen: { cub_age: 1, third_gen: { grandcub_age: 1, }, }, }) ).toEqual({ animal_age: 'Must be greater or equal to 24', second_gen: { cub_age: 'Must be greater or equal to 18', third_gen: { grandcub_age: 'Must be greater or equal to 10', }, }, }); // Assert maximum values expect( validateForm({ animal_age: 100, second_gen: { cub_age: 100, third_gen: { grandcub_age: 100, }, }, }) ).toEqual({ animal_age: 'Must be smaller or equal to 28', second_gen: { cub_age: 'Must be smaller or equal to 21', third_gen: { grandcub_age: 'Must be smaller or equal to 15', }, }, }); }); }); describe('in conditional fields', () => { const { fields, validateForm } = createScenario({ schema: schemaWithConditional(), config: { customProperties: { bonus: { maximum: (values, { maximum }) => ({ maximum: values.salary ? values.salary * 2 : maximum, 'x-jsf-errorMessage': { maximum: `The bonus cannot be twice of the salary ${values.salary}.`, }, }), }, }, }, }); it('validates conditional visible field', () => { // bonus fieldResult validateMoneyParams( { ...mockMoneyInput, name: 'bonus', minimum: 0, maximum: 500000, required: false, }, fields[2] ); // Basic path — the custom validation is triggered expect( validateForm({ is_employee: 'yes', salary: 150000, bonus: 310000, }) ).toEqual({ bonus: 'The bonus cannot be twice of the salary 150000.' }); // The values are valid: expect( validateForm({ is_employee: 'yes', salary: 150000, bonus: 20000, }) ).toBeUndefined(); expect(validateForm({ is_employee: 'yes', salary: 150000 })).toEqual({ bonus: 'Required field', }); }); it('ignores validation to conditional hidden field', () => { expect( validateForm({ is_employee: 'no', salary: 150000, bonus: 310000, // NOTE/Unrelated-bug: Should it throw an error saying this // "bonus" value is not expected? the native json schema spec throw an error... }) ).toBeUndefined(); }); it('given an out-of-range validation, logs warning', () => { expect( validateForm({ is_employee: 'yes', salary: 300000, bonus: 500100, }) ).toEqual({ bonus: 'No more than €5000.00', }); // eslint-disable-next-line no-console expect(console.warn).toHaveBeenNthCalledWith( 1, 'Custom validation for bonus is not allowed because maximum:600000 is less strict than the original range: 0 to 500000' ); // eslint-disable-next-line no-console console.warn.mockClear(); }); }); // TODO: delete after migration to x-jsf-errorMessage is completed describe('with errorMessage (deprecated)', () => { /* NOTE: We have 3 type of errors: - original error: (created by json-schema-form) - errorMessage: (declared on JSON Schema) - customValidation.errorMessage: (declared on config) */ it('overrides original error conditionally', () => { const { fields, validateForm } = createScenario({ schema: schemaBasic(), config: { customProperties: { child_age: { maximum: (values, { maximum }) => ({ maximum: values.parent_age || maximum, errorMessage: { maximum: `The child cannot be older than the parent of ${values.parent_age} yo.`, }, }), }, }, }, }); validateNumberParams( { ...mockNumberInput, name: 'child_age', minimum: 5, maximum: 30, }, fields[1] ); expect(validateForm({ parent_age: 18, child_age: 4 })).toEqual({ child_age: 'Must be greater or equal to 5', // applies the original error message }); expect(validateForm({ parent_age: 18, child_age: 19 })).toEqual({ child_age: 'The child cannot be older than the parent of 18 yo.', // applies the config.errorMessage }); }); it('overrides errorMessage conditionally', () => { const { fields, validateForm } = createScenario({ schema: schemaBasic({ newProperties: { parent_age: { maximum: 100, }, child_age: { maximum: 40, errorMessage: { maximum: 'The child cannot be older than 40yo.', }, }, }, }), config: { customProperties: { child_age: { minimum: (values, { maximum }) => { const minimumAge = values.parent_age / 2; if ( maximum > minimumAge && // prevent invalid out-of-range maximum values.parent_age > values.child_age * 2 // parent is 2x as big as child age ) { return { minimum: minimumAge, errorMessage: { minimum: `The child cannot be younger than half of the parent. Must be at least ${minimumAge}yo.`, }, }; } return null; }, }, }, }, }); validateNumberParams( { ...mockNumberInput, name: 'child_age', minimum: 5, maximum: 40, }, fields[1] ); // applies the errorMessage by default expect(validateForm({ parent_age: 50, child_age: 45 })).toEqual({ child_age: 'The child cannot be older than 40yo.', }); // applies the config.errorMessage if it's triggered expect(validateForm({ parent_age: 50, child_age: 10 })).toEqual({ child_age: `The child cannot be younger than half of the parent. Must be at least 25yo.`, }); }); }); describe('with x-jsf-errorMessage', () => { /* NOTE: We have 3 type of errors: - original error: (created by json-schema-form) - x-jsf-errorMessage: (declared on JSON Schema) - customValidation['x-jsf-errorMessage']: (declared on options) */ it('overrides original error conditionally', () => { const { fields, validateForm } = createScenario({ schema: schemaBasic(), config: { customProperties: { child_age: { maximum: (values, { maximum }) => ({ maximum: values.parent_age || maximum, 'x-jsf-errorMessage': { maximum: `The child cannot be older than the parent of ${values.parent_age} yo.`, }, }), }, }, }, }); validateNumberParams( { ...mockNumberInput, name: 'child_age', minimum: 5, maximum: 30, }, fields[1] ); expect(validateForm({ parent_age: 18, child_age: 4 })).toEqual({ child_age: 'Must be greater or equal to 5', // applies the original error message }); expect(validateForm({ parent_age: 18, child_age: 19 })).toEqual({ child_age: 'The child cannot be older than the parent of 18 yo.', // applies the config.errorMessage }); }); it('overrides errorMessage conditionally', () => { const { fields, validateForm } = createScenario({ schema: schemaBasic({ newProperties: { parent_age: { maximum: 100, }, child_age: { maximum: 40, 'x-jsf-errorMessage': { maximum: 'The child cannot be older than 40yo.', }, }, }, }), config: { customProperties: { child_age: { minimum: (values, { maximum }) => { const minimumAge = values.parent_age / 2; if ( maximum > minimumAge && // prevent invalid out-of-range maximum values.parent_age > values.child_age * 2 // parent is 2x as big as child age ) { return { minimum: minimumAge, 'x-jsf-errorMessage': { minimum: `The child cannot be younger than half of the parent. Must be at least ${minimumAge}yo.`, }, }; } return null; }, }, }, }, }); validateNumberParams( { ...mockNumberInput, name: 'child_age', minimum: 5, maximum: 40, }, fields[1] ); // applies the errorMessage by default expect(validateForm({ parent_age: 50, child_age: 45 })).toEqual({ child_age: 'The child cannot be older than 40yo.', }); // applies the config.errorMessage if it's triggered expect(validateForm({ parent_age: 50, child_age: 10 })).toEqual({ child_age: `The child cannot be younger than half of the parent. Must be at least 25yo.`, }); }); }); describe('invalid validations', () => { it('outside the schema range logs warning', () => { const { fields, validateForm } = createScenario({ schema: schemaBasic(), config: { customProperties: { parent_age: { minimum: 0, }, }, }, }); validateNumberParams( { ...mockNumberInput, minimum: 5, maximum: 100, name: 'parent_age' }, fields[0] ); // Keeps the default validation expect(validateForm({ parent_age: 0 })).toEqual({ parent_age: 'Must be greater or equal to 5', }); // eslint-disable-next-line no-console expect(console.warn).toHaveBeenNthCalledWith( 1, 'Custom validation for parent_age is not allowed because minimum:0 is less strict than the original range: 5 to 100' ); // eslint-disable-next-line no-console console.warn.mockClear(); }); it('null or undefined ignores validation', () => { const { fields, validateForm } = createScenario({ schema: schemaBasic(), config: { customProperties: { parent_age: { minimum: undefined, maximum: null, }, }, }, }); // The original validation is kept validateNumberParams( { ...mockNumberInput, minimum: 5, maximum: 100, name: 'parent_age' }, fields[0] ); expect(validateForm({ parent_age: 0 })).toEqual({ parent_age: 'Must be greater or equal to 5', }); expect(validateForm({ parent_age: 200 })).toEqual({ parent_age: 'Must be smaller or equal to 100', }); }); }); });