diff --git a/CHANGELOG.md b/CHANGELOG.md index a84feb1b99..04c6b8d1aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ should change the heading of the (upcoming) version to include a major version b --> +# 5.24.0 + +## @rjsf/utils + +- Fixed issue with formData not updating when dependencies change, fixing [#4325](https://github.com/rjsf-team/react-jsonschema-form/issues/4325) + # 5.23.1 ## @rjsf/chakra-ui diff --git a/packages/utils/src/mergeDefaultsWithFormData.ts b/packages/utils/src/mergeDefaultsWithFormData.ts index 5ffe657725..7704518172 100644 --- a/packages/utils/src/mergeDefaultsWithFormData.ts +++ b/packages/utils/src/mergeDefaultsWithFormData.ts @@ -2,6 +2,7 @@ import get from 'lodash/get'; import isObject from './isObject'; import { GenericObjectType } from '../src'; +import { isNil } from 'lodash'; /** Merges the `defaults` object of type `T` into the `formData` of type `T` * @@ -19,47 +20,69 @@ import { GenericObjectType } from '../src'; * @param [formData] - The form data into which the defaults will be merged * @param [mergeExtraArrayDefaults=false] - If true, any additional default array entries are appended onto the formData * @param [defaultSupercedesUndefined=false] - If true, an explicit undefined value will be overwritten by the default value + * @param [overrideFormDataWithDefaults=false] - If true, the default value will overwrite the form data value. If the value + * doesn't exist in the default, we take it from formData and in the case where the value is set to undefined in formData. + * This is useful when we have already merged formData with defaults and want to add an additional field from formData + * that does not exist in defaults. * @returns - The resulting merged form data with defaults */ export default function mergeDefaultsWithFormData( defaults?: T, formData?: T, mergeExtraArrayDefaults = false, - defaultSupercedesUndefined = false + defaultSupercedesUndefined = false, + overrideFormDataWithDefaults = false ): T | undefined { if (Array.isArray(formData)) { const defaultsArray = Array.isArray(defaults) ? defaults : []; - const mapped = formData.map((value, idx) => { - if (defaultsArray[idx]) { + + // If overrideFormDataWithDefaults is true, we want to override the formData with the defaults + const overrideArray = overrideFormDataWithDefaults ? defaultsArray : formData; + const overrideOppositeArray = overrideFormDataWithDefaults ? formData : defaultsArray; + + const mapped = overrideArray.map((value, idx) => { + if (overrideOppositeArray[idx]) { return mergeDefaultsWithFormData( defaultsArray[idx], - value, + formData[idx], mergeExtraArrayDefaults, - defaultSupercedesUndefined + defaultSupercedesUndefined, + overrideFormDataWithDefaults ); } return value; }); + // Merge any extra defaults when mergeExtraArrayDefaults is true - if (mergeExtraArrayDefaults && mapped.length < defaultsArray.length) { - mapped.push(...defaultsArray.slice(mapped.length)); + // Or when overrideFormDataWithDefaults is true and the default array is shorter than the formData array + if ((mergeExtraArrayDefaults || overrideFormDataWithDefaults) && mapped.length < overrideOppositeArray.length) { + mapped.push(...overrideOppositeArray.slice(mapped.length)); } return mapped as unknown as T; } if (isObject(formData)) { const acc: { [key in keyof T]: any } = Object.assign({}, defaults); // Prevent mutation of source object. return Object.keys(formData as GenericObjectType).reduce((acc, key) => { + const keyValue = get(formData, key); + const keyExistsInDefaults = isObject(defaults) && key in (defaults as GenericObjectType); + const keyExistsInFormData = key in (formData as GenericObjectType); acc[key as keyof T] = mergeDefaultsWithFormData( defaults ? get(defaults, key) : {}, - get(formData, key), + keyValue, mergeExtraArrayDefaults, - defaultSupercedesUndefined + defaultSupercedesUndefined, + // overrideFormDataWithDefaults can be true only when the key value exists in defaults + // Or if the key value doesn't exist in formData + overrideFormDataWithDefaults && (keyExistsInDefaults || !keyExistsInFormData) ); return acc; }, acc); } - if (defaultSupercedesUndefined && formData === undefined) { + if (defaultSupercedesUndefined && (isNil(formData) || (typeof formData === 'number' && isNaN(formData)))) { return defaults; + } else if (overrideFormDataWithDefaults && isNil(formData)) { + // If the overrideFormDataWithDefaults flag is true and formData is set to undefined or null return formData + return formData; } - return formData; + return overrideFormDataWithDefaults ? defaults : formData; } diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index f367b2cbf0..33a71d1663 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -30,8 +30,12 @@ import { ValidatorType, } from '../types'; import isMultiSelect from './isMultiSelect'; +import isSelect from './isSelect'; import retrieveSchema, { resolveDependencies } from './retrieveSchema'; +import isConstant from '../isConstant'; import { JSONSchema7Object } from 'json-schema'; +import { isEqual } from 'lodash'; +import optionsList from '../optionsList'; const PRIMITIVE_TYPES = ['string', 'number', 'integer', 'boolean', 'null']; @@ -169,6 +173,10 @@ interface ComputeDefaultsProps experimental_customMergeAllOf?: Experimental_CustomMergeAllOf; /** Optional flag, if true, indicates this schema was required in the parent schema. */ required?: boolean; + /** Optional flag, if true, It will merge defaults into formData. + * The formData should take precedence unless it's not valid. This is useful when for example the value from formData does not exist in the schema 'enum' property, in such cases we take the value from the defaults because the value from the formData is not valid. + */ + shouldMergeDefaultsIntoFormData?: boolean; } /** Computes the defaults for the current `schema` given the `rawFormData` and `parentDefaults` if any. This drills into @@ -193,6 +201,7 @@ export function computeDefaults( + defaultsWithFormData as T, + matchingFormData as T, + mergeExtraDefaults, + true + ) as T; + } + } + + return defaultsWithFormData; +} + +/** + * Ensure that the formData matches the given schema. If it's not matching in the case of a selectField, we change it to match the schema. + * @param validator - an implementation of the `ValidatorType` interface that will be used when necessary + * @param schema - The schema for which the formData state is desired + * @param rootSchema - The root schema, used to primarily to look up `$ref`s + * @param formData - The current formData + * @returns - valid formData that matches schema + */ +export function ensureFormDataMatchingSchema< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +>(validator: ValidatorType, schema: S, rootSchema: S, formData: T | undefined): T | T[] | undefined { + const isSelectField = !isConstant(schema) && isSelect(validator, schema, rootSchema); + let validFormData: T | T[] | undefined = formData; + + if (isSelectField) { + const getOptionsList = optionsList(schema); + const isValid = getOptionsList?.some((option) => isEqual(option.value, formData)); + validFormData = isValid ? formData : undefined; + } + return validFormData; } /** Computes the default value for objects. @@ -336,6 +388,7 @@ export function getObjectDefaults = {}, defaults?: T | T[] | undefined ): T { @@ -369,6 +422,7 @@ export function getObjectDefaults( acc, @@ -413,6 +467,7 @@ export function getObjectDefaults( @@ -447,6 +502,7 @@ export function getArrayDefaults = {}, defaults?: T | T[] | undefined ): T | T[] | undefined { @@ -474,6 +530,7 @@ export function getArrayDefaults(validator, theSchema, rootSchema, formData, experimental_customMergeAllOf); + + // Get the computed defaults with 'shouldMergeDefaultsIntoFormData' set to true to merge defaults into formData. + // This is done when for example the value from formData does not exist in the schema 'enum' property, in such cases we take the value from the defaults because the value from the formData is not valid. const defaults = computeDefaults(validator, schema, { rootSchema, includeUndefinedValues, experimental_defaultFormStateBehavior, experimental_customMergeAllOf, rawFormData: formData, + shouldMergeDefaultsIntoFormData: true, }); - if (formData === undefined || formData === null || (typeof formData === 'number' && isNaN(formData))) { - // No form data? Use schema defaults. - return defaults; - } - const { mergeDefaultsIntoFormData, arrayMinItems = {} } = experimental_defaultFormStateBehavior || {}; - const { mergeExtraDefaults } = arrayMinItems; - const defaultSupercedesUndefined = mergeDefaultsIntoFormData === 'useDefaultIfFormDataUndefined'; - if (isObject(formData)) { - return mergeDefaultsWithFormData(defaults as T, formData, mergeExtraDefaults, defaultSupercedesUndefined); - } - if (Array.isArray(formData)) { - return mergeDefaultsWithFormData(defaults as T[], formData, mergeExtraDefaults, defaultSupercedesUndefined); + // If the formData is an object or an array, add additional properties from formData and override formData with defaults since the defaults are already merged with formData. + if (isObject(formData) || Array.isArray(formData)) { + const { mergeDefaultsIntoFormData } = experimental_defaultFormStateBehavior || {}; + const defaultSupercedesUndefined = mergeDefaultsIntoFormData === 'useDefaultIfFormDataUndefined'; + const result = mergeDefaultsWithFormData( + defaults as T, + formData, + true, // set to true to add any additional default array entries. + defaultSupercedesUndefined, + true // set to true to override formData with defaults if they exist. + ); + return result; } - return formData; + + return defaults; } diff --git a/packages/utils/test/mergeDefaultsWithFormData.test.ts b/packages/utils/test/mergeDefaultsWithFormData.test.ts index 8ccf2dcb64..060cee8e6b 100644 --- a/packages/utils/test/mergeDefaultsWithFormData.test.ts +++ b/packages/utils/test/mergeDefaultsWithFormData.test.ts @@ -26,7 +26,7 @@ describe('mergeDefaultsWithFormData()', () => { }); it('should return default when formData is null and defaultSupercedesUndefined true', () => { - expect(mergeDefaultsWithFormData({}, null, undefined, true)).toBeNull(); + expect(mergeDefaultsWithFormData({}, null, undefined, true)).toEqual({}); }); it('should return undefined when formData is undefined', () => { @@ -140,4 +140,139 @@ describe('mergeDefaultsWithFormData()', () => { }; expect(mergeDefaultsWithFormData(obj1, obj2)?.a).toBeInstanceOf(File); }); + + describe('test with overrideFormDataWithDefaults set to true', () => { + it('should return data in formData when no defaults', () => { + expect(mergeDefaultsWithFormData(undefined, [2], undefined, undefined, true)).toEqual([2]); + }); + + it('should return formData when formData is undefined', () => { + expect(mergeDefaultsWithFormData({}, undefined, undefined, undefined, true)).toEqual(undefined); + }); + + it('should return default when formData is undefined and defaultSupercedesUndefined true', () => { + expect(mergeDefaultsWithFormData({}, undefined, undefined, true, true)).toEqual({}); + }); + + it('should return default when formData is null and defaultSupercedesUndefined true', () => { + expect(mergeDefaultsWithFormData({}, null, undefined, true, true)).toEqual({}); + }); + + it('should merge two one-level deep objects', () => { + expect(mergeDefaultsWithFormData({ a: 1 }, { b: 2 }, undefined, undefined, true)).toEqual({ + a: 1, + b: 2, + }); + }); + + it('should override the first object with the values from the second', () => { + expect(mergeDefaultsWithFormData({ a: 1 }, { a: 2 }, undefined, undefined, true)).toEqual({ a: 1 }); + }); + + it('should override non-existing values of the first object with the values from the second', () => { + expect( + mergeDefaultsWithFormData({ a: { b: undefined } }, { a: { b: { c: 1 } } }, undefined, undefined, true) + ).toEqual({ + a: { b: { c: 1 } }, + }); + }); + + it('should merge arrays using entries from second', () => { + expect(mergeDefaultsWithFormData([1, 2, 3], [4, 5], undefined, undefined, true)).toEqual([1, 2, 3]); + }); + + it('should merge arrays using entries from second and extra from the first', () => { + expect(mergeDefaultsWithFormData([1, 2], [4, 5, 6], undefined, undefined, true)).toEqual([1, 2, 6]); + }); + + it('should deeply merge arrays with overlapping entries', () => { + expect(mergeDefaultsWithFormData([{ a: 1 }], [{ b: 2 }, { c: 3 }], undefined, undefined, true)).toEqual([ + { a: 1, b: 2 }, + { c: 3 }, + ]); + }); + + it('should recursively merge deeply nested objects', () => { + const obj1 = { + a: 1, + b: { + c: 3, + d: [1, 2, 3], + e: { f: { g: 1 } }, + h: [{ i: 1 }, { i: 2 }], + }, + c: 2, + }; + const obj2 = { + a: 1, + b: { + d: [3], + e: { f: { h: 2 } }, + g: 1, + h: [{ i: 3 }], + }, + c: 3, + }; + const expected = { + a: 1, + b: { + c: 3, + d: [1, 2, 3], + e: { f: { g: 1, h: 2 } }, + g: 1, + h: [{ i: 1 }, { i: 2 }], + }, + c: 2, + }; + expect(mergeDefaultsWithFormData(obj1, obj2, undefined, undefined, true)).toEqual(expected); + }); + + it('should recursively merge deeply nested objects, including extra array data', () => { + const obj1 = { + a: 1, + b: { + c: 3, + d: [1, 2, 3], + e: { f: { g: 1 } }, + h: [{ i: 1 }, { i: 2 }], + }, + c: 2, + }; + const obj2 = { + a: 1, + b: { + d: [3], + e: { f: { h: 2 } }, + g: 1, + h: [{ i: 3 }, { i: 4 }, { i: 5 }], + }, + c: 3, + d: 4, + }; + const expected = { + a: 1, + b: { + c: 3, + d: [1, 2, 3], + e: { f: { g: 1, h: 2 } }, + g: 1, + h: [{ i: 1 }, { i: 2 }, { i: 5 }], + }, + c: 2, + d: 4, + }; + expect(mergeDefaultsWithFormData(obj1, obj2, undefined, undefined, true)).toEqual(expected); + }); + + it('should recursively merge File objects', () => { + const file = new File(['test'], 'test.txt'); + const obj1 = { + a: {}, + }; + const obj2 = { + a: file, + }; + expect(mergeDefaultsWithFormData(obj1, obj2)?.a).toBeInstanceOf(File); + }); + }); }); diff --git a/packages/utils/test/schema/getDefaultFormStateTest.ts b/packages/utils/test/schema/getDefaultFormStateTest.ts index cf3ef4cd6a..26f6a1bd86 100644 --- a/packages/utils/test/schema/getDefaultFormStateTest.ts +++ b/packages/utils/test/schema/getDefaultFormStateTest.ts @@ -6,131 +6,336 @@ import { getDefaultBasedOnSchemaType, getInnerSchemaForArrayItem, getObjectDefaults, + ensureFormDataMatchingSchema, } from '../../src/schema/getDefaultFormState'; import { RECURSIVE_REF, RECURSIVE_REF_ALLOF } from '../testUtils/testData'; -import { TestValidatorType } from './types'; +import { IExpectType, TestValidatorType } from './types'; import { resolveDependencies } from '../../src/schema/retrieveSchema'; -export default function getDefaultFormStateTest(testValidator: TestValidatorType) { - describe('getDefaultFormState()', () => { - let consoleWarnSpy: jest.SpyInstance; - beforeAll(() => { - consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); // mock this to avoid actually warning in the tests +/** + * Validate the expected value based on the index of the expectList + * @param index = index of the expectList + * @param expectList = list of expected values + * @param schema = schema + * @param options = optional arguments + */ +const validateBasedOnIndex = (index: number, expectList: IExpectType[], schema: RJSFSchema, options?: any) => { + const { expectedCB, toEqual } = expectList[index]; + expect(expectedCB(schema, options)).toEqual(toEqual); +}; + +type ObjectDefaultExpectList = [ + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType, + IExpectType +]; + +/** + * This function tests schema with type object default values with expectedList, which has a generic callback to get expected data and toEqual data. It is used in multiple places with different methods. This will then test all object default values across different methods. + * + * Important: when adding a new test, please make sure to add the test at the end of the list to avoid breaking the existing tests. Also update the 'ObjectDefaultExpectList' type and add one or more 'IExpectType' to it. This will let typescript show you an error if you didn't update all the 'testObjectDefault' methods accordingly. + * @param {TestValidatorType} testValidator + * @param {IExpectType[]} expectList + */ +const testObjectDefault = (testValidator: TestValidatorType, expectList: ObjectDefaultExpectList) => { + describe('object default test ', () => { + let schema: RJSFSchema; + it('test a schema with a ref', () => { + schema = { + definitions: { + foo: { + type: 'number', + default: 42, + }, + testdef: { + type: 'object', + properties: { + foo: { + $ref: '#/definitions/foo', + }, + }, + }, + }, + $ref: '#/definitions/testdef', + }; + validateBasedOnIndex(0, expectList, schema); }); - afterAll(() => { - consoleWarnSpy.mockRestore(); + it('test a schema with a const property', () => { + schema = { + type: 'object', + properties: { + test: { + type: 'string', + const: 'test', + }, + }, + }; + validateBasedOnIndex(1, expectList, schema); }); - it('throws error when schema is not an object', () => { - expect(() => getDefaultFormState(testValidator, null as unknown as RJSFSchema)).toThrowError('Invalid schema:'); + it('test a schema with a const property and constAsDefaults is never', () => { + expect( + computeDefaults(testValidator, schema, { + rootSchema: schema, + experimental_defaultFormStateBehavior: { constAsDefaults: 'never' }, + }) + ).toEqual({}); + validateBasedOnIndex(2, expectList, schema, { + experimental_defaultFormStateBehavior: { constAsDefaults: 'never' }, + }); }); - it('test an object const value merge with formData', () => { - const schema: RJSFSchema = { + it('test an object with an optional property that has a nested required property', () => { + schema = { type: 'object', properties: { - localConst: { + optionalProperty: { + type: 'object', + properties: { + nestedRequiredProperty: { + type: 'string', + }, + }, + required: ['nestedRequiredProperty'], + }, + requiredProperty: { type: 'string', - const: 'local', + default: 'foo', }, - RootConst: { + }, + required: ['requiredProperty'], + }; + validateBasedOnIndex(3, expectList, schema); + }); + it('test an object with an optional property that has a nested required property with default', () => { + schema = { + type: 'object', + properties: { + optionalProperty: { type: 'object', properties: { - attr1: { - type: 'number', - }, - attr2: { - type: 'boolean', + nestedRequiredProperty: { + type: 'string', + default: '', }, }, - const: { - attr1: 1, - attr2: true, - }, + required: ['nestedRequiredProperty'], }, - RootAndLocalConst: { + requiredProperty: { type: 'string', - const: 'FromLocal', + default: 'foo', }, - fromFormData: { + }, + required: ['requiredProperty'], + }; + validateBasedOnIndex(4, expectList, schema); + }); + it('test an object with an optional property that has a nested required property and includeUndefinedValues', () => { + schema = { + type: 'object', + properties: { + optionalProperty: { + type: 'object', + properties: { + nestedRequiredProperty: { + type: 'object', + properties: { + undefinedProperty: { + type: 'string', + }, + }, + }, + }, + required: ['nestedRequiredProperty'], + }, + requiredProperty: { type: 'string', + default: 'foo', }, }, - const: { - RootAndLocalConst: 'FromRoot', + required: ['requiredProperty'], + }; + validateBasedOnIndex(5, expectList, schema, { includeUndefinedValues: true }); + }); + it("test an object with an optional property that has a nested required property and includeUndefinedValues is 'excludeObjectChildren'", () => { + schema = { + type: 'object', + properties: { + optionalNumberProperty: { + type: 'number', + }, + optionalObjectProperty: { + type: 'object', + properties: { + nestedRequiredProperty: { + type: 'object', + properties: { + undefinedProperty: { + type: 'string', + }, + }, + }, + }, + required: ['nestedRequiredProperty'], + }, + requiredProperty: { + type: 'string', + default: 'foo', + }, }, + required: ['requiredProperty'], }; - expect( - getDefaultFormState( - testValidator, - schema, - { - fromFormData: 'fromFormData', + validateBasedOnIndex(6, expectList, schema, { includeUndefinedValues: 'excludeObjectChildren' }); + }); + it('test an object with an additionalProperties', () => { + schema = { + type: 'object', + properties: { + requiredProperty: { + type: 'string', + default: 'foo', }, - schema, - false, - { emptyObjectFields: 'skipDefaults' } - ) - ).toEqual({ - localConst: 'local', - RootConst: { - attr1: 1, - attr2: true, }, - RootAndLocalConst: 'FromLocal', - fromFormData: 'fromFormData', - }); + additionalProperties: true, + required: ['requiredProperty'], + default: { + foo: 'bar', + }, + }; + validateBasedOnIndex(7, expectList, schema); }); - it('test an object const value merge with formData and constAsDefault is never', () => { - const schema: RJSFSchema = { + it('test an object with an additionalProperties and includeUndefinedValues', () => { + schema = { type: 'object', properties: { - localConst: { + requiredProperty: { type: 'string', - const: 'local', + default: 'foo', }, - RootConst: { + }, + additionalProperties: { + type: 'string', + }, + required: ['requiredProperty'], + default: { + foo: 'bar', + }, + }; + validateBasedOnIndex(8, expectList, schema, { includeUndefinedValues: true }); + }); + it('test an object with additionalProperties type object with defaults and formdata', () => { + schema = { + type: 'object', + properties: { + test: { + title: 'Test', type: 'object', properties: { - attr1: { - type: 'number', - }, - attr2: { - type: 'boolean', + foo: { + type: 'string', }, }, - const: { - attr1: 1, - attr2: true, + additionalProperties: { + type: 'object', + properties: { + host: { + title: 'Host', + type: 'string', + default: 'localhost', + }, + port: { + title: 'Port', + type: 'integer', + default: 389, + }, + }, }, }, - RootAndLocalConst: { - type: 'string', - const: 'FromLocal', - }, - fromFormData: { - type: 'string', - }, }, - const: { - RootAndLocalConst: 'FromRoot', + }; + validateBasedOnIndex(9, expectList, schema, { rawFormData: { test: { foo: 'x', newKey: {} } } }); + }); + it('test an object with additionalProperties type object with formdata and no defaults', () => { + schema = { + type: 'object', + properties: { + test: { + title: 'Test', + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + additionalProperties: { + type: 'object', + properties: { + host: { + title: 'Host', + type: 'string', + }, + port: { + title: 'Port', + type: 'integer', + }, + }, + }, + }, }, }; - expect( - getDefaultFormState( - testValidator, - schema, - { - fromFormData: 'fromFormData', + validateBasedOnIndex(10, expectList, schema, { rawFormData: { test: { foo: 'x', newKey: {} } } }); + }); + it('test an object with additionalProperties type object with no defaults and non-object formdata', () => { + schema = { + type: 'object', + properties: { + test: { + title: 'Test', + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + additionalProperties: { + type: 'object', + properties: { + host: { + title: 'Host', + type: 'string', + }, + port: { + title: 'Port', + type: 'integer', + }, + }, + }, }, - schema, - false, - { emptyObjectFields: 'skipDefaults', constAsDefaults: 'never' } - ) - ).toEqual({ - fromFormData: 'fromFormData', - }); + }, + }; + validateBasedOnIndex(11, expectList, schema, { rawFormData: {} }); }); it('test an object with deep nested dependencies with formData', () => { - const schema: RJSFSchema = { + schema = { type: 'object', properties: { nestedObject: { @@ -183,1571 +388,1705 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType false, // Second oneOf... second !== first ], }); - expect( - getDefaultFormState( - testValidator, - schema, - { - nestedObject: { - first: 'yes', - }, - }, - schema, - false, - { - emptyObjectFields: 'populateAllDefaults', - allOf: 'skipDefaults', - arrayMinItems: { - populate: 'populate' as any, - mergeExtraDefaults: false, - }, - mergeDefaultsIntoFormData: 'useFormDataIfPresent', - } - ) - ).toEqual({ - nestedObject: { - first: 'yes', - second: { - deeplyNestedThird: 'before', + + validateBasedOnIndex(12, expectList, schema, { + rawFormData: { + nestedObject: { + first: 'yes', }, }, + testValidator, }); }); - it('getInnerSchemaForArrayItem() item of type boolean returns empty schema', () => { - expect(getInnerSchemaForArrayItem({ items: [true] }, AdditionalItemsHandling.Ignore, 0)).toEqual({}); + it('test handling an invalid property schema', () => { + schema = { + type: 'object', + properties: { + invalidProperty: 'not a valid property value', + }, + } as RJSFSchema; + + validateBasedOnIndex(13, expectList, schema, { + includeUndefinedValues: 'excludeObjectChildren', + }); }); - describe('resolveDependencies()', () => { - it('test an object with dependencies', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - first: { - type: 'string', - enum: ['no', 'yes'], - default: 'no', + it('test with a recursive schema', () => { + validateBasedOnIndex(14, expectList, RECURSIVE_REF, { + includeUndefinedValues: 'excludeObjectChildren', + }); + }); + it('test with a recursive allof schema', () => { + validateBasedOnIndex(15, expectList, RECURSIVE_REF_ALLOF); + }); + it('test returns undefined with simple schema and no optional args', () => { + schema = { type: 'string' }; + validateBasedOnIndex(16, expectList, schema); + }); + it('test an object const value merge with formData', () => { + schema = { + type: 'object', + properties: { + localConst: { + type: 'string', + const: 'local', + }, + RootConst: { + type: 'object', + properties: { + attr1: { + type: 'number', + }, + attr2: { + type: 'boolean', + }, + }, + const: { + attr1: 1, + attr2: true, }, }, - dependencies: { - first: { - oneOf: [ - { - properties: { - first: { - enum: ['yes'], - }, - second: { - type: 'object', - properties: { - deeplyNestedThird: { - type: 'string', - enum: ['before', 'after'], - default: 'before', - }, - }, - }, - }, - }, - { - properties: { - first: { - enum: ['no'], - }, - }, - }, - ], - }, - }, - }; - - // Mock isValid so that withExactlyOneSubschema works as expected - testValidator.setReturnValues({ - isValid: [ - true, // First oneOf... first === first - false, // Second oneOf... second !== first - ], - }); - expect( - resolveDependencies( - testValidator, - schema, - schema, - false, - [], - { - first: 'yes', - }, - undefined - ) - ).toEqual([ - { - type: 'object', - properties: { - first: { - type: 'string', - enum: ['no', 'yes'], - default: 'no', - }, - second: { - type: 'object', - properties: { - deeplyNestedThird: { - type: 'string', - enum: ['before', 'after'], - default: 'before', - }, - }, - }, - }, - }, - ]); - }); - }); - describe('computeDefaults()', () => { - it('test computeDefaults that is passed a schema with a ref', () => { - const schema: RJSFSchema = { - definitions: { - foo: { - type: 'number', - default: 42, - }, - testdef: { - type: 'object', - properties: { - foo: { - $ref: '#/definitions/foo', - }, - }, - }, - }, - $ref: '#/definitions/testdef', - }; - expect(computeDefaults(testValidator, schema, { rootSchema: schema })).toEqual({ - foo: 42, - }); - }); - it('test computeDefaults that is passed a schema with a const property', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - test: { - type: 'string', - const: 'test', - }, - }, - }; - expect(computeDefaults(testValidator, schema, { rootSchema: schema })).toEqual({ - test: 'test', - }); - }); - it('test computeDefaults that is passed a schema with a const property and constAsDefaults is never', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - test: { - type: 'string', - const: 'test', - }, - }, - }; - expect( - computeDefaults(testValidator, schema, { - rootSchema: schema, - experimental_defaultFormStateBehavior: { constAsDefaults: 'never' }, - }) - ).toEqual({}); - }); - it('test oneOf with const values and constAsDefaults is always', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - oneOfField: { - title: 'One Of Field', - type: 'string', - oneOf: [ - { - const: 'username', - title: 'Username and password', - }, - { - const: 'secret', - title: 'SSO', - }, - ], - }, - }, - required: ['oneOfField'], - }; - expect( - computeDefaults(testValidator, schema, { - rootSchema: schema, - experimental_defaultFormStateBehavior: { constAsDefaults: 'always' }, - }) - ).toEqual({ oneOfField: 'username' }); - }); - it('test oneOf with const values and constAsDefaults is skipOneOf', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - oneOfField: { - title: 'One Of Field', - type: 'string', - oneOf: [ - { - const: 'username', - title: 'Username and password', - }, - { - const: 'secret', - title: 'SSO', - }, - ], - }, - }, - required: ['oneOfField'], - }; - const result = computeDefaults(testValidator, schema, { - rootSchema: schema, - experimental_defaultFormStateBehavior: { constAsDefaults: 'skipOneOf' }, - }); - expect(result).toEqual({}); - }); - it('test oneOf with const values and constAsDefaults is never', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - oneOfField: { - title: 'One Of Field', - type: 'string', - oneOf: [ - { - const: 'username', - title: 'Username and password', - }, - { - const: 'secret', - title: 'SSO', - }, - ], - }, - }, - required: ['oneOfField'], - }; - expect( - computeDefaults(testValidator, schema, { - rootSchema: schema, - experimental_defaultFormStateBehavior: { constAsDefaults: 'never' }, - }) - ).toEqual({}); - }); - it('test an object with an optional property that has a nested required property', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - optionalProperty: { - type: 'object', - properties: { - nestedRequiredProperty: { - type: 'string', - }, - }, - required: ['nestedRequiredProperty'], - }, - requiredProperty: { - type: 'string', - default: 'foo', - }, - }, - required: ['requiredProperty'], - }; - expect(computeDefaults(testValidator, schema, { rootSchema: schema })).toEqual({ requiredProperty: 'foo' }); - }); - it('test an object with an optional property that has a nested required property with default', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - optionalProperty: { - type: 'object', - properties: { - nestedRequiredProperty: { - type: 'string', - default: '', - }, - }, - required: ['nestedRequiredProperty'], - }, - requiredProperty: { - type: 'string', - default: 'foo', - }, - }, - required: ['requiredProperty'], - }; - expect(computeDefaults(testValidator, schema, { rootSchema: schema })).toEqual({ - requiredProperty: 'foo', - optionalProperty: { nestedRequiredProperty: '' }, - }); - }); - it('test an object with an optional property that has a nested required property and includeUndefinedValues', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - optionalProperty: { - type: 'object', - properties: { - nestedRequiredProperty: { - type: 'object', - properties: { - undefinedProperty: { - type: 'string', - }, - }, - }, - }, - required: ['nestedRequiredProperty'], - }, - requiredProperty: { - type: 'string', - default: 'foo', - }, - }, - required: ['requiredProperty'], - }; - expect(computeDefaults(testValidator, schema, { rootSchema: schema, includeUndefinedValues: true })).toEqual({ - optionalProperty: { - nestedRequiredProperty: { - undefinedProperty: undefined, - }, - }, - requiredProperty: 'foo', - }); - }); - it("test an object with an optional property that has a nested required property and includeUndefinedValues is 'excludeObjectChildren'", () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - optionalNumberProperty: { - type: 'number', - }, - optionalObjectProperty: { - type: 'object', - properties: { - nestedRequiredProperty: { - type: 'object', - properties: { - undefinedProperty: { - type: 'string', - }, - }, - }, - }, - required: ['nestedRequiredProperty'], - }, - requiredProperty: { - type: 'string', - default: 'foo', - }, - }, - required: ['requiredProperty'], - }; - expect( - computeDefaults(testValidator, schema, { - rootSchema: schema, - includeUndefinedValues: 'excludeObjectChildren', - }) - ).toEqual({ - optionalNumberProperty: undefined, - optionalObjectProperty: { - nestedRequiredProperty: {}, - }, - requiredProperty: 'foo', - }); - }); - it('test an object with an additionalProperties', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - requiredProperty: { - type: 'string', - default: 'foo', - }, - }, - additionalProperties: true, - required: ['requiredProperty'], - default: { - foo: 'bar', - }, - }; - expect(computeDefaults(testValidator, schema, { rootSchema: schema })).toEqual({ - requiredProperty: 'foo', - foo: 'bar', - }); - }); - it('test an object with an additionalProperties and includeUndefinedValues', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - requiredProperty: { - type: 'string', - default: 'foo', - }, - }, - additionalProperties: { + RootAndLocalConst: { type: 'string', + const: 'FromLocal', }, - required: ['requiredProperty'], - default: { - foo: 'bar', + fromFormData: { + type: 'string', }, - }; - expect(computeDefaults(testValidator, schema, { rootSchema: schema, includeUndefinedValues: true })).toEqual({ - requiredProperty: 'foo', - foo: 'bar', - }); + }, + const: { + RootAndLocalConst: 'FromRoot', + }, + }; + + validateBasedOnIndex(17, expectList, schema, { + rawFormData: { + fromFormData: 'fromFormData', + }, + experimental_defaultFormStateBehavior: { + emptyObjectFields: 'skipDefaults', + }, }); - it('test an object with additionalProperties type object with defaults and formdata', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - test: { - title: 'Test', - type: 'object', - properties: { - foo: { - type: 'string', - }, - }, - additionalProperties: { - type: 'object', + }); + it('test an object const value merge with formData and constAsDefault is never', () => { + validateBasedOnIndex(18, expectList, schema, { + rawFormData: { + fromFormData: 'fromFormData', + }, + experimental_defaultFormStateBehavior: { + emptyObjectFields: 'skipDefaults', + constAsDefaults: 'never', + }, + testValidator, + }); + }); + it('test an object with non valid formData for enum properties', () => { + schema = { + type: 'object', + properties: { + animal: { + enum: ['Cat', 'Fish'], + }, + }, + dependencies: { + animal: { + oneOf: [ + { properties: { - host: { - title: 'Host', + animal: { + enum: ['Cat'], + }, + food: { type: 'string', - default: 'localhost', + enum: ['meat', 'grass', 'fish'], + default: 'meat', }, - port: { - title: 'Port', - type: 'integer', - default: 389, + multipleChoicesList: { + type: 'array', + title: 'A multiple choices list', + items: { + type: 'string', + enum: ['foo', 'bar', 'qux'], + }, + uniqueItems: true, + default: ['foo'], }, }, }, - }, - }, - }; - expect( - computeDefaults(testValidator, schema, { - rootSchema: schema, - rawFormData: { test: { foo: 'x', newKey: {} } }, - }) - ).toEqual({ - test: { - newKey: { - host: 'localhost', - port: 389, - }, - }, - }); - }); - it('test an object with additionalProperties type object with no defaults and formdata', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - test: { - title: 'Test', - type: 'object', - properties: { - foo: { - type: 'string', - }, - }, - additionalProperties: { - type: 'object', + { properties: { - host: { - title: 'Host', + animal: { + enum: ['Fish'], + }, + food: { type: 'string', + enum: ['insect', 'worms'], + default: 'worms', }, - port: { - title: 'Port', - type: 'integer', + multipleChoicesList: { + type: 'array', + title: 'A multiple choices list', + items: { + type: 'string', + enum: ['a', 'a', 'b', 'c'], + }, + uniqueItems: true, + default: ['a'], }, - }, - }, - }, - }, - }; - expect( - computeDefaults(testValidator, schema, { - rootSchema: schema, - rawFormData: { test: { foo: 'x', newKey: {} } }, - }) - ).toEqual({ - test: { - newKey: {}, - }, - }); - }); - it('test an object with additionalProperties type object with no defaults and non-object formdata', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - test: { - title: 'Test', - type: 'object', - properties: { - foo: { - type: 'string', - }, - }, - additionalProperties: { - type: 'object', - properties: { - host: { - title: 'Host', + water: { type: 'string', - }, - port: { - title: 'Port', - type: 'integer', + enum: ['lake', 'sea'], + default: 'sea', }, }, }, - }, + ], }, - }; - expect( - computeDefaults(testValidator, schema, { - rootSchema: schema, - rawFormData: {}, - }) - ).toEqual({}); + }, + }; + + // Mock isValid so that withExactlyOneSubschema works as expected + testValidator.setReturnValues({ + isValid: [false, true], }); - it('test an object with deep nested dependencies with formData', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - nestedObject: { - type: 'object', - properties: { - first: { - type: 'string', - enum: ['no', 'yes'], - default: 'no', - }, - }, - dependencies: { - first: { - oneOf: [ - { - properties: { - first: { - enum: ['yes'], - }, - second: { - type: 'object', - properties: { - deeplyNestedThird: { - type: 'string', - enum: ['before', 'after'], - default: 'before', - }, - }, - }, - }, - }, - { - properties: { - first: { - enum: ['no'], - }, - }, - }, - ], - }, - }, - }, - }, - }; - // Mock isValid so that withExactlyOneSubschema works as expected - testValidator.setReturnValues({ - isValid: [ - true, // First oneOf... first === first - false, // Second oneOf... second !== first - ], - }); - expect( - computeDefaults(testValidator, schema, { - rootSchema: schema, - rawFormData: { - nestedObject: { - first: 'yes', - }, - }, - }) - ).toEqual({ - nestedObject: { - first: 'no', - second: { - deeplyNestedThird: 'before', - }, - }, - }); + validateBasedOnIndex(19, expectList, schema, { + rawFormData: { + animal: 'Fish', + food: 'meat', + multipleChoicesList: ['a'], + water: null, + }, + shouldMergeDefaultsIntoFormData: true, + testValidator, + }); + }); + it('test an object with non valid formData for enum properties with mergeDefaultsIntoFormData set to "useDefaultIfFormDataUndefined"', () => { + // Mock isValid so that withExactlyOneSubschema works as expected + testValidator.setReturnValues({ + isValid: [false, true], }); - it('test an array with defaults with no formData', () => { - const schema: RJSFSchema = { - type: 'array', - minItems: 4, - default: ['Raphael', 'Michaelangelo'], - items: { - type: 'string', - default: 'Unknown', - }, - }; - expect( - computeDefaults(testValidator, schema, { - rootSchema: schema, - includeUndefinedValues: 'excludeObjectChildren', - }) - ).toEqual(['Raphael', 'Michaelangelo', 'Unknown', 'Unknown']); + validateBasedOnIndex(20, expectList, schema, { + rawFormData: { + animal: 'Fish', + food: 'meat', + multipleChoicesList: ['a'], + water: null, + }, + shouldMergeDefaultsIntoFormData: true, + experimental_defaultFormStateBehavior: { + mergeDefaultsIntoFormData: 'useDefaultIfFormDataUndefined', + }, + testValidator, }); - it('test an array with defaults with empty array as formData', () => { - const schema: RJSFSchema = { - type: 'array', - minItems: 4, - default: ['Raphael', 'Michaelangelo'], - items: { - type: 'string', - default: 'Unknown', - }, - }; - expect( - computeDefaults(testValidator, schema, { - rootSchema: schema, - rawFormData: [], - includeUndefinedValues: 'excludeObjectChildren', - experimental_defaultFormStateBehavior: { - arrayMinItems: { - mergeExtraDefaults: true, - populate: 'all', + // Reset the testValidator + if (typeof testValidator.reset === 'function') { + testValidator?.reset(); + } + }); + it('test oneOf with const values and constAsDefaults is always', () => { + schema = { + type: 'object', + properties: { + oneOfField: { + title: 'One Of Field', + type: 'string', + oneOf: [ + { + const: 'username', + title: 'Username and password', }, - }, - }) - ).toEqual(['Raphael', 'Michaelangelo', 'Unknown', 'Unknown']); - }); - it('test computeDefaults handles an invalid property schema', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - invalidProperty: 'not a valid property value', + { + const: 'secret', + title: 'SSO', + }, + ], }, - } as RJSFSchema; - expect( - computeDefaults(testValidator, schema, { - rootSchema: schema, - includeUndefinedValues: 'excludeObjectChildren', - }) - ).toEqual({}); + }, + required: ['oneOfField'], + }; + validateBasedOnIndex(21, expectList, schema, { + experimental_defaultFormStateBehavior: { + constAsDefaults: 'always', + }, + }); + }); + it('test oneOf with const values and constAsDefaults is skipOneOf', () => { + validateBasedOnIndex(22, expectList, schema, { + experimental_defaultFormStateBehavior: { + constAsDefaults: 'skipOneOf', + }, + }); + }); + it('test oneOf with const values and constAsDefaults is never', () => { + validateBasedOnIndex(23, expectList, schema, { + experimental_defaultFormStateBehavior: { + constAsDefaults: 'never', + }, + }); + }); + }); +}; + +type ArrayDefaultExpectList = [IExpectType, IExpectType, IExpectType, IExpectType, IExpectType, IExpectType]; + +/** + * This function tests schema with type array default values with expectedList, which has a generic callback to get expected data and toEqual data. It is used in multiple places with different methods. This will then test all array default values across different methods. + * + * Important: when adding a new test, please make sure to add the test at the end of the list to avoid breaking the existing tests. Also update the 'ArrayDefaultExpectList' type and add one or more 'IExpectType' to it. This will let typescript show you an error if you didn't update all the 'testArrayDefault' methods accordingly. + * @param {TestValidatorType} testValidator + * @param {IExpectType[]} expectList + */ +const testArrayDefault = (testValidator: TestValidatorType, expectList: ArrayDefaultExpectList) => { + describe('test array default', () => { + // Reset the testValidator + if (typeof testValidator.reset === 'function') { + testValidator?.reset(); + } + + it('test an array with defaults with no formData', () => { + const schema: RJSFSchema = { + type: 'array', + minItems: 4, + default: ['Raphael', 'Michaelangelo'], + items: { + type: 'string', + default: 'Unknown', + }, + }; + + validateBasedOnIndex(0, expectList, schema, { + includeUndefinedValues: 'excludeObjectChildren', }); - it('test with a recursive schema', () => { - expect(computeDefaults(testValidator, RECURSIVE_REF, { rootSchema: RECURSIVE_REF })).toEqual({ - name: '', - }); + }); + it('test an array with defaults with empty array as formData', () => { + const schema: RJSFSchema = { + type: 'array', + minItems: 4, + default: ['Raphael', 'Michaelangelo'], + items: { + type: 'string', + default: 'Unknown', + }, + }; + + validateBasedOnIndex(1, expectList, schema, { + rawFormData: [], + includeUndefinedValues: 'excludeObjectChildren', + experimental_defaultFormStateBehavior: { + arrayMinItems: { + mergeExtraDefaults: true, + populate: 'all', + }, + }, }); - it('test with a recursive allof schema', () => { - expect(computeDefaults(testValidator, RECURSIVE_REF_ALLOF, { rootSchema: RECURSIVE_REF_ALLOF })).toEqual({ - value: [undefined], - }); + }); + it('test an array with no defaults', () => { + const schema: RJSFSchema = { + type: 'array', + minItems: 4, + items: { + type: 'string', + }, + }; + + validateBasedOnIndex(2, expectList, schema, { + includeUndefinedValues: 'excludeObjectChildren', }); - it('test computeDefaults returns undefined with simple schema and no optional args', () => { - const schema: RJSFSchema = { type: 'string' }; - expect(computeDefaults(testValidator, schema)).toBe(undefined); + }); + it('test an array const value populate as defaults', () => { + const schema: RJSFSchema = { + type: 'array', + minItems: 4, + const: ['ConstFromRoot', 'ConstFromRoot'], + items: { + type: 'string', + const: 'Constant', + }, + }; + + validateBasedOnIndex(3, expectList, schema, { + includeUndefinedValues: 'excludeObjectChildren', }); }); - describe('getDefaultBasedOnSchemaType()', () => { - it('test an object with an optional property that has a nested required property', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - optionalProperty: { - type: 'object', - properties: { - nestedRequiredProperty: { - type: 'string', - }, - }, - required: ['nestedRequiredProperty'], - }, - requiredProperty: { - type: 'string', - default: 'foo', - }, - }, - required: ['requiredProperty'], - }; + it('test handling an invalid array schema', () => { + const schema: RJSFSchema = { + type: 'array', + items: 'not a valid item value', + } as RJSFSchema; - expect(getDefaultBasedOnSchemaType(testValidator, schema, { rootSchema: schema })).toEqual({ - requiredProperty: 'foo', - }); + validateBasedOnIndex(4, expectList, schema, { + includeUndefinedValues: 'excludeObjectChildren', }); - it('test an object with an optional property that has a nested required property with default', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - optionalProperty: { - type: 'object', - properties: { - nestedRequiredProperty: { - type: 'string', - default: '', - }, - }, - required: ['nestedRequiredProperty'], - }, - requiredProperty: { - type: 'string', - default: 'foo', - }, - }, - required: ['requiredProperty'], - }; - expect(getDefaultBasedOnSchemaType(testValidator, schema, { rootSchema: schema })).toEqual({ + }); + it('test returns undefined with simple schema and no optional args', () => { + const schema: RJSFSchema = { type: 'array' }; + validateBasedOnIndex(5, expectList, schema); + }); + }); +}; + +export default function getDefaultFormStateTest(testValidator: TestValidatorType) { + describe('getDefaultFormState()', () => { + let consoleWarnSpy: jest.SpyInstance; + beforeAll(() => { + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); // mock this to avoid actually warning in the tests + }); + afterAll(() => { + consoleWarnSpy.mockRestore(); + }); + it('throws error when schema is not an object', () => { + expect(() => getDefaultFormState(testValidator, null as unknown as RJSFSchema)).toThrowError('Invalid schema:'); + }); + // test object defaults + testObjectDefault(testValidator, [ + { + expectedCB: (schema) => getDefaultFormState(testValidator, schema, undefined, schema), + toEqual: { + foo: 42, + }, + }, + { + expectedCB: (schema) => getDefaultFormState(testValidator, schema, undefined, schema), + toEqual: { + test: 'test', + }, + }, + { + expectedCB: (schema, options) => + getDefaultFormState( + testValidator, + schema, + undefined, + schema, + undefined, + options.experimental_defaultFormStateBehavior + ), + toEqual: {}, + }, + { + expectedCB: (schema) => getDefaultFormState(testValidator, schema, undefined, schema), + toEqual: { requiredProperty: 'foo' }, + }, + { + expectedCB: (schema) => getDefaultFormState(testValidator, schema, undefined, schema), + toEqual: { requiredProperty: 'foo', optionalProperty: { nestedRequiredProperty: '' }, - }); - }); - it('test an object with an optional property that has a nested required property and includeUndefinedValues', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - optionalProperty: { - type: 'object', - properties: { - nestedRequiredProperty: { - type: 'object', - properties: { - undefinedProperty: { - type: 'string', - }, - }, - }, - }, - required: ['nestedRequiredProperty'], - }, - requiredProperty: { - type: 'string', - default: 'foo', - }, - }, - required: ['requiredProperty'], - }; - expect( - getDefaultBasedOnSchemaType(testValidator, schema, { rootSchema: schema, includeUndefinedValues: true }) - ).toEqual({ + }, + }, + { + expectedCB: (schema, options) => + getDefaultFormState(testValidator, schema, undefined, schema, options?.includeUndefinedValues), + toEqual: { optionalProperty: { nestedRequiredProperty: { undefinedProperty: undefined, }, }, requiredProperty: 'foo', - }); - }); - it("test an object with an optional property that has a nested required property and includeUndefinedValues is 'excludeObjectChildren'", () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - optionalNumberProperty: { - type: 'number', - }, - optionalObjectProperty: { - type: 'object', - properties: { - nestedRequiredProperty: { - type: 'object', - properties: { - undefinedProperty: { - type: 'string', - }, - }, - }, - }, - required: ['nestedRequiredProperty'], - }, - requiredProperty: { - type: 'string', - default: 'foo', - }, - }, - required: ['requiredProperty'], - }; - expect( - getDefaultBasedOnSchemaType(testValidator, schema, { - rootSchema: schema, - includeUndefinedValues: 'excludeObjectChildren', - }) - ).toEqual({ + }, + }, + { + expectedCB: (schema, options) => + getDefaultFormState(testValidator, schema, undefined, schema, options?.includeUndefinedValues), + toEqual: { optionalNumberProperty: undefined, optionalObjectProperty: { nestedRequiredProperty: {}, }, requiredProperty: 'foo', - }); - }); - it('test an object with an additionalProperties', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - requiredProperty: { - type: 'string', - default: 'foo', + }, + }, + { + expectedCB: (schema) => getDefaultFormState(testValidator, schema, undefined, schema), + toEqual: { + requiredProperty: 'foo', + foo: 'bar', + }, + }, + { + expectedCB: (schema, options) => + getDefaultFormState(testValidator, schema, undefined, schema, options?.includeUndefinedValues), + toEqual: { + requiredProperty: 'foo', + foo: 'bar', + }, + }, + { + expectedCB: (schema, options) => getDefaultFormState(testValidator, schema, options.rawFormData, schema), + toEqual: { + test: { + foo: 'x', + newKey: { + host: 'localhost', + port: 389, }, }, - additionalProperties: true, - required: ['requiredProperty'], - default: { - foo: 'bar', + }, + }, + { + expectedCB: (schema, options) => getDefaultFormState(testValidator, schema, options.rawFormData, schema), + toEqual: { + test: { + foo: 'x', + newKey: {}, }, - }; - expect(getDefaultBasedOnSchemaType(testValidator, schema, { rootSchema: schema }, { foo: 'bar' })).toEqual({ - requiredProperty: 'foo', - foo: 'bar', - }); - }); - it('test an object with an additionalProperties and includeUndefinedValues', () => { + }, + }, + { + expectedCB: (schema, options) => getDefaultFormState(testValidator, schema, options.rawFormData, schema), + toEqual: {}, + }, + { + expectedCB: (schema, options) => + getDefaultFormState(options.testValidator, schema, options.rawFormData, schema, false, { + emptyObjectFields: 'populateAllDefaults', + allOf: 'skipDefaults', + arrayMinItems: { + populate: 'populate' as any, + mergeExtraDefaults: false, + }, + mergeDefaultsIntoFormData: 'useFormDataIfPresent', + }), + toEqual: { + nestedObject: { + first: 'yes', + second: { + deeplyNestedThird: 'before', + }, + }, + }, + }, + { + expectedCB: (schema, options) => + getDefaultFormState(testValidator, schema, undefined, schema, options.includeUndefinedValues), + toEqual: {}, + }, + { + expectedCB: (schema) => getDefaultFormState(testValidator, schema, undefined, schema), + toEqual: { + children: { + name: '', + }, + name: '', + }, + }, + { + expectedCB: (schema) => getDefaultFormState(testValidator, schema, undefined, schema), + toEqual: { + value: [undefined], + }, + }, + { + expectedCB: (schema) => getDefaultFormState(testValidator, schema, undefined), + toEqual: undefined, + }, + { + expectedCB: (schema, options) => + getDefaultFormState( + testValidator, + schema, + options.rawFormData, + schema, + false, + options.experimental_defaultFormStateBehavior + ), + toEqual: { + localConst: 'local', + RootConst: { + attr1: 1, + attr2: true, + }, + RootAndLocalConst: 'FromLocal', + fromFormData: 'fromFormData', + }, + }, + { + expectedCB: (schema, options) => + getDefaultFormState( + testValidator, + schema, + options.rawFormData, + schema, + false, + options.experimental_defaultFormStateBehavior + ), + toEqual: { + fromFormData: 'fromFormData', + }, + }, + { + expectedCB: (schema, options) => + getDefaultFormState(options.testValidator, schema, options.rawFormData, schema), + toEqual: { + animal: 'Fish', + food: 'worms', + multipleChoicesList: ['a'], + water: null, + }, + }, + { + expectedCB: (schema, options) => + getDefaultFormState( + options.testValidator, + schema, + options.rawFormData, + schema, + undefined, + options.experimental_defaultFormStateBehavior + ), + toEqual: { + animal: 'Fish', + food: 'worms', + multipleChoicesList: ['a'], + water: 'sea', + }, + }, + { + expectedCB: (schema, options) => + getDefaultFormState( + testValidator, + schema, + undefined, + schema, + undefined, + options.experimental_defaultFormStateBehavior + ), + toEqual: { + oneOfField: 'username', + }, + }, + { + expectedCB: (schema, options) => + getDefaultFormState( + testValidator, + schema, + undefined, + schema, + undefined, + options.experimental_defaultFormStateBehavior + ), + toEqual: {}, + }, + { + expectedCB: (schema, options) => + getDefaultFormState( + testValidator, + schema, + undefined, + schema, + undefined, + options.experimental_defaultFormStateBehavior + ), + toEqual: {}, + }, + ]); + // test array defaults + testArrayDefault(testValidator, [ + { + expectedCB: (schema, options) => + getDefaultFormState(testValidator, schema, undefined, schema, options.includeUndefinedValues), + toEqual: ['Raphael', 'Michaelangelo', 'Unknown', 'Unknown'], + }, + { + expectedCB: (schema, options) => + getDefaultFormState( + testValidator, + schema, + undefined, + schema, + options.includeUndefinedValues, + options.experimental_defaultFormStateBehavior + ), + toEqual: ['Raphael', 'Michaelangelo', 'Unknown', 'Unknown'], + }, + { + expectedCB: (schema, options) => + getDefaultFormState(testValidator, schema, undefined, schema, options.includeUndefinedValues), + toEqual: [], + }, + { + expectedCB: (schema, options) => + getDefaultFormState(testValidator, schema, undefined, schema, options.includeUndefinedValues), + toEqual: ['ConstFromRoot', 'ConstFromRoot', 'Constant', 'Constant'], + }, + { + expectedCB: (schema, options) => + getDefaultFormState(testValidator, schema, undefined, schema, options.includeUndefinedValues), + toEqual: [], + }, + { + expectedCB: (schema) => getDefaultFormState(testValidator, schema), + toEqual: [], + }, + ]); + it('getInnerSchemaForArrayItem() item of type boolean returns empty schema', () => { + expect(getInnerSchemaForArrayItem({ items: [true] }, AdditionalItemsHandling.Ignore, 0)).toEqual({}); + }); + describe('resolveDependencies()', () => { + it('test an object with dependencies', () => { const schema: RJSFSchema = { type: 'object', properties: { - requiredProperty: { + first: { type: 'string', - default: 'foo', + enum: ['no', 'yes'], + default: 'no', }, }, - additionalProperties: { - type: 'string', - }, - required: ['requiredProperty'], - default: { - foo: 'bar', + dependencies: { + first: { + oneOf: [ + { + properties: { + first: { + enum: ['yes'], + }, + second: { + type: 'object', + properties: { + deeplyNestedThird: { + type: 'string', + enum: ['before', 'after'], + default: 'before', + }, + }, + }, + }, + }, + { + properties: { + first: { + enum: ['no'], + }, + }, + }, + ], + }, }, }; + + // Mock isValid so that withExactlyOneSubschema works as expected + testValidator.setReturnValues({ + isValid: [ + true, // First oneOf... first === first + false, // Second oneOf... second !== first + ], + }); expect( - getDefaultBasedOnSchemaType( + resolveDependencies( testValidator, schema, - { rootSchema: schema, includeUndefinedValues: true }, - { foo: 'bar' } + schema, + false, + [], + { + first: 'yes', + }, + undefined ) - ).toEqual({ - requiredProperty: 'foo', - foo: 'bar', - }); - }); - it('test an object with additionalProperties type object with defaults and formdata', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - test: { - title: 'Test', - type: 'object', - properties: { - foo: { - type: 'string', - }, + ).toEqual([ + { + type: 'object', + properties: { + first: { + type: 'string', + enum: ['no', 'yes'], + default: 'no', }, - additionalProperties: { + second: { type: 'object', properties: { - host: { - title: 'Host', + deeplyNestedThird: { type: 'string', - default: 'localhost', - }, - port: { - title: 'Port', - type: 'integer', - default: 389, + enum: ['before', 'after'], + default: 'before', }, }, }, }, }, - }; - expect( - getDefaultBasedOnSchemaType(testValidator, schema, { - rootSchema: schema, - rawFormData: { test: { foo: 'x', newKey: {} } }, - }) - ).toEqual({ - test: { - newKey: { - host: 'localhost', - port: 389, - }, - }, - }); + ]); }); - it('test an object with additionalProperties type object with no defaults and formdata', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - test: { - title: 'Test', - type: 'object', - properties: { - foo: { - type: 'string', - }, - }, - additionalProperties: { - type: 'object', - properties: { - host: { - title: 'Host', - type: 'string', - }, - port: { - title: 'Port', - type: 'integer', - }, - }, + }); + describe('computeDefaults()', () => { + // test object defaults + testObjectDefault(testValidator, [ + { + expectedCB: (schema) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { + foo: 42, + }, + }, + { + expectedCB: (schema) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { + test: 'test', + }, + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { requiredProperty: 'foo' }, + }, + { + expectedCB: (schema) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { + requiredProperty: 'foo', + optionalProperty: { nestedRequiredProperty: '' }, + }, + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + optionalProperty: { + nestedRequiredProperty: { + undefinedProperty: undefined, }, }, + requiredProperty: 'foo', }, - }; - expect( - getDefaultBasedOnSchemaType(testValidator, schema, { - rootSchema: schema, - rawFormData: { test: { foo: 'x', newKey: {} } }, - }) - ).toEqual({ - test: { - newKey: {}, + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + optionalNumberProperty: undefined, + optionalObjectProperty: { + nestedRequiredProperty: {}, + }, + requiredProperty: 'foo', }, - }); - }); - it('test an object with additionalProperties type object with no defaults and non-object formdata', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { + }, + { + expectedCB: (schema) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { + requiredProperty: 'foo', + foo: 'bar', + }, + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + requiredProperty: 'foo', + foo: 'bar', + }, + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { test: { - title: 'Test', - type: 'object', - properties: { - foo: { - type: 'string', - }, + newKey: { + host: 'localhost', + port: 389, }, - additionalProperties: { - type: 'object', - properties: { - host: { - title: 'Host', - type: 'string', - }, - port: { - title: 'Port', - type: 'integer', - }, - }, + }, + }, + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + test: { + newKey: {}, + }, + }, + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + nestedObject: { + first: 'no', + second: { + deeplyNestedThird: 'before', }, }, }, - }; - expect( - getDefaultBasedOnSchemaType(testValidator, schema, { - rootSchema: schema, - rawFormData: {}, - }) - ).toEqual({}); - }); - it('test an array with defaults', () => { - const schema: RJSFSchema = { - type: 'array', - minItems: 4, - default: ['Raphael', 'Michaelangelo'], - items: { - type: 'string', - default: 'Unknown', + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { + name: '', }, - }; - - expect( - getDefaultBasedOnSchemaType( - testValidator, - schema, - { + }, + { + expectedCB: (schema) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { + value: [undefined], + }, + }, + { + expectedCB: (schema) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + }), + toEqual: undefined, + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { rootSchema: schema, - includeUndefinedValues: 'excludeObjectChildren', + ...options, + }), + toEqual: { + localConst: 'local', + RootConst: { + attr1: 1, + attr2: true, }, - ['Raphael', 'Michaelangelo'] - ) - ).toEqual(['Raphael', 'Michaelangelo', 'Unknown', 'Unknown']); - }); - it('test an array with no defaults', () => { - const schema: RJSFSchema = { - type: 'array', - minItems: 4, - items: { - type: 'string', + RootAndLocalConst: 'FromLocal', }, - }; - - expect( - getArrayDefaults(testValidator, schema, { - rootSchema: schema, - includeUndefinedValues: 'excludeObjectChildren', - }) - ).toEqual([]); - }); - it('test computeDefaults handles an invalid property schema', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - invalidProperty: 'not a valid property value', + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema, options) => + computeDefaults(options.testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + animal: 'Fish', + food: 'worms', + multipleChoicesList: ['a'], + water: 'sea', }, - } as RJSFSchema; - expect( - getDefaultBasedOnSchemaType(testValidator, schema, { - rootSchema: schema, - includeUndefinedValues: 'excludeObjectChildren', - }) - ).toEqual({}); - }); - it('test with a recursive allof schema', () => { - expect( - getDefaultBasedOnSchemaType(testValidator, RECURSIVE_REF_ALLOF, { rootSchema: RECURSIVE_REF_ALLOF }) - ).toEqual({ - value: [undefined], - }); - }); - it('test computeDefaults returns undefined with simple schema and no optional args', () => { - const schema: RJSFSchema = { type: 'string' }; - expect(getDefaultBasedOnSchemaType(testValidator, schema)).toBe(undefined); - }); + }, + { + expectedCB: (schema, options) => + computeDefaults(options.testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + animal: 'Fish', + food: 'worms', + multipleChoicesList: ['a'], + water: 'sea', + }, + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + oneOfField: 'username', + }, + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + ]); + // test array defaults + testArrayDefault(testValidator, [ + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: ['Raphael', 'Michaelangelo', 'Unknown', 'Unknown'], + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: ['Raphael', 'Michaelangelo', 'Unknown', 'Unknown'], + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: [], + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: ['ConstFromRoot', 'ConstFromRoot', 'Constant', 'Constant'], + }, + { + expectedCB: (schema, options) => + computeDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: [], + }, + { + expectedCB: (schema) => getArrayDefaults(testValidator, schema), + toEqual: [], + }, + ]); }); - describe('getObjectDefaults()', () => { - it('test an object with an optional property that has a nested required property', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - optionalProperty: { - type: 'object', - properties: { - nestedRequiredProperty: { - type: 'string', - }, + describe('getDefaultBasedOnSchemaType()', () => { + // test object defaults + testObjectDefault(testValidator, [ + { + expectedCB: (schema) => + getDefaultBasedOnSchemaType( + testValidator, + schema, + { + rootSchema: schema, }, - required: ['nestedRequiredProperty'], - }, - requiredProperty: { - type: 'string', - default: 'foo', - }, + { + foo: 42, + } + ), + toEqual: undefined, + }, + { + expectedCB: (schema) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { + test: 'test', }, - required: ['requiredProperty'], - }; - - expect(getObjectDefaults(testValidator, schema, { rootSchema: schema })).toEqual({ - requiredProperty: 'foo', - }); - }); - it('test an object with an optional property that has a nested required property with default', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { requiredProperty: 'foo' }, + }, + { + expectedCB: (schema) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { + requiredProperty: 'foo', + optionalProperty: { nestedRequiredProperty: '' }, + }, + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { optionalProperty: { - type: 'object', - properties: { - nestedRequiredProperty: { - type: 'string', - default: '', - }, + nestedRequiredProperty: { + undefinedProperty: undefined, }, - required: ['nestedRequiredProperty'], }, - requiredProperty: { - type: 'string', - default: 'foo', + requiredProperty: 'foo', + }, + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + optionalNumberProperty: undefined, + optionalObjectProperty: { + nestedRequiredProperty: {}, }, + requiredProperty: 'foo', }, - required: ['requiredProperty'], - }; - expect(getObjectDefaults(testValidator, schema, { rootSchema: schema })).toEqual({ - requiredProperty: 'foo', - optionalProperty: { nestedRequiredProperty: '' }, - }); - }); - it('test an object with an optional property that has a nested required property and includeUndefinedValues', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - optionalProperty: { - type: 'object', - properties: { - nestedRequiredProperty: { - type: 'object', - properties: { - undefinedProperty: { - type: 'string', - }, - }, - }, + }, + { + expectedCB: (schema) => + getDefaultBasedOnSchemaType( + testValidator, + schema, + { + rootSchema: schema, }, - required: ['nestedRequiredProperty'], - }, - requiredProperty: { - type: 'string', - default: 'foo', - }, + { foo: 'bar' } + ), + toEqual: { + requiredProperty: 'foo', + foo: 'bar', }, - required: ['requiredProperty'], - }; - expect(getObjectDefaults(testValidator, schema, { rootSchema: schema, includeUndefinedValues: true })).toEqual({ - optionalProperty: { - nestedRequiredProperty: { - undefinedProperty: undefined, + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType( + testValidator, + schema, + { + rootSchema: schema, + ...options, + }, + { foo: 'bar' } + ), + toEqual: { + requiredProperty: 'foo', + foo: 'bar', + }, + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + test: { + newKey: { + host: 'localhost', + port: 389, + }, }, }, - requiredProperty: 'foo', - }); - }); - it("test an object with an optional property that has a nested required property and includeUndefinedValues is 'excludeObjectChildren'", () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - optionalNumberProperty: { - type: 'number', + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + test: { + newKey: {}, }, - optionalObjectProperty: { - type: 'object', - properties: { - nestedRequiredProperty: { - type: 'object', - properties: { - undefinedProperty: { - type: 'string', - }, - }, - }, + }, + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + nestedObject: { + first: 'no', + second: { + deeplyNestedThird: 'before', }, - required: ['nestedRequiredProperty'], - }, - requiredProperty: { - type: 'string', - default: 'foo', }, }, - required: ['requiredProperty'], - }; - expect( - getObjectDefaults(testValidator, schema, { - rootSchema: schema, - includeUndefinedValues: 'excludeObjectChildren', - }) - ).toEqual({ - optionalNumberProperty: undefined, - optionalObjectProperty: { - nestedRequiredProperty: {}, + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + }), + toEqual: undefined, + }, + { + expectedCB: (schema) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { + value: [undefined], }, - requiredProperty: 'foo', - }); - }); - it('test an object const value populate as field defaults', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - localConst: { - type: 'string', - const: 'local', - }, + }, + { + expectedCB: (schema) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + }), + toEqual: undefined, + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + localConst: 'local', RootConst: { - type: 'object', - properties: { - attr1: { - type: 'number', - }, - attr2: { - type: 'boolean', - }, - }, - const: { - attr1: 1, - attr2: true, - }, - }, - fromFormData: { - type: 'string', - default: 'notUsed', - }, - RootAndLocalConst: { - type: 'string', - const: 'FromLocal', + attr1: 1, + attr2: true, }, + RootAndLocalConst: 'FromLocal', }, - const: { - RootAndLocalConst: 'FromRoot', + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(options.testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + animal: 'Fish', }, - }; - expect( - getObjectDefaults(testValidator, schema, { - rootSchema: schema, - experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipDefaults' }, - rawFormData: { - fromFormData: 'fromFormData', - }, - }) - ).toEqual({ - localConst: 'local', - RootConst: { - attr1: 1, - attr2: true, + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(options.testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + animal: 'Fish', }, - RootAndLocalConst: 'FromLocal', - }); - }); - it('test an object const value NOT populate as field defaults when constAsDefault is never', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - localConst: { - type: 'string', - const: 'local', - }, - RootConst: { - type: 'object', - properties: { - attr1: { - type: 'number', - }, - attr2: { - type: 'boolean', - }, + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + oneOfField: 'username', + }, + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + ]); + // test array defaults + testArrayDefault(testValidator, [ + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType( + testValidator, + schema, + { + rootSchema: schema, + ...options, + }, + ['Raphael', 'Michaelangelo'] + ), + toEqual: ['Raphael', 'Michaelangelo', 'Unknown', 'Unknown'], + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType( + testValidator, + schema, + { + rootSchema: schema, + ...options, + }, + ['Raphael', 'Michaelangelo'] + ), + toEqual: ['Raphael', 'Michaelangelo', 'Unknown', 'Unknown'], + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: [], + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType( + testValidator, + schema, + { + rootSchema: schema, + ...options, + }, + ['ConstFromRoot', 'ConstFromRoot'] + ), + toEqual: ['ConstFromRoot', 'ConstFromRoot', 'Constant', 'Constant'], + }, + { + expectedCB: (schema, options) => + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: [], + }, + { + expectedCB: (schema) => getDefaultBasedOnSchemaType(testValidator, schema), + toEqual: [], + }, + ]); + }); + describe('getObjectDefaults()', () => { + // test object defaults + testObjectDefault(testValidator, [ + { + expectedCB: (schema) => + getObjectDefaults( + testValidator, + schema, + { + rootSchema: schema, }, - const: { - attr1: 1, - attr2: true, + { + foo: 42, + } + ), + toEqual: {}, + }, + { + expectedCB: (schema) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { + test: 'test', + }, + }, + { + expectedCB: (schema, options) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { requiredProperty: 'foo' }, + }, + { + expectedCB: (schema) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { + requiredProperty: 'foo', + optionalProperty: { nestedRequiredProperty: '' }, + }, + }, + { + expectedCB: (schema, options) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + optionalProperty: { + nestedRequiredProperty: { + undefinedProperty: undefined, }, }, - fromFormData: { - type: 'string', - default: 'notUsed', - }, - RootAndLocalConst: { - type: 'string', - const: 'FromLocal', - }, + requiredProperty: 'foo', }, - const: { - RootAndLocalConst: 'FromRoot', - }, - }; - expect( - getObjectDefaults(testValidator, schema, { - rootSchema: schema, - experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipDefaults', constAsDefaults: 'never' }, - rawFormData: { - fromFormData: 'fromFormData', - }, - }) - ).toEqual({}); - }); - it('test an object with an additionalProperties', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - requiredProperty: { - type: 'string', - default: 'foo', + }, + { + expectedCB: (schema, options) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + optionalNumberProperty: undefined, + optionalObjectProperty: { + nestedRequiredProperty: {}, }, + requiredProperty: 'foo', }, - additionalProperties: true, - required: ['requiredProperty'], - default: { + }, + { + expectedCB: (schema) => + getObjectDefaults( + testValidator, + schema, + { + rootSchema: schema, + }, + { foo: 'bar' } + ), + toEqual: { + requiredProperty: 'foo', foo: 'bar', }, - }; - expect(getObjectDefaults(testValidator, schema, { rootSchema: schema }, { foo: 'bar' })).toEqual({ - requiredProperty: 'foo', - foo: 'bar', - }); - }); - it('test an object with an additionalProperties and includeUndefinedValues', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - requiredProperty: { - type: 'string', - default: 'foo', - }, - }, - additionalProperties: { - type: 'string', - }, - required: ['requiredProperty'], - default: { + }, + { + expectedCB: (schema, options) => + getObjectDefaults( + testValidator, + schema, + { + rootSchema: schema, + ...options, + }, + { foo: 'bar' } + ), + toEqual: { + requiredProperty: 'foo', foo: 'bar', }, - }; - expect( - getObjectDefaults( - testValidator, - schema, - { + }, + { + expectedCB: (schema, options) => + getObjectDefaults(testValidator, schema, { rootSchema: schema, - includeUndefinedValues: true, - }, - { - foo: 'bar', - } - ) - ).toEqual({ requiredProperty: 'foo', foo: 'bar' }); - }); - it('test an object with additionalProperties type object with defaults and formdata', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { + ...options, + }), + toEqual: { test: { - title: 'Test', - type: 'object', - properties: { - foo: { - type: 'string', - }, - }, - additionalProperties: { - type: 'object', - properties: { - host: { - title: 'Host', - type: 'string', - default: 'localhost', - }, - port: { - title: 'Port', - type: 'integer', - default: 389, - }, - }, + newKey: { + host: 'localhost', + port: 389, }, }, }, - }; - expect( - getObjectDefaults(testValidator, schema, { - rootSchema: schema, - rawFormData: { test: { foo: 'x', newKey: {} } }, - }) - ).toEqual({ - test: { - newKey: { - host: 'localhost', - port: 389, - }, - }, - }); - }); - it('test an object with additionalProperties type object with no defaults and formdata', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { + }, + { + expectedCB: (schema, options) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { test: { - title: 'Test', - type: 'object', - properties: { - foo: { - type: 'string', - }, - }, - additionalProperties: { - type: 'object', - properties: { - host: { - title: 'Host', - type: 'string', - }, - port: { - title: 'Port', - type: 'integer', - }, - }, - }, + newKey: {}, }, }, - }; - expect( - getObjectDefaults(testValidator, schema, { - rootSchema: schema, - rawFormData: { test: { foo: 'x', newKey: {} } }, - }) - ).toEqual({ - test: { - newKey: {}, - }, - }); - }); - it('test an object with additionalProperties type object with no defaults and non-object formdata', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - test: { - title: 'Test', - type: 'object', - properties: { - foo: { - type: 'string', - }, - }, - additionalProperties: { - type: 'object', - properties: { - host: { - title: 'Host', - type: 'string', - }, - port: { - title: 'Port', - type: 'integer', - }, - }, + }, + { + expectedCB: (schema, options) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema, options) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + nestedObject: { + first: 'no', + second: { + deeplyNestedThird: 'before', }, }, }, - }; - expect( - getObjectDefaults(testValidator, schema, { - rootSchema: schema, - rawFormData: {}, - }) - ).toEqual({}); - }); - it('test computeDefaults handles an invalid property schema', () => { - const schema: RJSFSchema = { - type: 'object', - properties: { - invalidProperty: 'not a valid property value', - }, - } as RJSFSchema; - expect( - getObjectDefaults(testValidator, schema, { - rootSchema: schema, - includeUndefinedValues: 'excludeObjectChildren', - }) - ).toEqual({}); - }); - it('test with a recursive allof schema', () => { - expect(getObjectDefaults(testValidator, RECURSIVE_REF_ALLOF, { rootSchema: RECURSIVE_REF_ALLOF })).toEqual({ - value: [undefined], - }); - }); - it('test computeDefaults returns undefined with simple schema and no optional args', () => { - const schema: RJSFSchema = { type: 'object' }; - expect(getObjectDefaults(testValidator, schema)).toStrictEqual({}); - }); - }); - describe('getArrayDefaults()', () => { - it('test an array with defaults', () => { - const schema: RJSFSchema = { - type: 'array', - minItems: 4, - default: ['Raphael', 'Michaelangelo'], - items: { - type: 'string', - default: 'Unknown', + }, + { + expectedCB: (schema, options) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + }), + toEqual: {}, + }, + { + expectedCB: (schema) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + }), + toEqual: { + value: [undefined], }, - }; - - expect( - getArrayDefaults( - testValidator, - schema, - { + }, + { + expectedCB: (schema) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + }), + toEqual: {}, + }, + { + expectedCB: (schema, options) => + getObjectDefaults(testValidator, schema, { rootSchema: schema, - includeUndefinedValues: 'excludeObjectChildren', + ...options, + }), + toEqual: { + localConst: 'local', + RootConst: { + attr1: 1, + attr2: true, }, - ['Raphael', 'Michaelangelo'] - ) - ).toEqual(['Raphael', 'Michaelangelo', 'Unknown', 'Unknown']); - }); - it('test an array const value populate as defaults', () => { - const schema: RJSFSchema = { - type: 'array', - minItems: 4, - const: ['ConstFromRoot', 'ConstFromRoot'], - items: { - type: 'string', - const: 'Constant', + RootAndLocalConst: 'FromLocal', }, - }; - - expect( - getArrayDefaults( - testValidator, - schema, - { + }, + { + expectedCB: (schema, options) => + getObjectDefaults(testValidator, schema, { rootSchema: schema, - includeUndefinedValues: 'excludeObjectChildren', - }, - ['ConstFromRoot', 'ConstFromRoot'] - ) - ).toEqual(['ConstFromRoot', 'ConstFromRoot', 'Constant', 'Constant']); - }); - it('test an array const value NOT populate as defaults when constAsDefaults is never', () => { - const schema: RJSFSchema = { - type: 'array', - minItems: 4, - const: ['ConstFromRoot', 'ConstFromRoot'], - items: { - type: 'string', - const: 'Constant', + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema, options) => + getObjectDefaults(options.testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + animal: 'Fish', }, - }; - - expect( - getArrayDefaults( - testValidator, - schema, - { + }, + { + expectedCB: (schema, options) => + getObjectDefaults(options.testValidator, schema, { rootSchema: schema, - includeUndefinedValues: 'excludeObjectChildren', - experimental_defaultFormStateBehavior: { constAsDefaults: 'never' }, - }, - ['ConstFromRoot', 'ConstFromRoot'] - ) - ).toEqual(['ConstFromRoot', 'ConstFromRoot']); - }); - it('test an array with no defaults', () => { - const schema: RJSFSchema = { - type: 'array', - minItems: 4, - items: { - type: 'string', + ...options, + }), + toEqual: { + animal: 'Fish', + }, + }, + { + expectedCB: (schema, options) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: { + oneOfField: 'username', }, + }, + { + expectedCB: (schema, options) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + { + expectedCB: (schema, options) => + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: {}, + }, + ]); + }); + describe('getArrayDefaults()', () => { + // test array defaults + testArrayDefault(testValidator, [ + { + expectedCB: (schema, options) => + getArrayDefaults( + testValidator, + schema, + { + rootSchema: schema, + ...options, + }, + ['Raphael', 'Michaelangelo'] + ), + toEqual: ['Raphael', 'Michaelangelo', 'Unknown', 'Unknown'], + }, + { + expectedCB: (schema, options) => + getArrayDefaults( + testValidator, + schema, + { + rootSchema: schema, + ...options, + }, + ['Raphael', 'Michaelangelo'] + ), + toEqual: ['Raphael', 'Michaelangelo', 'Unknown', 'Unknown'], + }, + { + expectedCB: (schema, options) => + getArrayDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: [], + }, + { + expectedCB: (schema, options) => + getArrayDefaults( + testValidator, + schema, + { + rootSchema: schema, + ...options, + }, + ['ConstFromRoot', 'ConstFromRoot'] + ), + toEqual: ['ConstFromRoot', 'ConstFromRoot', 'Constant', 'Constant'], + }, + { + expectedCB: (schema, options) => + getArrayDefaults(testValidator, schema, { + rootSchema: schema, + ...options, + }), + toEqual: [], + }, + { + expectedCB: (schema) => getArrayDefaults(testValidator, schema), + toEqual: [], + }, + ]); + }); + describe('getValidFormData', () => { + let schema: RJSFSchema; + it('Test schema with non valid formData for enum property', () => { + schema = { + type: 'string', + enum: ['a', 'b', 'c'], }; - expect( - getArrayDefaults(testValidator, schema, { - rootSchema: schema, - includeUndefinedValues: 'excludeObjectChildren', - }) - ).toEqual([]); + expect(ensureFormDataMatchingSchema(testValidator, schema, schema, 'd')).toBeUndefined(); }); - it('test computeDefaults handles an invalid array schema', () => { - const schema: RJSFSchema = { - type: 'array', - items: 'not a valid item value', - } as RJSFSchema; - expect( - getArrayDefaults(testValidator, schema, { - rootSchema: schema, - includeUndefinedValues: 'excludeObjectChildren', - }) - ).toEqual([]); + it('Test schema with valid formData for enum property', () => { + expect(ensureFormDataMatchingSchema(testValidator, schema, schema, 'b')).toEqual('b'); }); - it('test computeDefaults returns undefined with simple schema and no optional args', () => { - const schema: RJSFSchema = { type: 'array' }; - expect(getArrayDefaults(testValidator, schema)).toStrictEqual([]); + it('Test schema with const property', () => { + schema = { + type: 'string', + enum: ['a', 'b', 'c'], + const: 'a', + }; + + expect(ensureFormDataMatchingSchema(testValidator, schema, schema, 'a')).toEqual('a'); }); }); describe('default form state behavior: ignore min items unless required', () => { @@ -4312,7 +4651,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType requiredArray: ['raw0'], }); }); - it('should combine ALL defaults with raw form data for a required array property with minItems', () => { + it('should combine ALL defaults with raw form data for a array property with minItems', () => { const schema: RJSFSchema = { type: 'object', properties: { diff --git a/packages/utils/test/schema/types.ts b/packages/utils/test/schema/types.ts index 02136856bc..1c74a9d074 100644 --- a/packages/utils/test/schema/types.ts +++ b/packages/utils/test/schema/types.ts @@ -1,4 +1,4 @@ -import { RJSFValidationError, ValidationData, ValidatorType } from '../../src'; +import { RJSFSchema, RJSFValidationError, ValidationData, ValidatorType } from '../../src'; export interface TestValidatorParams { isValid?: boolean[]; @@ -6,6 +6,12 @@ export interface TestValidatorParams { errorList?: RJSFValidationError[][]; } +export interface IExpectType { + // eslint-disable-next-line no-unused-vars + expectedCB: (schema: RJSFSchema, options?: any) => unknown; + toEqual: any; +} + export interface TestValidatorType extends ValidatorType { // eslint-disable-next-line no-unused-vars setReturnValues(params?: TestValidatorParams): void;