mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-07 09:26:06 +08:00
parent
84381e3e6c
commit
a9a651ffd5
@ -258,7 +258,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
|||||||
nameRef.current = [...mergedName];
|
nameRef.current = [...mergedName];
|
||||||
if (fieldKey) {
|
if (fieldKey) {
|
||||||
const fieldKeys = Array.isArray(fieldKey) ? fieldKey : [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);
|
updateItemErrors(nameRef.current.join('__SPLIT__'), errors);
|
||||||
}
|
}
|
||||||
|
@ -10,11 +10,6 @@ import { sleep } from '../../../tests/utils';
|
|||||||
|
|
||||||
jest.mock('scroll-into-view-if-needed');
|
jest.mock('scroll-into-view-if-needed');
|
||||||
|
|
||||||
const delay = (timeout = 0) =>
|
|
||||||
new Promise(resolve => {
|
|
||||||
setTimeout(resolve, timeout);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Form', () => {
|
describe('Form', () => {
|
||||||
mountTest(Form);
|
mountTest(Form);
|
||||||
mountTest(Form.Item);
|
mountTest(Form.Item);
|
||||||
@ -27,7 +22,7 @@ describe('Form', () => {
|
|||||||
|
|
||||||
async function change(wrapper, index, value) {
|
async function change(wrapper, index, value) {
|
||||||
wrapper.find(Input).at(index).simulate('change', { target: { value } });
|
wrapper.find(Input).at(index).simulate('change', { target: { value } });
|
||||||
await delay(50);
|
await sleep(100);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,133 +40,6 @@ describe('Form', () => {
|
|||||||
scrollIntoView.mockRestore();
|
scrollIntoView.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('List', () => {
|
|
||||||
function testList(name, renderField) {
|
|
||||||
it(name, async () => {
|
|
||||||
const wrapper = mount(
|
|
||||||
<Form>
|
|
||||||
<Form.List name="list">
|
|
||||||
{(fields, { add, remove }) => (
|
|
||||||
<>
|
|
||||||
{fields.map(field => renderField(field))}
|
|
||||||
<Button className="add" onClick={add}>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="remove"
|
|
||||||
onClick={() => {
|
|
||||||
remove(1);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form.List>
|
|
||||||
</Form>,
|
|
||||||
);
|
|
||||||
|
|
||||||
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 => (
|
|
||||||
<Form.Item {...field} rules={[{ required: true }]}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
));
|
|
||||||
|
|
||||||
testList('nest noStyle', field => (
|
|
||||||
<Form.Item key={field.key}>
|
|
||||||
<Form.Item noStyle {...field} rules={[{ required: true }]}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</Form.Item>
|
|
||||||
));
|
|
||||||
|
|
||||||
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(
|
|
||||||
<Form
|
|
||||||
onFinish={v => {
|
|
||||||
if (typeof v.list[0] === 'object') {
|
|
||||||
/* old version led to SyntheticEvent be passed as an value here
|
|
||||||
that led to weird infinite loop somewhere and OutOfMemory crash */
|
|
||||||
v = new Error('We expect value to be a primitive here');
|
|
||||||
}
|
|
||||||
onFinish(v);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form.List name="list">
|
|
||||||
{(fields, { add, remove }) => (
|
|
||||||
<>
|
|
||||||
{fields.map(field => (
|
|
||||||
// key is in a field
|
|
||||||
// eslint-disable-next-line react/jsx-key
|
|
||||||
<Form.Item {...field}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
<Button className="add" onClick={add}>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
<Button className="remove" onClick={() => remove(0)}>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form.List>
|
|
||||||
</Form>,
|
|
||||||
);
|
|
||||||
|
|
||||||
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 () => {
|
it('noStyle Form.Item', async () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
|
|
||||||
@ -300,7 +168,7 @@ describe('Form', () => {
|
|||||||
|
|
||||||
expect(scrollIntoView).not.toHaveBeenCalled();
|
expect(scrollIntoView).not.toHaveBeenCalled();
|
||||||
wrapper.find('form').simulate('submit');
|
wrapper.find('form').simulate('submit');
|
||||||
await delay(50);
|
await sleep(50);
|
||||||
expect(scrollIntoView).toHaveBeenCalled();
|
expect(scrollIntoView).toHaveBeenCalled();
|
||||||
expect(onFinishFailed).toHaveBeenCalled();
|
expect(onFinishFailed).toHaveBeenCalled();
|
||||||
|
|
||||||
@ -401,7 +269,7 @@ describe('Form', () => {
|
|||||||
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual("'name' is required");
|
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual("'name' is required");
|
||||||
|
|
||||||
await change(wrapper, 0, 'p');
|
await change(wrapper, 0, 'p');
|
||||||
await delay(100);
|
await sleep(100);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('not a p');
|
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('not a p');
|
||||||
}
|
}
|
||||||
@ -585,9 +453,9 @@ describe('Form', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
wrapper.find('form').simulate('submit');
|
wrapper.find('form').simulate('submit');
|
||||||
await delay(100);
|
await sleep(100);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
await delay(100);
|
await sleep(100);
|
||||||
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('Bamboo is good!');
|
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(renderTimes).toEqual(1);
|
||||||
expect(wrapper.find('input').props().value).toEqual('a');
|
expect(wrapper.find('input').props().value).toEqual('a');
|
||||||
|
167
components/form/__tests__/list.test.js
Normal file
167
components/form/__tests__/list.test.js
Normal file
@ -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(
|
||||||
|
<Form>
|
||||||
|
<Form.List name="list">
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
{fields.map(field => renderField(field))}
|
||||||
|
<Button className="add" onClick={add}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="remove-0"
|
||||||
|
onClick={() => {
|
||||||
|
remove(0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="remove-1"
|
||||||
|
onClick={() => {
|
||||||
|
remove(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</Form>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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 => (
|
||||||
|
<Form.Item {...field} rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
));
|
||||||
|
|
||||||
|
testList('nest noStyle', field => (
|
||||||
|
<Form.Item key={field.key}>
|
||||||
|
<Form.Item noStyle {...field} rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Form.Item>
|
||||||
|
));
|
||||||
|
|
||||||
|
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(
|
||||||
|
<Form
|
||||||
|
onFinish={v => {
|
||||||
|
if (typeof v.list[0] === 'object') {
|
||||||
|
/* old version led to SyntheticEvent be passed as an value here
|
||||||
|
that led to weird infinite loop somewhere and OutOfMemory crash */
|
||||||
|
v = new Error('We expect value to be a primitive here');
|
||||||
|
}
|
||||||
|
onFinish(v);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.List name="list">
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
{fields.map(field => (
|
||||||
|
// key is in a field
|
||||||
|
// eslint-disable-next-line react/jsx-key
|
||||||
|
<Form.Item {...field}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Button className="add" onClick={add}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
<Button className="remove" onClick={() => remove(0)}>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</Form>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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'] });
|
||||||
|
});
|
||||||
|
});
|
@ -21,19 +21,19 @@ export function useCacheErrors(
|
|||||||
|
|
||||||
const [, forceUpdate] = React.useState({});
|
const [, forceUpdate] = React.useState({});
|
||||||
|
|
||||||
const update = () => {
|
const update = (newErrors: React.ReactNode[]) => {
|
||||||
const prevVisible = cacheRef.current.visible;
|
const prevVisible = cacheRef.current.visible;
|
||||||
const newVisible = !!errors.length;
|
const newVisible = !!newErrors.length;
|
||||||
|
|
||||||
const prevErrors = cacheRef.current.errors;
|
const prevErrors = cacheRef.current.errors;
|
||||||
cacheRef.current.errors = errors;
|
cacheRef.current.errors = newErrors;
|
||||||
cacheRef.current.visible = newVisible;
|
cacheRef.current.visible = newVisible;
|
||||||
|
|
||||||
if (prevVisible !== newVisible) {
|
if (prevVisible !== newVisible) {
|
||||||
changeTrigger(newVisible);
|
changeTrigger(newVisible);
|
||||||
} else if (
|
} else if (
|
||||||
prevErrors.length !== errors.length ||
|
prevErrors.length !== newErrors.length ||
|
||||||
prevErrors.some((prevErr, index) => prevErr !== errors[index])
|
prevErrors.some((prevErr, index) => prevErr !== newErrors[index])
|
||||||
) {
|
) {
|
||||||
forceUpdate({});
|
forceUpdate({});
|
||||||
}
|
}
|
||||||
@ -41,13 +41,17 @@ export function useCacheErrors(
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!directly) {
|
if (!directly) {
|
||||||
const timeout = setTimeout(update, 10);
|
const timeout = setTimeout(() => {
|
||||||
return () => clearTimeout(timeout);
|
update(errors);
|
||||||
|
}, 10);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}, [errors]);
|
}, [errors]);
|
||||||
|
|
||||||
if (directly) {
|
if (directly) {
|
||||||
update();
|
update(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [cacheRef.current.visible, cacheRef.current.errors];
|
return [cacheRef.current.visible, cacheRef.current.errors];
|
||||||
|
@ -8,4 +8,6 @@ export function resetMockDate() {
|
|||||||
MockDate.reset();
|
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));
|
||||||
|
Loading…
Reference in New Issue
Block a user