import type { ChangeEventHandler } from 'react'; import React, { useState } from 'react'; import scrollIntoView from 'scroll-into-view-if-needed'; import userEvent from '@testing-library/user-event'; 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, render, sleep, act, screen, pureRender, 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 change = async ( container: ReturnType['container'], index: number, value: string, executeMockTimer: boolean, ) => { fireEvent.change(container.querySelectorAll('input')?.[index], { target: { value } }); await sleep(200); if (executeMockTimer) { for (let i = 0; i < 10; i += 1) { act(() => { jest.runAllTimers(); }); } await sleep(1); } }; beforeEach(() => { jest.useRealTimers(); (scrollIntoView as any).mockReset(); }); afterEach(() => { errorSpy.mockReset(); }); afterAll(() => { 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 userEvent.type(screen.getByLabelText('test'), 'test'); await userEvent.clear(screen.getByLabelText('test')); // should show alert with correct message and show correct styles await expect(screen.findByRole('alert')).resolves.toHaveTextContent("'test' is required"); expect(screen.getByLabelText('test')).toHaveClass('ant-input-status-error'); expect(container.querySelectorAll('.ant-form-item-has-error').length).toBeTruthy(); expect(onChange).toHaveBeenCalled(); }); it('should clean up', async () => { jest.useFakeTimers(); const Demo: React.FC = () => { const [form] = Form.useForm(); return (
{ await sleep(0); try { await form.validateFields(); } catch { // do nothing } }} /> {() => { const aaa = form.getFieldValue('aaa'); if (aaa === '1') { return ( ); } return ( ); }}
); }; const { container } = render(); await change(container, 0, '1', true); expect(screen.getByRole('alert')).toHaveTextContent('aaa'); await change(container, 0, '2', true); expect(screen.getByRole('alert')).toHaveTextContent('ccc'); await change(container, 0, '1', true); expect(screen.getByRole('alert')).toHaveTextContent('aaa'); jest.useRealTimers(); }); }); it('`shouldUpdate` should work with render props', () => { render(
{() => null}
, ); expect(errorSpy).toHaveBeenCalledWith( 'Warning: [antd: Form.Item] `children` of render props only work with `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://ant.design/components/form/#dependencies.", ); }); it('`name` should not work with render props', () => { render(
{() => null}
, ); expect(errorSpy).toHaveBeenCalledWith( "Warning: [antd: Form.Item] Do not use `name` with `children` of render props since it's not a field.", ); }); it('children is array has name props', () => { render(
one
two
, ); expect(errorSpy).toHaveBeenCalledWith( 'Warning: [antd: Form.Item] `children` is array of render props cannot have `name`.', ); }); 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 () => { jest.useFakeTimers(); const { container } = pureRender(
, ); fireEvent.change(container.querySelector('input')!, { target: { value: 'Invalid number' } }); await waitFakeTimer(); expect(container.querySelector('input')?.getAttribute('aria-describedby')).toBe('test_help'); expect(container.querySelector('.ant-form-item-explain')?.id).toBe('test_help'); jest.clearAllTimers(); jest.useRealTimers(); }); it('input element should have the prop aria-invalid when there are errors', async () => { const { container } = render(
, ); fireEvent.change(container.querySelector('input')!, { target: { value: 'Invalid number' } }); await sleep(800); 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(); render(
, ); expect(scrollIntoView).not.toHaveBeenCalled(); await userEvent.click(screen.getByRole('button', { name: /submit/i })); 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 () => { render(
({ required: getFieldValue('light') })]} >
, ); // should not show alert by default expect(screen.queryByRole('alert')).not.toBeInTheDocument(); // click to change the light field value to true await userEvent.click(screen.getByLabelText('light')); // user input something and clear await userEvent.type(screen.getByLabelText('bamboo'), '1'); await userEvent.clear(screen.getByLabelText('bamboo')); // should show alert says that the field is required await expect(screen.findByRole('alert')).resolves.toHaveTextContent("'bamboo' is required"); }); it('should show alert with string when help is non-empty string', async () => { render(
, ); await expect(screen.findByRole('alert')).resolves.toHaveTextContent('good'); }); it('should show alert with empty string when help is empty string', async () => { render(
, ); await expect(screen.findByRole('alert')).resolves.toHaveTextContent(''); }); describe('should show related className when customize help', () => { it('normal', () => { const { container } = render(
, ); expect(container.querySelector('.ant-form-item-with-help')).toBeTruthy(); }); it('empty string', () => { const { container } = render(
, ); 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 () => { jest.useFakeTimers(); 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 change(container, 0, 'bamboo', true); await change(container, 0, '', true); expect(container.querySelector('.ant-form-item-explain')?.textContent).toEqual( "'name' is required", ); await change(container, 0, 'p', true); await sleep(100); expect(container.querySelector('.ant-form-item-explain')?.textContent).toEqual('not a p'); } /* eslint-enable */ jest.useRealTimers(); }); // 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 (
); }; render(); await userEvent.click(screen.getByRole('button')); expect(errorSpy).not.toHaveBeenCalled(); }); it('`label` support template', async () => { render( // eslint-disable-next-line no-template-curly-in-string
, ); await userEvent.click(screen.getByRole('button')); await expect(screen.findByRole('alert')).resolves.toHaveTextContent('Bamboo is good!'); }); // https://github.com/ant-design/ant-design/issues/33691 it('should keep upper locale in nested ConfigProvider', async () => { render(
, ); await userEvent.click(screen.getByRole('button')); await expect(screen.findByRole('alert')).resolves.toHaveTextContent('请输入Bamboo'); }); it('`name` support template when label is not provided', async () => { render( // eslint-disable-next-line no-template-curly-in-string
, ); await userEvent.click(screen.getByRole('button')); await expect(screen.findByRole('alert')).resolves.toHaveTextContent('Bamboo is good!'); }); it('`messageVariables` support validate', async () => { render( // eslint-disable-next-line no-template-curly-in-string
, ); await userEvent.click(screen.getByRole('button')); await expect(screen.findByRole('alert')).resolves.toHaveTextContent('Bamboo is good!'); }); it('validation message should has alert role', async () => { // https://github.com/ant-design/ant-design/issues/25711 render( // eslint-disable-next-line no-template-curly-in-string
, ); await userEvent.click(screen.getByRole('button')); await expect(screen.findByRole('alert')).resolves.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 ( ); }; pureRender(); for (let i = 0; i < 5; i += 1) { // eslint-disable-next-line no-await-in-loop await userEvent.click(screen.getByRole('button')); } 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 = () => (
); pureRender(); renderTimes = 0; jest.clearAllMocks(); fireEvent.change(screen.getByLabelText('username'), { target: { value: 'a' } }); expect(renderTimes).toEqual(1); expect(screen.getByLabelText('username')).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 { rerender } = render(); await expect(screen.findByRole('alert')).resolves.toBeInTheDocument(); rerender(); expect(screen.queryByRole('alert')).not.toBeInTheDocument(); }); it('no warning of initialValue & getValueProps & preserve', () => { render(
({})} preserve={false}>
, ); expect(errorSpy).not.toHaveBeenCalled(); }); it('should customize id when pass with id', () => { render(
, ); expect(screen.getByRole('textbox')).toHaveAttribute('id', 'bamboo'); }); it('should trigger validate when onBlur when pass validateTrigger onBlur', async () => { render(
, ); // type a invalidate value, not trigger validation await userEvent.type(screen.getByRole('textbox'), '7777'); expect(screen.queryByRole('alert')).not.toBeInTheDocument(); // tab(onBlur) the input field, trigger and see the alert fireEvent.blur(screen.getByRole('textbox')); await expect(screen.findByRole('alert')).resolves.toBeInTheDocument(); }); 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', () => { render(
, ); expect(screen.getByRole('form')).toHaveClass('ant-form-hide-required-mark'); }); it('form should support disabled', () => { const App: React.FC = () => (
disabled Apple Pear