import type { ChangeEventHandler } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import scrollIntoView from 'scroll-into-view-if-needed';
import classNames from 'classnames';
import type { ColProps } from 'antd/es/grid';
import type { FormInstance } from '..';
import Form from '..';
import * as Util from '../util';
import Button from '../../button';
import Input from '../../input';
import Select from '../../select';
import Upload from '../../upload';
import Cascader from '../../cascader';
import Checkbox from '../../checkbox';
import DatePicker from '../../date-picker';
import InputNumber from '../../input-number';
import Radio from '../../radio';
import Switch from '../../switch';
import TreeSelect from '../../tree-select';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, pureRender, render, screen, waitFakeTimer } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import Drawer from '../../drawer';
import zhCN from '../../locale/zh_CN';
import Modal from '../../modal';
import type { NamePath } from '../interface';
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 (err) {
// 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');
});
});
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,
};
});
});
it('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('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('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 (