import type { ChangeEventHandler } from 'react';
import React, { version as ReactVersion, useEffect, useRef, useState } from 'react';
import { AlertFilled } from '@ant-design/icons';
import type { ColProps } from 'antd/es/grid';
import classNames from 'classnames';
import scrollIntoView from 'scroll-into-view-if-needed';
import type { FormInstance } from '..';
import Form from '..';
import { resetWarned } from '../../_util/warning';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { act, fireEvent, pureRender, render, screen, waitFakeTimer } from '../../../tests/utils';
import Button from '../../button';
import Cascader from '../../cascader';
import Checkbox from '../../checkbox';
import ColorPicker from '../../color-picker';
import ConfigProvider from '../../config-provider';
import DatePicker from '../../date-picker';
import Drawer from '../../drawer';
import Input from '../../input';
import InputNumber from '../../input-number';
import zhCN from '../../locale/zh_CN';
import Modal from '../../modal';
import Radio from '../../radio';
import Select from '../../select';
import Slider from '../../slider';
import Switch from '../../switch';
import TreeSelect from '../../tree-select';
import Upload from '../../upload';
import type { NamePath } from '../interface';
import * as Util from '../util';
const { RangePicker } = DatePicker;
const { TextArea } = Input;
jest.mock('scroll-into-view-if-needed');
describe('Form', () => {
mountTest(Form);
mountTest(Form.Item);
rtlTest(Form);
rtlTest(Form.Item);
(scrollIntoView as any).mockImplementation(() => {});
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const changeValue = async (
input: HTMLElement | null | number,
value: string,
advTimer = 1000,
) => {
let element: HTMLElement;
if (typeof input === 'number') {
element = document.querySelectorAll('input')[input];
}
expect(element!).toBeTruthy();
fireEvent.change(element!, {
target: {
value,
},
});
if (advTimer) {
await waitFakeTimer(advTimer / 20, 20);
}
};
beforeEach(() => {
document.body.innerHTML = '';
jest.useFakeTimers();
(scrollIntoView as any).mockReset();
});
afterEach(() => {
errorSpy.mockReset();
});
afterAll(() => {
jest.clearAllTimers();
jest.useRealTimers();
errorSpy.mockRestore();
warnSpy.mockRestore();
(scrollIntoView as any).mockRestore();
});
describe('noStyle Form.Item', () => {
it('should show alert when form field is required but empty', async () => {
const onChange = jest.fn();
const { container } = render(
,
);
// user type something and clear
await changeValue(0, 'test');
await changeValue(0, '');
// should show alert with correct message and show correct styles
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent(
"'test' is required",
);
expect(container.querySelector('.ant-input-status-error')).toBeTruthy();
expect(container.querySelector('.ant-form-item-has-error')).toBeTruthy();
expect(onChange).toHaveBeenCalled();
});
it('should clean up', async () => {
const Demo: React.FC = () => {
const [form] = Form.useForm();
const onChange = async () => {
// Wait a while and then some logic to validate
await waitFakeTimer();
try {
await form.validateFields();
} catch {
// do nothing
}
};
return (
{() => {
const aaa = form.getFieldValue('aaa');
if (aaa === '1') {
return (
);
}
return (
);
}}
);
};
const { container } = render();
await changeValue(0, '1');
await waitFakeTimer(2000, 2000);
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent('aaa');
await changeValue(0, '2');
await waitFakeTimer(2000, 2000);
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent('ccc');
await changeValue(0, '1');
await waitFakeTimer(2000, 2000);
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent('aaa');
});
// https://github.com/ant-design/ant-design/issues/41620
it('should not throw error when `help=false` and `noStyle=true`', async () => {
const App: React.FC<{ help?: React.ReactNode }> = (props) => {
const { help = false } = props || {};
return (
);
};
const { container, getByRole, rerender } = render();
// click submit to trigger validate
fireEvent.click(getByRole('button'));
await waitFakeTimer();
expect(container.querySelectorAll('.ant-form-item-explain-error')).toHaveLength(1);
// When noStyle=true but help is not false, help will be displayed
rerender();
await waitFakeTimer();
fireEvent.click(getByRole('button'));
await waitFakeTimer();
expect(container.querySelectorAll('.ant-form-item-explain-error')).toHaveLength(3);
});
});
it('render functions require either `shouldUpdate` or `dependencies`', () => {
render(
{() => null}
,
);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Form.Item] A `Form.Item` with a render function must have either `shouldUpdate` or `dependencies`.',
);
});
it("`shouldUpdate` shouldn't work with `dependencies`", () => {
render(
{() => null}
,
);
expect(errorSpy).toHaveBeenCalledWith(
"Warning: [antd: Form.Item] `shouldUpdate` and `dependencies` shouldn't be used together. See https://u.ant.design/form-deps.",
);
});
it('`name` should not work with render props', () => {
render(
{() => null}
,
);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Form.Item] A `Form.Item` with a render function cannot be a field, and thus cannot have a `name` prop.',
);
});
it('multiple children with a name prop', () => {
render(
one
two
,
);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Form.Item] A `Form.Item` with a `name` prop must have a single child element. For information on how to render more complex form items, see https://u.ant.design/complex-form-item.',
);
});
it('input element should have the prop aria-describedby pointing to the help id when there is a help message', () => {
const { container } = pureRender(
,
);
expect(container.querySelector('input')?.getAttribute('aria-describedby')).toBe('test_help');
expect(container.querySelector('.ant-form-item-explain')?.id).toBe('test_help');
});
it('input element should not have the prop aria-describedby pointing to the help id when there is a help message and name is not defined', () => {
const { container } = render(
,
);
expect(container.querySelector('input')?.getAttribute('aria-describedby')).toBeFalsy();
expect(container.querySelector('.ant-form-item-explain')?.id).toBeFalsy();
});
it('input element should have the prop aria-describedby concatenated with the form name pointing to the help id when there is a help message', () => {
const { container } = render(
,
);
expect(container.querySelector('input')?.getAttribute('aria-describedby')).toBe(
'form_test_help',
);
expect(container.querySelector('.ant-form-item-explain')?.id).toBe('form_test_help');
});
it('input element should have the prop aria-describedby pointing to the help id when there are errors', async () => {
const { container } = pureRender(
,
);
await changeValue(0, 'Invalid number');
expect(container.querySelector('input')?.getAttribute('aria-describedby')).toBe('test_help');
expect(container.querySelector('.ant-form-item-explain')?.id).toBe('test_help');
});
it('input element should have the prop aria-invalid when there are errors', async () => {
const { container } = render(
,
);
await changeValue(0, 'Invalid number');
expect(container.querySelector('input')?.getAttribute('aria-invalid')).toBe('true');
});
it('input element should have the prop aria-required when the prop `required` is true', () => {
const { container } = render(
,
);
expect(container.querySelector('input')?.getAttribute('aria-required')).toBe('true');
});
it('input element should have the prop aria-required when there is a rule with required', () => {
const { container } = render(
,
);
expect(container.querySelector('input')?.getAttribute('aria-required')).toBe('true');
});
it('input element should have the prop aria-describedby pointing to the extra id when there is a extra message', () => {
const { container } = render(
,
);
expect(container.querySelector('input')?.getAttribute('aria-describedby')).toBe('test_extra');
expect(container.querySelector('.ant-form-item-extra')?.id).toBe('test_extra');
});
it('input element should not have the prop aria-describedby pointing to the extra id when there is a extra message and name is not defined', () => {
const { container } = render(
,
);
expect(container.querySelector('input')?.getAttribute('aria-describedby')).toBeFalsy();
expect(container.querySelector('.ant-form-item-extra')?.id).toBeFalsy();
});
it('input element should have the prop aria-describedby pointing to the help and extra id when there is a help and extra message', () => {
const { container } = render(
,
);
expect(container.querySelector('input')?.getAttribute('aria-describedby')).toBe(
'test_help test_extra',
);
});
describe('scrollToField', () => {
const test = (name: string, genForm: () => any) => {
it(name, () => {
let callGetForm: any;
const Demo: React.FC = () => {
const { props, getForm } = genForm();
callGetForm = getForm;
return (
);
};
render();
expect(scrollIntoView).not.toHaveBeenCalled();
const form = callGetForm();
form.scrollToField('test', {
block: 'start',
});
const inputNode = document.getElementById('scroll_test');
expect(scrollIntoView).toHaveBeenCalledWith(inputNode, {
block: 'start',
scrollMode: 'if-needed',
});
});
};
// hooks
test('useForm', () => {
const [form] = Form.useForm();
return {
props: { form },
getForm: () => form,
};
});
// ref
test('ref', () => {
let form: any;
return {
props: {
ref: (instance: any) => {
form = instance;
},
},
getForm: () => form,
};
});
});
describe('scrollToFirstError', () => {
it('should work with scrollToFirstError', async () => {
const onFinishFailed = jest.fn();
const { container } = render(
,
);
expect(scrollIntoView).not.toHaveBeenCalled();
fireEvent.submit(container.querySelector('form')!);
await waitFakeTimer();
const inputNode = document.getElementById('test');
expect(scrollIntoView).toHaveBeenCalledWith(inputNode, {
block: 'center',
scrollMode: 'if-needed',
});
expect(onFinishFailed).toHaveBeenCalled();
});
it('should work with scrollToFirstError with ref', async () => {
const ForwardRefInput = React.forwardRef(({ id, ...props }, ref) => (
));
const NativeInput = React.forwardRef(({ id, ...props }, ref) => {
const internalRef = React.useRef(null);
React.useImperativeHandle(ref, () => ({
nativeElement: internalRef.current,
}));
return ;
});
const NormalInput = (props: any) => ;
const { getByRole, getAllByRole } = render(
,
);
// click submit to trigger validate
const allInputs = getAllByRole('textbox');
const button = getByRole('button');
expect(allInputs).toHaveLength(3);
fireEvent.click(button);
await waitFakeTimer();
expect(scrollIntoView).toHaveBeenNthCalledWith(1, allInputs[0], expect.any(Object));
// change the value of the first input
fireEvent.change(allInputs[0], { target: { value: '123' } });
fireEvent.click(button);
await waitFakeTimer();
expect(scrollIntoView).toHaveBeenNthCalledWith(2, allInputs[1], expect.any(Object));
// change the value of the second input
fireEvent.change(allInputs[1], { target: { value: 'abc' } });
fireEvent.click(button);
await waitFakeTimer();
expect(scrollIntoView).toHaveBeenNthCalledWith(3, allInputs[2], expect.any(Object));
expect(scrollIntoView).toHaveBeenCalledTimes(3);
});
// https://github.com/ant-design/ant-design/issues/28869
it('should work with Upload', async () => {
const uploadRef = React.createRef();
const { getByRole } = render(
(Array.isArray(e) ? e : e?.fileList)}
rules={[{ required: true }]}
>
,
);
fireEvent.click(getByRole('button'));
await waitFakeTimer();
expect(scrollIntoView).toHaveBeenCalled();
expect((scrollIntoView as any).mock.calls[0][0]).toBe(uploadRef.current.nativeElement);
});
// https://github.com/ant-design/ant-design/issues/48981
it('should not throw error when use InputNumber', async () => {
const inputNumberRef = React.createRef();
const { getByText } = render(
,
);
fireEvent.click(getByText('Submit'));
await waitFakeTimer();
expect(scrollIntoView).toHaveBeenCalled();
expect((scrollIntoView as any).mock.calls[0][0]).toBe(inputNumberRef.current?.nativeElement);
});
});
it('Form.Item should support data-*、aria-* and custom attribute', () => {
const { container } = render(
text
,
);
expect(container.firstChild).toMatchSnapshot();
});
it('warning when use `name` but children is not validate element', () => {
render(
text
,
);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Form.Item] `name` is only used for validate React element. If you are using Form.Item as layout display, please remove `name` instead.',
);
});
it('No warning when use noStyle and children is empty', () => {
render(
,
);
expect(errorSpy).not.toHaveBeenCalled();
});
it('dynamic change required', async () => {
const { container } = render(
({ required: getFieldValue('light') })]}
>
,
);
// should not show alert by default
expect(container.querySelector('.ant-form-item-explain')).toBeFalsy();
// click to change the light field value to true
fireEvent.click(container.querySelector('input')!);
await waitFakeTimer();
// user input something and clear
await changeValue(1, '1');
await changeValue(1, '');
// should show alert says that the field is required
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent(
"'bamboo' is required",
);
});
describe('should show related className when customize help', () => {
it('normal', async () => {
const { container } = render(
,
);
await waitFakeTimer();
expect(container.querySelector('.ant-form-item-explain')).toHaveTextContent('good');
expect(container.querySelector('.ant-form-item-with-help')).toBeTruthy();
});
it('empty string', async () => {
const { container } = render(
,
);
await waitFakeTimer();
expect(container.querySelector('.ant-form-item-explain')).toHaveTextContent('');
expect(container.querySelector('.ant-form-item-with-help')).toBeTruthy();
});
});
it('warning when use v3 function', () => {
Form.create();
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Form] antd v4 removed `Form.create`. Please remove or use `@ant-design/compatible` instead.',
);
});
// https://github.com/ant-design/ant-design/issues/20706
it('Error change should work', async () => {
const { container } = render(
{
if (value === 'p') {
return Promise.reject(new Error('not a p'));
}
return Promise.resolve();
},
},
]}
>
,
);
/* eslint-disable no-await-in-loop */
for (let i = 0; i < 3; i += 1) {
await changeValue(0, 'bamboo');
await changeValue(0, '');
expect(container.querySelector('.ant-form-item-explain')?.textContent).toEqual(
"'name' is required",
);
await changeValue(0, 'p');
expect(container.querySelector('.ant-form-item-explain')?.textContent).toEqual('not a p');
}
/* eslint-enable */
});
// https://github.com/ant-design/ant-design/issues/20813
it('should update help directly when provided', async () => {
const App: React.FC = () => {
const [message, updateMessage] = React.useState('');
return (