fix: Form List warning (#24867)

* fix: Form List warning

* clean reaf
This commit is contained in:
二货机器人 2020-06-09 18:10:43 +08:00 committed by GitHub
parent 84381e3e6c
commit a9a651ffd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 189 additions and 148 deletions

View File

@ -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);
}

View File

@ -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(
<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 () => {
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');

View 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'] });
});
});

View File

@ -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];

View File

@ -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));