diff --git a/components/form/FormItem.tsx b/components/form/FormItem.tsx
index b4807c176d..077c2ebf23 100644
--- a/components/form/FormItem.tsx
+++ b/components/form/FormItem.tsx
@@ -258,7 +258,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
nameRef.current = [...mergedName];
if (fieldKey) {
const fieldKeys = Array.isArray(fieldKey) ? fieldKey : [fieldKey];
- nameRef.current = [...mergedName.slice(-1), ...fieldKeys];
+ nameRef.current = [...mergedName.slice(0, -1), ...fieldKeys];
}
updateItemErrors(nameRef.current.join('__SPLIT__'), errors);
}
diff --git a/components/form/__tests__/index.test.js b/components/form/__tests__/index.test.js
index 4e84bc26e5..b80f563a7f 100644
--- a/components/form/__tests__/index.test.js
+++ b/components/form/__tests__/index.test.js
@@ -10,11 +10,6 @@ import { sleep } from '../../../tests/utils';
jest.mock('scroll-into-view-if-needed');
-const delay = (timeout = 0) =>
- new Promise(resolve => {
- setTimeout(resolve, timeout);
- });
-
describe('Form', () => {
mountTest(Form);
mountTest(Form.Item);
@@ -27,7 +22,7 @@ describe('Form', () => {
async function change(wrapper, index, value) {
wrapper.find(Input).at(index).simulate('change', { target: { value } });
- await delay(50);
+ await sleep(100);
wrapper.update();
}
@@ -45,133 +40,6 @@ describe('Form', () => {
scrollIntoView.mockRestore();
});
- describe('List', () => {
- function testList(name, renderField) {
- it(name, async () => {
- const wrapper = mount(
-
- {(fields, { add, remove }) => (
- <>
- {fields.map(field => renderField(field))}
-
-
- >
- )}
-
- ,
- );
-
- async function operate(className) {
- wrapper.find(className).last().simulate('click');
- await delay();
- wrapper.update();
- }
-
- await operate('.add');
- expect(wrapper.find(Input).length).toBe(1);
-
- await operate('.add');
- expect(wrapper.find(Input).length).toBe(2);
-
- await change(wrapper, 1, '');
- wrapper.update();
- await sleep(300);
- expect(wrapper.find('.ant-form-item-explain').length).toBe(1);
-
- await operate('.remove');
- wrapper.update();
- expect(wrapper.find(Input).length).toBe(1);
- expect(wrapper.find('.ant-form-item-explain').length).toBe(0);
- });
- }
-
- testList('operation correctly', field => (
-
-
-
- ));
-
- testList('nest noStyle', field => (
-
-
-
-
-
- ));
-
- it('correct onFinish values', async () => {
- async function click(wrapper, className) {
- wrapper.find(className).last().simulate('click');
- await delay();
- wrapper.update();
- }
-
- const onFinish = jest.fn().mockImplementation(() => {});
-
- const wrapper = mount(
-
- {(fields, { add, remove }) => (
- <>
- {fields.map(field => (
- // key is in a field
- // eslint-disable-next-line react/jsx-key
-
-
-
- ))}
-
-
- >
- )}
-
- ,
- );
-
- await click(wrapper, '.add');
- await change(wrapper, 0, 'input1');
- wrapper.find('form').simulate('submit');
- await delay();
- expect(onFinish).toHaveBeenLastCalledWith({ list: ['input1'] });
-
- await click(wrapper, '.add');
- await change(wrapper, 1, 'input2');
- await click(wrapper, '.add');
- await change(wrapper, 2, 'input3');
- wrapper.find('form').simulate('submit');
- await delay();
- expect(onFinish).toHaveBeenLastCalledWith({ list: ['input1', 'input2', 'input3'] });
-
- await click(wrapper, '.remove'); // will remove first input
- wrapper.find('form').simulate('submit');
- await delay();
- expect(onFinish).toHaveBeenLastCalledWith({ list: ['input2', 'input3'] });
- });
- });
-
it('noStyle Form.Item', async () => {
const onChange = jest.fn();
@@ -300,7 +168,7 @@ describe('Form', () => {
expect(scrollIntoView).not.toHaveBeenCalled();
wrapper.find('form').simulate('submit');
- await delay(50);
+ await sleep(50);
expect(scrollIntoView).toHaveBeenCalled();
expect(onFinishFailed).toHaveBeenCalled();
@@ -401,7 +269,7 @@ describe('Form', () => {
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual("'name' is required");
await change(wrapper, 0, 'p');
- await delay(100);
+ await sleep(100);
wrapper.update();
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('not a p');
}
@@ -585,9 +453,9 @@ describe('Form', () => {
);
wrapper.find('form').simulate('submit');
- await delay(100);
+ await sleep(100);
wrapper.update();
- await delay(100);
+ await sleep(100);
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('Bamboo is good!');
});
@@ -642,7 +510,7 @@ describe('Form', () => {
},
});
- await delay();
+ await sleep();
expect(renderTimes).toEqual(1);
expect(wrapper.find('input').props().value).toEqual('a');
diff --git a/components/form/__tests__/list.test.js b/components/form/__tests__/list.test.js
new file mode 100644
index 0000000000..d966649a0b
--- /dev/null
+++ b/components/form/__tests__/list.test.js
@@ -0,0 +1,167 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import raf from 'raf';
+import { act } from 'react-dom/test-utils';
+import Form from '..';
+import Input from '../../input';
+import Button from '../../button';
+import { sleep } from '../../../tests/utils';
+
+jest.mock('raf');
+
+describe('Form.List', () => {
+ raf.mockImplementation(callback => window.setTimeout(callback));
+
+ afterAll(() => {
+ raf.mockRestore();
+ });
+
+ async function change(wrapper, index, value) {
+ wrapper.find(Input).at(index).simulate('change', { target: { value } });
+ await sleep();
+ wrapper.update();
+ }
+
+ function testList(name, renderField) {
+ it(name, async () => {
+ jest.useFakeTimers();
+
+ const wrapper = mount(
+
+ {(fields, { add, remove }) => (
+ <>
+ {fields.map(field => renderField(field))}
+
+
+ ,
+ );
+
+ function operate(className) {
+ act(() => {
+ wrapper.find(className).last().simulate('click');
+ jest.runAllTimers();
+ });
+ wrapper.update();
+ }
+
+ operate('.add');
+ expect(wrapper.find(Input).length).toBe(1);
+
+ operate('.add');
+ expect(wrapper.find(Input).length).toBe(2);
+
+ operate('.add');
+ expect(wrapper.find(Input).length).toBe(3);
+
+ await change(wrapper, 2, '');
+ act(() => {
+ jest.runAllTimers();
+ });
+ wrapper.update();
+ expect(wrapper.find('.ant-form-item-explain div').length).toBe(1);
+
+ operate('.remove-0');
+ expect(wrapper.find(Input).length).toBe(2);
+ expect(wrapper.find('.ant-form-item-explain div').length).toBe(1);
+
+ operate('.remove-1');
+ expect(wrapper.find(Input).length).toBe(1);
+ expect(wrapper.find('.ant-form-item-explain div').length).toBe(0);
+
+ jest.useRealTimers();
+ });
+ }
+
+ testList('operation correctly', field => (
+
+
+
+ ));
+
+ testList('nest noStyle', field => (
+
+
+
+
+
+ ));
+
+ it('correct onFinish values', async () => {
+ async function click(wrapper, className) {
+ wrapper.find(className).last().simulate('click');
+ await sleep();
+ wrapper.update();
+ }
+
+ const onFinish = jest.fn().mockImplementation(() => {});
+
+ const wrapper = mount(
+
+ {(fields, { add, remove }) => (
+ <>
+ {fields.map(field => (
+ // key is in a field
+ // eslint-disable-next-line react/jsx-key
+
+
+
+ ))}
+
+ Add
+
+ remove(0)}>
+ Remove
+
+ >
+ )}
+
+ ,
+ );
+
+ await click(wrapper, '.add');
+ await change(wrapper, 0, 'input1');
+ wrapper.find('form').simulate('submit');
+ await sleep();
+ expect(onFinish).toHaveBeenLastCalledWith({ list: ['input1'] });
+
+ await click(wrapper, '.add');
+ await change(wrapper, 1, 'input2');
+ await click(wrapper, '.add');
+ await change(wrapper, 2, 'input3');
+ wrapper.find('form').simulate('submit');
+ await sleep();
+ expect(onFinish).toHaveBeenLastCalledWith({ list: ['input1', 'input2', 'input3'] });
+
+ await click(wrapper, '.remove'); // will remove first input
+ wrapper.find('form').simulate('submit');
+ await sleep();
+ expect(onFinish).toHaveBeenLastCalledWith({ list: ['input2', 'input3'] });
+ });
+});
diff --git a/components/form/util.ts b/components/form/util.ts
index 2ab1242cb6..93fd896890 100644
--- a/components/form/util.ts
+++ b/components/form/util.ts
@@ -21,19 +21,19 @@ export function useCacheErrors(
const [, forceUpdate] = React.useState({});
- const update = () => {
+ const update = (newErrors: React.ReactNode[]) => {
const prevVisible = cacheRef.current.visible;
- const newVisible = !!errors.length;
+ const newVisible = !!newErrors.length;
const prevErrors = cacheRef.current.errors;
- cacheRef.current.errors = errors;
+ cacheRef.current.errors = newErrors;
cacheRef.current.visible = newVisible;
if (prevVisible !== newVisible) {
changeTrigger(newVisible);
} else if (
- prevErrors.length !== errors.length ||
- prevErrors.some((prevErr, index) => prevErr !== errors[index])
+ prevErrors.length !== newErrors.length ||
+ prevErrors.some((prevErr, index) => prevErr !== newErrors[index])
) {
forceUpdate({});
}
@@ -41,13 +41,17 @@ export function useCacheErrors(
React.useEffect(() => {
if (!directly) {
- const timeout = setTimeout(update, 10);
- return () => clearTimeout(timeout);
+ const timeout = setTimeout(() => {
+ update(errors);
+ }, 10);
+ return () => {
+ clearTimeout(timeout);
+ };
}
}, [errors]);
if (directly) {
- update();
+ update(errors);
}
return [cacheRef.current.visible, cacheRef.current.errors];
diff --git a/tests/utils.ts b/tests/utils.ts
index 53b05cc741..c6914d0afd 100644
--- a/tests/utils.ts
+++ b/tests/utils.ts
@@ -8,4 +8,6 @@ export function resetMockDate() {
MockDate.reset();
}
-export const sleep = (timeout = 0) => new Promise(resolve => setTimeout(resolve, timeout));
+const globalTimeout = global.setTimeout;
+
+export const sleep = (timeout = 0) => new Promise(resolve => globalTimeout(resolve, timeout));