diff --git a/src/process/validation/rules/__tests__/fixtures/components.ts b/src/process/validation/rules/__tests__/fixtures/components.ts index 16c19723..f39f5137 100644 --- a/src/process/validation/rules/__tests__/fixtures/components.ts +++ b/src/process/validation/rules/__tests__/fixtures/components.ts @@ -204,6 +204,14 @@ export const simpleRadioField: RadioComponent = { input: true, }; +export const simpleCheckBoxField = { + label: 'Checkbox', + tableView: true, + key: 'component', + type: 'checkbox', + input: true, +}; + export const hiddenRequiredField: HiddenComponent = { type: 'hidden', key: 'someData', @@ -234,4 +242,4 @@ export const requiredNonInputField: any = { validate: { required: true } -}; \ No newline at end of file +}; diff --git a/src/process/validation/rules/__tests__/validateRequired.test.ts b/src/process/validation/rules/__tests__/validateRequired.test.ts index 7e57b2a9..0ae84619 100644 --- a/src/process/validation/rules/__tests__/validateRequired.test.ts +++ b/src/process/validation/rules/__tests__/validateRequired.test.ts @@ -2,10 +2,18 @@ import { expect } from 'chai'; import { FieldError } from 'error'; import { validateRequired } from '../validateRequired'; -import { conditionallyHiddenRequiredHiddenField, hiddenRequiredField, requiredNonInputField, simpleTextField } from './fixtures/components'; +import { + conditionallyHiddenRequiredHiddenField, + hiddenRequiredField, + requiredNonInputField, + simpleTextField, + simpleSelectBoxes, + simpleRadioField, + simpleCheckBoxField, +} from './fixtures/components'; import { processOne } from 'processes/processOne'; import { generateProcessorContext } from './fixtures/util'; -import { ProcessorsContext, ValidationScope } from 'types'; +import { ProcessorsContext, SelectBoxesComponent, ValidationScope } from 'types'; import { validateAllProcess, validateProcessInfo } from 'processes/validation'; it('Validating a simple component that is required and not present in the data will return a field error', async () => { @@ -116,7 +124,7 @@ it('Should not validate a non input comonent', async () => { expect(context.scope.errors.length).to.equal(0); }); -it('Should validate a conditionally hidden compoentn with validateWhenHidden flag set to true', async () => { +it('Should validate a conditionally hidden component with validateWhenHidden flag set to true', async () => { const component = {...simpleTextField}; component.validate = { required: true }; component.validateWhenHidden = true; @@ -132,3 +140,66 @@ it('Should validate a conditionally hidden compoentn with validateWhenHidden fla expect(context.scope.errors.length).to.equal(1); expect(context.scope.errors[0] && context.scope.errors[0].errorKeyOrMessage).to.equal('required'); }); + +it('Validating a simple radio component that is required and present in the data with value set to false will return null', async () => { + const component = { ...simpleRadioField, validate: { required: true }, values: [ + { + label: 'Yes', + value: 'true', + }, + { + label: 'No', + value: 'false', + }] }; + const data = { component: false }; + const context = generateProcessorContext(component, data); + const result = await validateRequired(context); + expect(result).to.equal(null); +}); + + +it('Validating a simple selectbox that is required and present in the data with value set to 0 will return null', async () => { + const component = { ...simpleSelectBoxes, validate: { required: true }, values: [ + { + label: 'true', + value: 'true', + }, + { + label: 'Null', + value: '0', + }] }; + const data = { component: 0 }; + const context = generateProcessorContext(component, data); + const result = await validateRequired(context); + expect(result).to.equal(null); +}); + +it('Validating a simple selectbox that is required and present in the data with value set to false will return a FieldError', async () => { + const component: SelectBoxesComponent = { ...simpleSelectBoxes, validate: { required: true }, values: [ + { + label: 'true', + value: 'true', + }, + { + label: 'false', + value: 'false', + }] + }; + const data = { + component: { + true: false, + false: false + } + }; + const context = generateProcessorContext(component, data); + const result = await validateRequired(context); + expect(result).to.be.instanceOf(FieldError); +}); + +it('Validating a simple checkbox that is required and present in the data with value set to false will return a FieldError', async () => { + const component = { ...simpleCheckBoxField, validate: { required: true } }; + const data = { component: false }; + const context = generateProcessorContext(component, data); + const result = await validateRequired(context); + expect(result).to.be.instanceOf(FieldError); +}); diff --git a/src/process/validation/rules/validateRequired.ts b/src/process/validation/rules/validateRequired.ts index 565d2219..96fb872e 100644 --- a/src/process/validation/rules/validateRequired.ts +++ b/src/process/validation/rules/validateRequired.ts @@ -1,15 +1,57 @@ import { FieldError } from 'error'; -import { AddressComponentDataObject, RuleFn, RuleFnSync, ValidationContext } from 'types'; +import { + AddressComponentDataObject, + RuleFn, + RuleFnSync, + ValidationContext, + AddressComponent, + DayComponent +} from 'types'; import { isEmptyObject } from '../util'; import { ProcessorInfo } from 'types/process/ProcessorInfo'; +const isAddressComponent = (component: any): component is AddressComponent => { + return component.type === 'address'; +} + +const isDayComponent = (component: any): component is DayComponent => { + return component.type === 'day'; +} + const isAddressComponentDataObject = (value: any): value is AddressComponentDataObject => { return value !== null && typeof value === 'object' && value.mode && value.address && typeof value.address === 'object'; } +// Checkboxes and selectboxes consider false to be falsy, whereas other components with +// settable values (e.g. radio, select, datamap, container, etc.) consider it truthy +const isComponentThatCannotHaveFalseValue = (component: any): boolean => { + return component.type === 'checkbox' || component.type === 'selectboxes' +} + +const valueIsPresent = (value: any, considerFalseTruthy: boolean): boolean => { + // Evaluate for 3 out of 6 falsy values ("", null, undefined), don't check for 0 + // and only check for false under certain conditions + if (value === null || value === undefined || value === "" || (!considerFalseTruthy && value === false)) { + return false; + } + // Evaluate for empty object + else if (isEmptyObject(value)) { + return false; + } + // Evaluate for empty array + else if (Array.isArray(value) && value.length === 0) { + return false; + } + // Recursively evaluate + else if (typeof value === 'object') { + return Object.values(value).some((val) => valueIsPresent(val, considerFalseTruthy)); + } + return true; +} + export const shouldValidate = (context: ValidationContext) => { const { component } = context; - if (component.validate?.required) { + if (component.validate?.required && !component.hidden) { return true; } return false; @@ -25,25 +67,16 @@ export const validateRequiredSync: RuleFnSync = (context: ValidationContext) => if (!shouldValidate(context)) { return null; } - if ( - (value === null || value === undefined || isEmptyObject(value) || (!!value === false && value !== 0)) && - !component.hidden - ) { - return error; - } - else if (Array.isArray(value) && value.length === 0) { - return error; - } - else if (component.type === 'address' && isAddressComponentDataObject(value)) { + if (isAddressComponent(component) && isAddressComponentDataObject(value)) { return isEmptyObject(value.address) ? error : Object.values(value.address).every((val) => !!val) ? null : error; } - else if (component.type === 'day' && value === '00/00/0000') { + else if (isDayComponent(component) && value === '00/00/0000') { return error; } - else if (typeof value === 'object' && value !== null) { - return Object.values(value).some((val) => !!val) ? null : error; + else if (isComponentThatCannotHaveFalseValue(component)) { + return !valueIsPresent(value, false) ? error : null; } - return null; + return !valueIsPresent(value, true) ? error : null; }; export const validateRequiredInfo: ProcessorInfo = { diff --git a/src/types/Component.ts b/src/types/Component.ts index c050eb8d..63ebfebe 100644 --- a/src/types/Component.ts +++ b/src/types/Component.ts @@ -328,16 +328,30 @@ export type ListComponent = BaseComponent & { valueProperty?: string; }; -export type RadioComponent = ListComponent & { +type StaticValuesRadioComponent = ListComponent & { values: { label: string; value: string; shortcut?: string }[]; - data?: { - url?: string; + dataSrc?: "values"; + fieldSet?: boolean; + optionsLabelPosition?: string; + inline?: boolean; +}; + +type UrlValuesRadioComponent = ListComponent & { + data: { + url: string; + headers: { + key: string; + value: string; + }[]; }; + dataSrc: 'url'; fieldSet?: boolean; optionsLabelPosition?: string; inline?: boolean; }; +export type RadioComponent = StaticValuesRadioComponent | UrlValuesRadioComponent; + export type RecaptchaComponent = BaseComponent; type StaticValuesSelectData = {