浏览代码

fix: [Form] validate race condition problem when using fieldProps.rules (#1376)

* fix: [Form] validate race condition problem
* chore: unit test for validate race condition
Cong-Cong Pan 2 年之前
父节点
当前提交
f4956245ca
共有 2 个文件被更改,包括 57 次插入1 次删除
  1. 45 0
      packages/semi-ui/form/__test__/field.test.js
  2. 12 1
      packages/semi-ui/form/hoc/withField.tsx

+ 45 - 0
packages/semi-ui/form/__test__/field.test.js

@@ -2,6 +2,9 @@ import { Form, Select } from '../../index';
 import { noop } from 'lodash';
 import { noop } from 'lodash';
 import { func } from 'prop-types';
 import { func } from 'prop-types';
 import { BASE_CLASS_PREFIX } from '../../../semi-foundation/base/constants';
 import { BASE_CLASS_PREFIX } from '../../../semi-foundation/base/constants';
+import { sleep as baseSleep } from '../../_test_/utils/index';
+
+const sleep = (ms = 200) => baseSleep(ms);
 
 
 function getForm(props) {
 function getForm(props) {
     return mount(<Form {...props}></Form>);
     return mount(<Form {...props}></Form>);
@@ -414,6 +417,48 @@ describe('Form-field', () => {
         expect(fieldsDOM.at(5).instance().value).toEqual('f');
         expect(fieldsDOM.at(5).instance().value).toEqual('f');
     });
     });
 
 
+    it('validate race condition', async () => {
+        let formApi = null;
+        let asyncValidatorCallback = null;
+
+        const fieldProps = {
+            field: 'text',
+            rules: [
+                { type: 'string', max: 10 },
+                {
+                    asyncValidator(rule, value, callback) {
+                        if (!asyncValidatorCallback) {
+                            asyncValidatorCallback = callback;
+                        } else {
+                            callback();
+                        }
+                    }
+                }
+            ]
+        }
+        const props = {
+            getFormApi(api) {
+                formApi = api;
+            },
+            children: getInput(fieldProps),
+        };
+        const form = getForm(props);
+
+        const event1 = { target: { value: 'semi' } };
+        form.find(`.${BASE_CLASS_PREFIX}-input`).simulate('change', event1);
+        await sleep(200);
+        form.update();
+        expect(formApi.getError('text')).toBeUndefined();
+
+        const event2 = { target: { value: 'Prefer knowledge to wealth, for the one is transitory, the other perpetual.' } };
+        form.find(`.${BASE_CLASS_PREFIX}-input`).simulate('change', event2);
+        await sleep(200);
+        asyncValidatorCallback();
+        await sleep(200);
+        form.update();
+        expect(formApi.getError('text')).not.toBeUndefined();
+    });
+
     // TODO
     // TODO
     // it('allowEmptyString', () => {});
     // it('allowEmptyString', () => {});
     // it('extraText')
     // it('extraText')

+ 12 - 1
packages/semi-ui/form/hoc/withField.tsx

@@ -128,6 +128,7 @@ function withField<
 
 
         const rulesRef = useRef(rules);
         const rulesRef = useRef(rules);
         const validateRef = useRef(validate);
         const validateRef = useRef(validate);
+        const validatePromise = useRef<Promise<any> | null>(null);
 
 
         // notNotify is true means that the onChange of the Form does not need to be triggered
         // notNotify is true means that the onChange of the Form does not need to be triggered
         // notUpdate is true means that this operation does not need to trigger the forceUpdate
         // notUpdate is true means that this operation does not need to trigger the forceUpdate
@@ -182,7 +183,7 @@ function withField<
                 [field]: val,
                 [field]: val,
             };
             };
 
 
-            return new Promise((resolve, reject) => {
+            const rootPromise = new Promise((resolve, reject) => {
                 validator
                 validator
                     .validate(
                     .validate(
                         model,
                         model,
@@ -193,12 +194,18 @@ function withField<
                         (errors, fields) => {}
                         (errors, fields) => {}
                     )
                     )
                     .then(res => {
                     .then(res => {
+                        if (validatePromise.current !== rootPromise) {
+                            return;
+                        }
                         // validation passed
                         // validation passed
                         setStatus('success');
                         setStatus('success');
                         updateError(undefined, callOpts);
                         updateError(undefined, callOpts);
                         resolve({});
                         resolve({});
                     })
                     })
                     .catch(err => {
                     .catch(err => {
+                        if (validatePromise.current !== rootPromise) {
+                            return;
+                        }
                         let { errors, fields } = err;
                         let { errors, fields } = err;
                         if (errors && fields) {
                         if (errors && fields) {
                             let messages = errors.map((e: any) => e.message);
                             let messages = errors.map((e: any) => e.message);
@@ -220,6 +227,10 @@ function withField<
                         }
                         }
                     });
                     });
             });
             });
+
+            validatePromise.current = rootPromise;
+
+            return rootPromise;
         };
         };
 
 
         // execute custom validate function
         // execute custom validate function