[Form lib] Fix regression on field not being validated after reset to its default value. (#76379)

This commit is contained in:
Sébastien Loix 2020-09-03 08:58:05 +02:00 committed by GitHub
parent 210d6f2df1
commit 182e0de18f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 157 additions and 9 deletions

View file

@ -64,6 +64,93 @@ describe('<UseField />', () => {
});
});
describe('validation', () => {
let formHook: FormHook | null = null;
beforeEach(() => {
formHook = null;
});
const onFormHook = (form: FormHook) => {
formHook = form;
};
const getTestComp = (fieldConfig: FieldConfig) => {
const TestComp = ({ onForm }: { onForm: (form: FormHook) => void }) => {
const { form } = useForm<any>();
useEffect(() => {
onForm(form);
}, [onForm, form]);
return (
<Form form={form}>
<UseField path="name" config={fieldConfig} data-test-subj="myField" />
</Form>
);
};
return TestComp;
};
const setup = (fieldConfig: FieldConfig) => {
return registerTestBed(getTestComp(fieldConfig), {
memoryRouter: { wrapComponent: false },
defaultProps: { onForm: onFormHook },
})() as TestBed;
};
test('should update the form validity whenever the field value changes', async () => {
const fieldConfig: FieldConfig = {
defaultValue: '', // empty string, which is not valid
validations: [
{
validator: ({ value }) => {
// Validate that string is not empty
if ((value as string).trim() === '') {
return { message: 'Error: field is empty.' };
}
},
},
],
};
// Mount our TestComponent
const {
form: { setInputValue },
} = setup(fieldConfig);
if (formHook === null) {
throw new Error('FormHook object has not been set.');
}
let { isValid } = formHook;
expect(isValid).toBeUndefined(); // Initially the form validity is undefined...
await act(async () => {
await formHook!.validate(); // ...until we validate the form
});
({ isValid } = formHook);
expect(isValid).toBe(false);
// Change to a non empty string to pass validation
await act(async () => {
setInputValue('myField', 'changedValue');
});
({ isValid } = formHook);
expect(isValid).toBe(true);
// Change back to an empty string to fail validation
await act(async () => {
setInputValue('myField', '');
});
({ isValid } = formHook);
expect(isValid).toBe(false);
});
});
describe('serializer(), deserializer(), formatter()', () => {
interface MyForm {
name: string;

View file

@ -69,11 +69,12 @@ export const useField = <T>(
const [isChangingValue, setIsChangingValue] = useState(false);
const [isValidated, setIsValidated] = useState(false);
const isMounted = useRef<boolean>(false);
const validateCounter = useRef(0);
const changeCounter = useRef(0);
const hasBeenReset = useRef<boolean>(false);
const inflightValidation = useRef<Promise<any> | null>(null);
const debounceTimeout = useRef<NodeJS.Timeout | null>(null);
const isMounted = useRef<boolean>(false);
// -- HELPERS
// ----------------------------------
@ -142,11 +143,7 @@ export const useField = <T>(
__updateFormDataAt(path, value);
// Validate field(s) (that will update form.isValid state)
// We only validate if the value is different than the initial or default value
// to avoid validating after a form.reset() call.
if (value !== initialValue && value !== defaultValue) {
await __validateFields(fieldsToValidateOnChange ?? [path]);
}
await __validateFields(fieldsToValidateOnChange ?? [path]);
if (isMounted.current === false) {
return;
@ -172,8 +169,6 @@ export const useField = <T>(
}, [
path,
value,
defaultValue,
initialValue,
valueChangeListener,
errorDisplayDelay,
fieldsToValidateOnChange,
@ -468,6 +463,7 @@ export const useField = <T>(
setErrors([]);
if (resetValue) {
hasBeenReset.current = true;
const newValue = deserializeValue(updatedDefaultValue ?? defaultValue);
setValue(newValue);
return newValue;
@ -539,6 +535,13 @@ export const useField = <T>(
}, [path, __removeField]);
useEffect(() => {
// If the field value has been reset, we don't want to call the "onValueChange()"
// as it will set the "isPristine" state to true or validate the field, which initially we don't want.
if (hasBeenReset.current) {
hasBeenReset.current = false;
return;
}
if (!isMounted.current) {
return;
}

View file

@ -22,7 +22,13 @@ import { act } from 'react-dom/test-utils';
import { registerTestBed, getRandomString, TestBed } from '../shared_imports';
import { Form, UseField } from '../components';
import { FormSubmitHandler, OnUpdateHandler, FormHook, ValidationFunc } from '../types';
import {
FormSubmitHandler,
OnUpdateHandler,
FormHook,
ValidationFunc,
FieldConfig,
} from '../types';
import { useForm } from './use_form';
interface MyForm {
@ -441,5 +447,57 @@ describe('useForm() hook', () => {
deeply: { nested: { value: '' } }, // Fallback to empty string as no config was provided
});
});
test('should not validate the fields after resetting its value (form validity should be undefined)', async () => {
const fieldConfig: FieldConfig = {
defaultValue: '',
validations: [
{
validator: ({ value }) => {
if ((value as string).trim() === '') {
return { message: 'Error: empty string' };
}
},
},
],
};
const TestResetComp = () => {
const { form } = useForm();
useEffect(() => {
formHook = form;
}, [form]);
return (
<Form form={form}>
<UseField path="username" config={fieldConfig} data-test-subj="myField" />
</Form>
);
};
const {
form: { setInputValue },
} = registerTestBed(TestResetComp, {
memoryRouter: { wrapComponent: false },
})() as TestBed;
let { isValid } = formHook!;
expect(isValid).toBeUndefined();
await act(async () => {
setInputValue('myField', 'changedValue');
});
({ isValid } = formHook!);
expect(isValid).toBe(true);
await act(async () => {
// When we reset the form, value is back to "", which is invalid for the field
formHook!.reset();
});
({ isValid } = formHook!);
expect(isValid).toBeUndefined(); // Make sure it is "undefined" and not "false".
});
});
});