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(
{/* @ts-ignore */}
, ); 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 (
); }; const { container } = pureRender(); await waitFakeTimer(); expect(container.querySelector('#changed')!.value).toEqual(''); expect(shouldNotRender).toHaveBeenCalledTimes(1); expect(shouldRender).toHaveBeenCalledTimes(1); fireEvent.click(container.querySelector('#fill-btn')!); await waitFakeTimer(); expect(shouldNotRender).toHaveBeenCalledTimes(1); expect(shouldRender).toHaveBeenLastCalledWith('bamboo'); expect(shouldRender).toHaveBeenCalledTimes(2); }); it('empty help should also render', () => { const { container } = render( , ); expect(container.querySelectorAll('.ant-form-item-explain').length).toBeTruthy(); }); it('Form.Item with `help` should display error style when validate failed', async () => { const { container } = render(
, ); await changeValue(0, ''); expect(container.querySelector('.ant-form-item')).toHaveClass('ant-form-item-has-error'); expect(container.querySelector('.ant-form-item-explain')!.textContent).toEqual('help'); }); it('clear validation message when', async () => { const { container } = render(
, ); await changeValue(0, '1'); expect(container.querySelectorAll('.ant-form-item-explain').length).toBeFalsy(); await changeValue(0, ''); expect(container.querySelectorAll('.ant-form-item-explain').length).toBeTruthy(); await changeValue(0, '123'); expect(container.querySelectorAll('.ant-form-item-explain').length).toBeFalsy(); }); // https://github.com/ant-design/ant-design/issues/21167 it('`require` without `name`', () => { const { container } = render( , ); // expect(screen.getByTitle('test')).toHaveClass('ant-form-item-required'); expect(container.querySelector('.ant-form-item-required')).toBeTruthy(); }); it('0 is a validate Field', () => { render( , ); // if getByLabelText can get element, then it is a validate field with form control and label expect(screen.getByLabelText('0')).toBeInTheDocument(); }); it('`null` triggers warning and is treated as `undefined`', () => { render( , ); // if getByLabelText can get element, then it is a validate field with form control and label expect(screen.queryByLabelText('test')).not.toBeInTheDocument(); expect(errorSpy).toHaveBeenCalledWith( 'Warning: [antd: Form.Item] `null` is passed as `name` property', ); }); // https://github.com/ant-design/ant-design/issues/21415 it('should not throw error when Component.props.onChange is null', async () => { const CustomComponent: React.FC = () => ( } /> ); render(
, ); await changeValue(0, 'aaa'); }); it('change `help` should not warning', async () => { const Demo: React.FC = () => { const [error, setError] = React.useState(false); return (
); }; const { container } = render(); fireEvent.click(container.querySelector('button')!); expect(errorSpy).not.toHaveBeenCalled(); }); it('`label` support template', async () => { const { container } = render( // eslint-disable-next-line no-template-curly-in-string
, ); fireEvent.submit(container.querySelector('form')!); await waitFakeTimer(); expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent( 'Bamboo is good!', ); }); // https://github.com/ant-design/ant-design/issues/33691 it('should keep upper locale in nested ConfigProvider', async () => { const { container } = render(
, ); fireEvent.submit(container.querySelector('form')!); await waitFakeTimer(); expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent( '请输入Bamboo', ); }); it('`name` support template when label is not provided', async () => { const { container } = render( // eslint-disable-next-line no-template-curly-in-string
, ); fireEvent.submit(container.querySelector('form')!); await waitFakeTimer(); expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent( 'Bamboo is good!', ); }); it('`messageVariables` support validate', async () => { const { container } = render( // eslint-disable-next-line no-template-curly-in-string
, ); fireEvent.submit(container.querySelector('form')!); await waitFakeTimer(); expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent( 'Bamboo is good!', ); }); it('validation message should has alert role', async () => { // https://github.com/ant-design/ant-design/issues/25711 const { container } = render( // eslint-disable-next-line no-template-curly-in-string
, ); fireEvent.submit(container.querySelector('form')!); await waitFakeTimer(); expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent( 'name is good!', ); }); it('return same form instance', async () => { const instances = new Set(); const App: React.FC = () => { const [form] = Form.useForm(); instances.add(form); const [, forceUpdate] = React.useState({}); return ( ); }; const { container } = pureRender(); for (let i = 0; i < 5; i += 1) { fireEvent.click(container.querySelector('button')!); // eslint-disable-next-line no-await-in-loop await waitFakeTimer(); } expect(instances.size).toBe(1); }); it('should avoid re-render', async () => { let renderTimes = 0; const MyInput: React.FC<{ value?: string }> = ({ value = '', ...props }) => { renderTimes += 1; return ; }; const Demo: React.FC = () => (
); const { container } = pureRender(); renderTimes = 0; await changeValue(0, 'a'); expect(renderTimes).toEqual(1); expect(container.querySelector('input')).toHaveValue('a'); }); it('should warning with `defaultValue`', () => { render(
, ); expect(errorSpy).toHaveBeenCalledWith( 'Warning: [antd: Form.Item] `defaultValue` will not work on controlled Field. You should use `initialValues` of Form instead.', ); }); it('should remove Field and also reset error', async () => { const Demo: React.FC<{ showA?: boolean }> = ({ showA }) => (
{showA ? ( ) : ( )}
); const { container, rerender } = render(); await waitFakeTimer(); expect(container.querySelector('.ant-form-item-explain')).toBeTruthy(); rerender(); await waitFakeTimer(); expect(container.querySelector('.ant-form-item-explain')).toBeFalsy(); }); it('no warning of initialValue & getValueProps & preserve', () => { render(
({})} preserve={false}>
, ); expect(errorSpy).not.toHaveBeenCalled(); }); it('should customize id when pass with id', () => { const { container } = render(
, ); expect(container.querySelector('input')!.id).toEqual('bamboo'); }); it('should trigger validate when onBlur when pass validateTrigger onBlur', async () => { const { container } = render(
, ); // type a invalidate value, not trigger validation await changeValue(0, '7777'); expect(container.querySelector('.ant-form-item-explain')).toBeFalsy(); // tab(onBlur) the input field, trigger and see the alert fireEvent.blur(container.querySelector('input')!); await waitFakeTimer(); expect(container.querySelector('.ant-form-item-explain')).toBeTruthy(); }); describe('Form item hidden', () => { it('should work', () => { const { container } = render(
, ); expect(container.firstChild).toMatchSnapshot(); }); it('noStyle should not work when hidden', () => { const { container } = render(
, ); expect(container.firstChild).toMatchSnapshot(); }); }); it('legacy hideRequiredMark', () => { const { container } = render(
, ); expect(container.querySelector('form')!).toHaveClass('ant-form-hide-required-mark'); }); it('form should support disabled', () => { const App: React.FC = () => (
disabled Apple Pear