2020-02-18 15:38:00 +08:00
import React , { Component } from 'react' ;
2019-07-03 20:14:39 +08:00
import { mount } from 'enzyme' ;
2019-12-23 18:33:08 +08:00
import scrollIntoView from 'scroll-into-view-if-needed' ;
2019-07-03 20:14:39 +08:00
import Form from '..' ;
import Input from '../../input' ;
import Button from '../../button' ;
2019-08-26 22:53:20 +08:00
import mountTest from '../../../tests/shared/mountTest' ;
2020-01-02 19:10:16 +08:00
import rtlTest from '../../../tests/shared/rtlTest' ;
2020-03-27 12:07:18 +08:00
import { sleep } from '../../../tests/utils' ;
2019-07-03 20:14:39 +08:00
2019-12-23 18:33:08 +08:00
jest . mock ( 'scroll-into-view-if-needed' ) ;
2019-07-04 15:00:11 +08:00
2019-12-26 15:48:52 +08:00
const delay = ( timeout = 0 ) =>
2019-07-03 20:14:39 +08:00
new Promise ( resolve => {
2019-12-26 15:48:52 +08:00
setTimeout ( resolve , timeout ) ;
2019-07-03 20:14:39 +08:00
} ) ;
describe ( 'Form' , ( ) => {
2019-08-26 22:53:20 +08:00
mountTest ( Form ) ;
mountTest ( Form . Item ) ;
2020-01-02 19:10:16 +08:00
rtlTest ( Form ) ;
rtlTest ( Form . Item ) ;
2019-07-04 15:00:11 +08:00
scrollIntoView . mockImplementation ( ( ) => { } ) ;
2019-07-03 20:14:39 +08:00
const errorSpy = jest . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
async function change ( wrapper , index , value ) {
2020-03-24 22:23:40 +08:00
wrapper . find ( Input ) . at ( index ) . simulate ( 'change' , { target : { value } } ) ;
2019-12-27 16:50:44 +08:00
await delay ( 50 ) ;
2019-07-03 20:14:39 +08:00
wrapper . update ( ) ;
}
2019-07-04 15:00:11 +08:00
beforeEach ( ( ) => {
2019-12-27 16:50:44 +08:00
jest . useRealTimers ( ) ;
2019-07-04 15:00:11 +08:00
scrollIntoView . mockReset ( ) ;
} ) ;
2019-07-03 20:14:39 +08:00
afterEach ( ( ) => {
errorSpy . mockReset ( ) ;
} ) ;
afterAll ( ( ) => {
errorSpy . mockRestore ( ) ;
2019-07-04 15:00:11 +08:00
scrollIntoView . mockRestore ( ) ;
2019-07-03 20:14:39 +08:00
} ) ;
describe ( 'List' , ( ) => {
function testList ( name , renderField ) {
it ( name , async ( ) => {
const wrapper = mount (
< Form >
< Form . List name = "list" >
{ ( fields , { add , remove } ) => (
2019-08-29 23:41:46 +08:00
< >
2019-07-03 20:14:39 +08:00
{ fields . map ( field => renderField ( field ) ) }
< Button className = "add" onClick = { add } >
Add
< / B u t t o n >
< Button
className = "remove"
onClick = { ( ) => {
remove ( 1 ) ;
} }
>
Remove
< / B u t t o n >
2019-08-29 23:41:46 +08:00
< / >
2019-07-03 20:14:39 +08:00
) }
< / F o r m . L i s t >
< / F o r m > ,
) ;
async function operate ( className ) {
2020-03-24 22:23:40 +08:00
wrapper . find ( className ) . last ( ) . simulate ( 'click' ) ;
2019-07-03 20:14:39 +08:00
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 , '' ) ;
2020-01-17 11:50:06 +08:00
wrapper . update ( ) ;
2020-03-27 12:07:18 +08:00
await sleep ( 0 ) ;
2019-07-03 20:14:39 +08:00
expect ( wrapper . find ( '.ant-form-item-explain' ) . length ) . toBe ( 1 ) ;
await operate ( '.remove' ) ;
2020-01-17 11:50:06 +08:00
wrapper . update ( ) ;
2019-07-03 20:14:39 +08:00
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 / >
< / F o r m . I t e m >
) ) ;
2019-07-19 14:07:39 +08:00
testList ( 'nest noStyle' , field => (
2019-07-03 20:14:39 +08:00
< Form . Item key = { field . key } >
2019-07-19 14:07:39 +08:00
< Form . Item noStyle { ... field } rules = { [ { required : true } ] } >
2019-07-03 20:14:39 +08:00
< Input / >
< / F o r m . I t e m >
< / F o r m . I t e m >
) ) ;
2020-02-03 17:54:33 +08:00
it ( 'correct onFinish values' , async ( ) => {
async function click ( wrapper , className ) {
2020-03-24 22:23:40 +08:00
wrapper . find ( className ) . last ( ) . simulate ( 'click' ) ;
2020-02-03 17:54:33 +08:00
await delay ( ) ;
wrapper . update ( ) ;
}
const onFinish = jest . fn ( ) . mockImplementation ( ( ) => { } ) ;
const wrapper = mount (
< Form
2020-02-19 13:20:43 +08:00
onFinish = { v => {
2020-02-03 17:54:33 +08:00
if ( typeof v . list [ 0 ] === 'object' ) {
/ * o l d v e r s i o n l e d t o S y n t h e t i c E v e n t b e p a s s e d a s a n v a l u e h e r e
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 / >
< / F o r m . I t e m >
) ) }
2020-02-19 13:20:43 +08:00
< Button className = "add" onClick = { add } >
2020-02-03 17:54:33 +08:00
Add
< / B u t t o n >
2020-02-19 13:20:43 +08:00
< Button className = "remove" onClick = { ( ) => remove ( 0 ) } >
2020-02-03 17:54:33 +08:00
Remove
< / B u t t o n >
< / >
) }
< / F o r m . L i s t >
< / F o r m > ,
) ;
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' ] } ) ;
} ) ;
2019-07-03 20:14:39 +08:00
} ) ;
2019-07-19 14:07:39 +08:00
it ( 'noStyle Form.Item' , async ( ) => {
2019-07-03 20:14:39 +08:00
const onChange = jest . fn ( ) ;
const wrapper = mount (
< Form >
< Form . Item >
< Form . Item name = "test" rules = { [ { required : true } ] } >
< Input onChange = { onChange } / >
< / F o r m . I t e m >
< / F o r m . I t e m >
< / F o r m > ,
) ;
await change ( wrapper , 0 , '' ) ;
expect ( wrapper . find ( '.ant-form-item-explain' ) . length ) . toBe ( 1 ) ;
expect ( onChange ) . toHaveBeenCalled ( ) ;
} ) ;
it ( '`shouldUpdate` should work with render props' , ( ) => {
mount (
< Form >
< Form . Item > { ( ) => null } < / F o r m . I t e m >
< / F o r m > ,
) ;
expect ( errorSpy ) . toHaveBeenCalledWith (
'Warning: [antd: Form.Item] `children` of render props only work with `shouldUpdate`.' ,
) ;
} ) ;
2020-01-07 19:17:51 +08:00
it ( '`name` should not work with render props' , ( ) => {
mount (
< Form >
< Form . Item name = "test" shouldUpdate >
{ ( ) => null }
< / F o r m . I t e m >
< / F o r m > ,
) ;
expect ( errorSpy ) . toHaveBeenCalledWith (
"Warning: [antd: Form.Item] Do not use `name` with `children` of render props since it's not a field." ,
) ;
} ) ;
2020-01-01 20:07:10 +08:00
it ( 'children is array has name props' , ( ) => {
mount (
< Form >
< Form . Item name = "test" >
< div > one < / d i v >
< div > two < / d i v >
< / F o r m . I t e m >
< / F o r m > ,
) ;
expect ( errorSpy ) . toHaveBeenCalledWith (
'Warning: [antd: Form.Item] `children` is array of render props cannot have `name`.' ,
) ;
} ) ;
2019-07-04 15:00:11 +08:00
describe ( 'scrollToField' , ( ) => {
function test ( name , genForm ) {
it ( name , ( ) => {
let callGetForm ;
const Demo = ( ) => {
const { props , getForm } = genForm ( ) ;
callGetForm = getForm ;
return (
< Form name = "scroll" { ... props } >
< Form . Item name = "test" >
< Input / >
< / F o r m . I t e m >
< / F o r m >
) ;
} ;
2020-02-19 17:38:46 +08:00
const wrapper = mount ( < Demo / > , { attachTo : document . body } ) ;
2019-07-04 15:00:11 +08:00
expect ( scrollIntoView ) . not . toHaveBeenCalled ( ) ;
2020-01-14 22:50:49 +08:00
const form = callGetForm ( ) ;
form . scrollToField ( 'test' , {
block : 'start' ,
} ) ;
const inputNode = document . getElementById ( 'scroll_test' ) ;
expect ( scrollIntoView ) . toHaveBeenCalledWith ( inputNode , {
block : 'start' ,
scrollMode : 'if-needed' ,
} ) ;
2020-02-19 17:38:46 +08:00
wrapper . unmount ( ) ;
2019-07-04 15:00:11 +08:00
} ) ;
}
// hooks
test ( 'useForm' , ( ) => {
const [ form ] = Form . useForm ( ) ;
return {
props : { form } ,
getForm : ( ) => form ,
} ;
} ) ;
// ref
test ( 'ref' , ( ) => {
let form ;
return {
props : {
ref : instance => {
form = instance ;
} ,
} ,
getForm : ( ) => form ,
} ;
} ) ;
} ) ;
2019-11-13 11:48:20 +08:00
2020-02-19 17:38:46 +08:00
it ( 'scrollToFirstError' , async ( ) => {
const onFinishFailed = jest . fn ( ) ;
const wrapper = mount (
< Form scrollToFirstError onFinishFailed = { onFinishFailed } >
< Form . Item name = "test" rules = { [ { required : true } ] } >
< input / >
< / F o r m . I t e m >
< / F o r m > ,
{ attachTo : document . body } ,
) ;
expect ( scrollIntoView ) . not . toHaveBeenCalled ( ) ;
wrapper . find ( 'form' ) . simulate ( 'submit' ) ;
await delay ( 50 ) ;
expect ( scrollIntoView ) . toHaveBeenCalled ( ) ;
expect ( onFinishFailed ) . toHaveBeenCalled ( ) ;
wrapper . unmount ( ) ;
} ) ;
2019-11-13 11:48:20 +08:00
it ( 'Form.Item should support data-*、aria-* and custom attribute' , ( ) => {
const wrapper = mount (
< Form >
< Form . Item data - text = "123" aria - hidden = "true" cccc = "bbbb" >
text
< / F o r m . I t e m >
< / F o r m > ,
) ;
expect ( wrapper . render ( ) ) . toMatchSnapshot ( ) ;
} ) ;
2019-12-11 16:08:59 +08:00
it ( 'warning when use `name` but children is not validate element' , ( ) => {
mount (
< Form >
< Form . Item name = "warning" > text < / F o r m . I t e m >
< / F o r m > ,
) ;
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.' ,
) ;
} ) ;
2019-12-12 12:05:59 +08:00
it ( 'dynamic change required' , ( ) => {
const wrapper = mount (
< Form >
< Form . Item label = "light" name = "light" valuePropName = "checked" >
< input type = "checkbox" / >
< / F o r m . I t e m >
< Form . Item
label = "bamboo"
name = "bamboo"
dependencies = { [ 'light' ] }
rules = { [ ( { getFieldValue } ) => ( { required : getFieldValue ( 'light' ) } ) ] }
>
< input / >
< / F o r m . I t e m >
< / F o r m > ,
) ;
expect ( wrapper . find ( '.ant-form-item-required' ) ) . toHaveLength ( 0 ) ;
wrapper . find ( 'input[type="checkbox"]' ) . simulate ( 'change' , { target : { checked : true } } ) ;
wrapper . update ( ) ;
expect ( wrapper . find ( '.ant-form-item-required' ) ) . toHaveLength ( 1 ) ;
} ) ;
2019-12-27 16:50:44 +08:00
it ( 'should show related className when customize help' , ( ) => {
const wrapper = mount (
< Form >
< Form . Item help = "good" >
< input / >
< / F o r m . I t e m >
< / F o r m > ,
) ;
expect ( wrapper . find ( '.ant-form-item-with-help' ) . length ) . toBeTruthy ( ) ;
} ) ;
2019-12-30 12:13:58 +08:00
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.' ,
) ;
} ) ;
2020-01-07 16:20:18 +08:00
// https://github.com/ant-design/ant-design/issues/20706
it ( 'Error change should work' , async ( ) => {
const wrapper = mount (
< Form >
< Form . Item
name = "name"
rules = { [
{ required : true } ,
{
validator : ( _ , value ) => {
if ( value === 'p' ) {
return Promise . reject ( new Error ( 'not a p' ) ) ;
}
return Promise . resolve ( ) ;
} ,
} ,
] }
>
< Input / >
< / F o r m . I t e m >
< / F o r m > ,
) ;
/* eslint-disable no-await-in-loop */
for ( let i = 0 ; i < 3 ; i += 1 ) {
await change ( wrapper , 0 , '' ) ;
2020-03-24 22:23:40 +08:00
expect ( wrapper . find ( '.ant-form-item-explain' ) . first ( ) . text ( ) ) . toEqual ( "'name' is required" ) ;
2020-01-07 16:20:18 +08:00
await change ( wrapper , 0 , 'p' ) ;
2020-03-06 11:53:09 +08:00
await delay ( 100 ) ;
wrapper . update ( ) ;
2020-03-24 22:23:40 +08:00
expect ( wrapper . find ( '.ant-form-item-explain' ) . first ( ) . text ( ) ) . toEqual ( 'not a p' ) ;
2020-01-07 16:20:18 +08:00
}
/* eslint-enable */
} ) ;
2020-01-14 14:45:22 +08:00
// https://github.com/ant-design/ant-design/issues/20813
it ( 'should update help directly when provided' , ( ) => {
function App ( ) {
const [ message , updateMessage ] = React . useState ( '' ) ;
return (
< Form >
< Form . Item label = "hello" help = { message } >
< Input / >
< / F o r m . I t e m >
< Button onClick = { ( ) => updateMessage ( 'bamboo' ) } / >
< / F o r m >
) ;
}
const wrapper = mount ( < App / > ) ;
wrapper . find ( 'button' ) . simulate ( 'click' ) ;
2020-03-24 22:23:40 +08:00
expect ( wrapper . find ( '.ant-form-item' ) . first ( ) . hasClass ( 'ant-form-item-with-help' ) ) . toBeTruthy ( ) ;
2020-01-14 14:45:22 +08:00
expect ( wrapper . find ( '.ant-form-item-explain' ) . text ( ) ) . toEqual ( 'bamboo' ) ;
} ) ;
2020-01-17 11:50:06 +08:00
it ( 'warning when use `dependencies` but `name` is empty & children is not a render props' , ( ) => {
mount (
< Form >
< Form . Item dependencies = { [ ] } > text < / F o r m . I t e m >
< / F o r m > ,
) ;
expect ( errorSpy ) . toHaveBeenCalledWith (
'Warning: [antd: Form.Item] Must set `name` or use render props when `dependencies` is set.' ,
) ;
} ) ;
// https://github.com/ant-design/ant-design/issues/20948
it ( 'not repeat render when Form.Item is not a real Field' , async ( ) => {
const shouldNotRender = jest . fn ( ) ;
const StaticInput = ( ) => {
shouldNotRender ( ) ;
return < Input / > ;
} ;
const shouldRender = jest . fn ( ) ;
const DynamicInput = ( ) => {
shouldRender ( ) ;
return < Input / > ;
} ;
const formRef = React . createRef ( ) ;
mount (
< div >
< Form ref = { formRef } >
< Form . Item >
< StaticInput / >
< / F o r m . I t e m >
< Form . Item name = "light" >
< DynamicInput / >
< / F o r m . I t e m >
< / F o r m >
< / d i v > ,
) ;
expect ( shouldNotRender ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( shouldRender ) . toHaveBeenCalledTimes ( 1 ) ;
formRef . current . setFieldsValue ( { light : 'bamboo' } ) ;
await Promise . resolve ( ) ;
expect ( shouldNotRender ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( shouldRender ) . toHaveBeenCalledTimes ( 2 ) ;
} ) ;
2020-01-20 16:09:59 +08:00
it ( 'empty help should also render' , ( ) => {
const wrapper = mount (
< Form . Item help = "" >
< input / >
< / F o r m . I t e m > ,
) ;
expect ( wrapper . find ( '.ant-form-item-explain' ) . length ) . toBeTruthy ( ) ;
} ) ;
2020-02-01 20:09:29 +08:00
// https://github.com/ant-design/ant-design/issues/21167
it ( '`require` without `name`' , ( ) => {
const wrapper = mount (
< Form . Item label = "test" required >
< input / >
< / F o r m . I t e m > ,
) ;
expect ( wrapper . find ( '.ant-form-item-required' ) ) . toHaveLength ( 1 ) ;
} ) ;
2020-02-02 22:28:05 +08:00
it ( '0 is a validate Field' , ( ) => {
const wrapper = mount (
< Form . Item name = { 0 } >
< input / >
< / F o r m . I t e m > ,
) ;
expect ( wrapper . find ( 'Field' ) ) . toHaveLength ( 1 ) ;
} ) ;
2020-02-03 17:54:33 +08:00
it ( '`null` triggers warning and is treated as `undefined`' , ( ) => {
const wrapper = mount (
< Form . Item name = { null } >
< input / >
< / F o r m . I t e m > ,
) ;
expect ( wrapper . find ( 'Field' ) ) . toHaveLength ( 0 ) ;
expect ( errorSpy ) . toHaveBeenCalledWith (
'Warning: [antd: Form.Item] `null` is passed as `name` property' ,
) ;
} ) ;
2020-02-18 15:38:00 +08:00
// https://github.com/ant-design/ant-design/issues/21415
it ( 'Component.props.onChange is null' , ( ) => {
2020-04-03 15:02:11 +08:00
// eslint-disable-next-line react/prefer-stateless-function
2020-02-18 15:38:00 +08:00
class CustomComponent extends Component {
static defaultProps = {
onChange : null ,
} ;
render ( ) {
return < input { ... this . props } / > ;
}
}
expect ( ( ) => {
const wrapper = mount (
< Form >
< Form . Item name = "custom" >
< CustomComponent / >
< / F o r m . I t e m >
< / F o r m > ,
) ;
wrapper . find ( CustomComponent ) . simulate ( 'change' , { value : '123' } ) ;
} ) . not . toThrow ( ) ;
} ) ;
2020-03-03 12:10:32 +08:00
it ( 'change `help` should not warning' , ( ) => {
const Demo = ( ) => {
const [ error , setError ] = React . useState ( null ) ;
return (
< Form >
< Form . Item
help = { error ? 'This is an error msg' : undefined }
validateStatus = { error ? 'error' : '' }
label = "Username"
name = "username"
>
< input / >
< / F o r m . I t e m >
< Form . Item >
< button type = "button" onClick = { ( ) => setError ( ! error ) } >
Trigger
< / b u t t o n >
< / F o r m . I t e m >
< / F o r m >
) ;
} ;
const wrapper = mount ( < Demo / > ) ;
wrapper . find ( 'button' ) . simulate ( 'click' ) ;
expect ( errorSpy ) . not . toHaveBeenCalled ( ) ;
} ) ;
2020-03-06 12:07:55 +08:00
2020-03-03 21:57:06 +08:00
it ( '`label` support template' , async ( ) => {
const wrapper = mount (
// eslint-disable-next-line no-template-curly-in-string
< Form validateMessages = { { required : '${label} is good!' } } >
< Form . Item name = "test" label = "Bamboo" rules = { [ { required : true } ] } >
< input / >
< / F o r m . I t e m >
< / F o r m > ,
) ;
wrapper . find ( 'form' ) . simulate ( 'submit' ) ;
await delay ( 50 ) ;
wrapper . update ( ) ;
expect (
wrapper
. find ( '.ant-form-item-explain' )
. first ( )
. text ( ) ,
) . toEqual ( 'Bamboo is good!' ) ;
} ) ;
2020-03-06 18:12:39 +08:00
2020-03-06 12:07:55 +08:00
it ( 'return same form instance' , ( ) => {
const instances = new Set ( ) ;
const App = ( ) => {
const [ form ] = Form . useForm ( ) ;
instances . add ( form ) ;
const [ , forceUpdate ] = React . useState ( { } ) ;
return (
< button
type = "button"
onClick = { ( ) => {
forceUpdate ( { } ) ;
} }
>
Refresh
< / b u t t o n >
) ;
} ;
const wrapper = mount ( < App / > ) ;
for ( let i = 0 ; i < 5 ; i += 1 ) {
wrapper . find ( 'button' ) . simulate ( 'click' ) ;
}
expect ( instances . size ) . toEqual ( 1 ) ;
} ) ;
2020-03-08 16:28:33 +08:00
it ( 'avoid re-render' , async ( ) => {
let renderTimes = 0 ;
const MyInput = ( { value = '' , ... props } ) => {
renderTimes += 1 ;
return < input value = { value } { ... props } / > ;
} ;
const Demo = ( ) => (
< Form >
< Form . Item name = "username" rules = { [ { required : true } ] } >
< MyInput / >
< / F o r m . I t e m >
< / F o r m >
) ;
const wrapper = mount ( < Demo / > ) ;
renderTimes = 0 ;
wrapper . find ( 'input' ) . simulate ( 'change' , {
target : {
value : 'a' ,
} ,
} ) ;
await delay ( ) ;
expect ( renderTimes ) . toEqual ( 1 ) ;
expect ( wrapper . find ( 'input' ) . props ( ) . value ) . toEqual ( 'a' ) ;
} ) ;
2020-03-24 22:23:40 +08:00
it ( 'warning with `defaultValue`' , ( ) => {
mount (
< Form >
< Form . Item name = "light" >
< input defaultValue = "should warning" / >
< / F o r m . I t e m >
< / F o r m > ,
) ;
expect ( errorSpy ) . toHaveBeenCalledWith (
'Warning: [antd: Form.Item] `defaultValue` will not work on controlled Field. You should use `initialValues` of Form instead.' ,
) ;
} ) ;
2019-07-03 20:14:39 +08:00
} ) ;