From dc457581931104f289054a9d9a638d369b6fea16 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 19 Jun 2019 14:49:25 +0800 Subject: [PATCH 1/6] list coverage --- src/Field.tsx | 16 ++++-- src/List.tsx | 6 ++- src/index.tsx | 2 +- tests/list.test.js | 129 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 tests/list.test.js diff --git a/src/Field.tsx b/src/Field.tsx index 9b639176..7e0b644e 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -69,6 +69,8 @@ class Field extends React.Component implements FieldEnti private cancelRegisterFunc: () => void | null = null; + private destroy: boolean = false; + /** * Follow state should not management in State since it will async update by React. * This makes first render of form can not get correct state value. @@ -91,6 +93,7 @@ class Field extends React.Component implements FieldEnti public componentWillUnmount() { this.cancelRegister(); + this.destroy = true; } public cancelRegister = () => { @@ -121,7 +124,14 @@ class Field extends React.Component implements FieldEnti ); }; + public reRender() { + if (this.destroy) return; + this.forceUpdate(); + } + public refresh = () => { + if (this.destroy) return; + /** * We update `reset` state twice to clean up current node. * Which helps to reset value without define the type. @@ -181,7 +191,7 @@ class Field extends React.Component implements FieldEnti const validating = this.isFieldValidating(); if (this.prevValidating !== validating || !isSimilar(this.prevErrors, errors)) { - this.forceUpdate(); + this.reRender(); } break; } @@ -195,7 +205,7 @@ class Field extends React.Component implements FieldEnti (namePathList && containsNamePath(namePathList, namePath)) || dependencyList.some(dependency => containsNamePath(info.relatedFields, dependency)) ) { - this.forceUpdate(); + this.reRender(); } break; } @@ -214,7 +224,7 @@ class Field extends React.Component implements FieldEnti ) || (shouldUpdate ? shouldUpdate(prevStore, values, info) : prevValue !== curValue) ) { - this.forceUpdate(); + this.reRender(); } break; } diff --git a/src/List.tsx b/src/List.tsx index 153f335f..5abb4356 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -1,11 +1,13 @@ import * as React from 'react'; -import { FormInstance, InternalNamePath, NamePath, InternalFormInstance } from './interface'; +import warning from 'warning'; +import { InternalNamePath, NamePath, InternalFormInstance } from './interface'; import FieldContext, { HOOK_MARK } from './FieldContext'; import Field from './Field'; import { getNamePath, setValue } from './utils/valueUtil'; interface ListField { name: number; + key: number; } interface ListOperations { @@ -26,6 +28,7 @@ interface ListRenderProps { const List: React.FunctionComponent = ({ name, children }) => { // User should not pass `children` as other type. if (typeof children !== 'function') { + warning(false, 'Form.List only accepts function as children.'); return null; } @@ -83,6 +86,7 @@ const List: React.FunctionComponent = ({ name, children }) => { value.map( (__, index): ListField => ({ name: index, + key: index, }), ), operations, diff --git a/src/index.tsx b/src/index.tsx index 5ec866c6..b38ff66e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,6 +23,6 @@ RefStateForm.Field = Field; RefStateForm.List = List; RefStateForm.useForm = useForm; -export { FormInstance, Field, useForm, FormProvider }; +export { FormInstance, Field, List, useForm, FormProvider }; export default RefStateForm; diff --git a/tests/list.test.js b/tests/list.test.js new file mode 100644 index 00000000..f095faff --- /dev/null +++ b/tests/list.test.js @@ -0,0 +1,129 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import Form, { Field, List } from '../src'; +import InfoField, { Input } from './common/InfoField'; +import { changeValue, matchError, getField } from './common'; +import timeout from './common/timeout'; + +describe('list', () => { + let form; + + function generateForm(renderList, formProps) { + const wrapper = mount( +
+
{ + form = instance; + }} + {...formProps} + > + {renderList} +
+
, + ); + + return [wrapper, () => getField(wrapper).find('div')]; + } + + it('basic', async () => { + const [, getList] = generateForm( + fields => ( +
+ {fields.map(field => ( + + + + ))} +
+ ), + { + initialValues: { + list: ['', '', ''], + }, + }, + ); + + const listNode = getList(); + + await changeValue(getField(listNode, 0), '111'); + await changeValue(getField(listNode, 1), '222'); + await changeValue(getField(listNode, 2), '333'); + + expect(form.getFieldsValue()).toEqual({ + list: ['111', '222', '333'], + }); + }); + + it.only('operation', async () => { + let operation; + const [wrapper, getList] = generateForm((fields, opt) => { + operation = opt; + return ( +
+ {fields.map(field => ( + + + + ))} +
+ ); + }); + + // Add + operation.add(); + operation.add(); + operation.add(); + wrapper.update(); + expect(getList().find(Field).length).toEqual(3); + + // Modify + await changeValue(getField(getList(), 1), '222'); + expect(form.getFieldsValue()).toEqual({ + list: [undefined, '222', undefined], + }); + expect(form.isFieldTouched(['list', 0])).toBeFalsy(); + expect(form.isFieldTouched(['list', 1])).toBeTruthy(); + expect(form.isFieldTouched(['list', 2])).toBeFalsy(); + + // Remove + operation.remove(1); + wrapper.update(); + expect(getList().find(Field).length).toEqual(2); + expect(form.getFieldsValue()).toEqual({ + list: [undefined, undefined], + }); + expect(form.isFieldTouched(['list', 0])).toBeFalsy(); + expect(form.isFieldTouched(['list', 2])).toBeFalsy(); + }); + + it('validate', async () => { + const [, getList] = generateForm( + fields => ( +
+ {fields.map(field => ( + + + + ))} +
+ ), + { + initialValues: { list: [''] }, + }, + ); + + await changeValue(getField(getList()), ''); + + expect(form.getFieldError(['list', 0])).toEqual(["'list.0' is required"]); + }); + + it('warning if children is not function', () => { + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + generateForm(
); + + expect(errorSpy).toHaveBeenCalledWith('Warning: Form.List only accepts function as children.'); + + errorSpy.mockRestore(); + }); +}); From 36f9c9b34309433174486d71086b48d725a9a669 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 19 Jun 2019 15:07:07 +0800 Subject: [PATCH 2/6] fix list render logic --- src/List.tsx | 3 +++ tests/list.test.js | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/List.tsx b/src/List.tsx index 5abb4356..87f4113e 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -26,6 +26,8 @@ interface ListRenderProps { } const List: React.FunctionComponent = ({ name, children }) => { + const [_, forceUpdate] = React.useState(); + // User should not pass `children` as other type. if (typeof children !== 'function') { warning(false, 'Form.List only accepts function as children.'); @@ -79,6 +81,7 @@ const List: React.FunctionComponent = ({ name, children }) => { setFieldsValue(setValue({}, prefixName, [])); setFields(fields); + forceUpdate({}); }, }; diff --git a/tests/list.test.js b/tests/list.test.js index f095faff..08b34419 100644 --- a/tests/list.test.js +++ b/tests/list.test.js @@ -1,4 +1,5 @@ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { mount } from 'enzyme'; import Form, { Field, List } from '../src'; import InfoField, { Input } from './common/InfoField'; @@ -54,7 +55,7 @@ describe('list', () => { }); }); - it.only('operation', async () => { + it('operation', async () => { let operation; const [wrapper, getList] = generateForm((fields, opt) => { operation = opt; @@ -86,7 +87,9 @@ describe('list', () => { expect(form.isFieldTouched(['list', 2])).toBeFalsy(); // Remove - operation.remove(1); + act(() => { + operation.remove(1); + }); wrapper.update(); expect(getList().find(Field).length).toEqual(2); expect(form.getFieldsValue()).toEqual({ From 6d336e116e01fd0c355dad12b11a8164ec3431cf Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 19 Jun 2019 16:06:48 +0800 Subject: [PATCH 3/6] context test --- .eslintrc.js | 1 + README.md | 4 +- examples/StateForm-basic.tsx | 6 +-- examples/StateForm-context.tsx | 14 +++---- examples/StateForm-layout.tsx | 6 +-- examples/StateForm-list.tsx | 8 ++-- examples/StateForm-redux.tsx | 10 ++--- examples/StateForm-renderProps.tsx | 8 ++-- examples/StateForm-reset.tsx | 10 ++--- examples/StateForm-useForm.tsx | 8 ++-- examples/StateForm-validate-perf.tsx | 6 +-- examples/StateForm-validate.tsx | 8 ++-- examples/components/LabelField.tsx | 4 +- src/Field.tsx | 6 +-- src/FieldContext.ts | 17 +++++++- src/Form.tsx | 8 ++-- src/List.tsx | 13 ++++-- src/index.tsx | 20 ++++----- tests/context.test.js | 61 +++++++++++++++++++++++++++- tests/index.test.js | 32 +++++++++++++++ 20 files changed, 180 insertions(+), 70 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 443f8748..f30d7a7e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,5 +7,6 @@ module.exports = { 'no-template-curly-in-string': 0, 'prefer-promise-reject-errors': 0, 'react/no-array-index-key': 0, + 'react/sort-comp': 0, }, }; diff --git a/README.md b/README.md index 3abe4b85..ca6c1043 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ open http://localhost:9001/ ```js import Form, { Field } from 'rc-field-form'; - { console.log('Finish:', values); }} @@ -50,7 +50,7 @@ import Form, { Field } from 'rc-field-form'; -; +; export default Demo; ``` diff --git a/examples/StateForm-basic.tsx b/examples/StateForm-basic.tsx index 9835622a..96b5cd27 100644 --- a/examples/StateForm-basic.tsx +++ b/examples/StateForm-basic.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import StateForm, { Field } from '../src/'; +import Form, { Field } from '../src/'; import Input from './components/Input'; @@ -12,7 +12,7 @@ export default class Demo extends React.Component { return (

State Form ({list.length} inputs)

- +
@@ -48,7 +48,7 @@ export default class Demo extends React.Component { ))} - +
); } diff --git a/examples/StateForm-context.tsx b/examples/StateForm-context.tsx index 389a20cc..658a4be3 100644 --- a/examples/StateForm-context.tsx +++ b/examples/StateForm-context.tsx @@ -1,7 +1,7 @@ /* eslint-disable react/prop-types */ import React from 'react'; -import StateForm, { FormProvider } from '../src'; +import Form, { FormProvider } from '../src'; import Input from './components/Input'; import LabelField from './components/LabelField'; import { ValidateMessages } from '../src/interface'; @@ -16,10 +16,10 @@ const formStyle: React.CSSProperties = { }; const Form1 = () => { - const [form] = StateForm.useForm(); + const [form] = Form.useForm(); return ( - +

Form 1

Change me!

@@ -30,15 +30,15 @@ const Form1 = () => { - +
); }; const Form2 = () => { - const [form] = StateForm.useForm(); + const [form] = Form.useForm(); return ( - +

Form 2

Will follow Form 1 but not sync back

@@ -49,7 +49,7 @@ const Form2 = () => { - +
); }; diff --git a/examples/StateForm-layout.tsx b/examples/StateForm-layout.tsx index c4c84beb..334ed92e 100644 --- a/examples/StateForm-layout.tsx +++ b/examples/StateForm-layout.tsx @@ -1,7 +1,7 @@ /* eslint-disable jsx-a11y/label-has-associated-control, react/prop-types */ import React from 'react'; -import StateForm from '../src'; +import Form from '../src'; import Input from './components/Input'; import LabelField from './components/LabelField'; @@ -14,7 +14,7 @@ export default class Demo extends React.Component { return (

State Form ({list.length} inputs)

- +
@@ -24,7 +24,7 @@ export default class Demo extends React.Component { - +
); } diff --git a/examples/StateForm-list.tsx b/examples/StateForm-list.tsx index beea04e5..5c02ce14 100644 --- a/examples/StateForm-list.tsx +++ b/examples/StateForm-list.tsx @@ -1,11 +1,11 @@ /* eslint-disable react/prop-types */ import React from 'react'; -import StateForm from '../src/'; +import Form from '../src/'; import Input from './components/Input'; import LabelField from './components/LabelField'; -const { List, useForm } = StateForm; +const { List, useForm } = Form; const Demo = () => { const [form] = useForm(); @@ -15,7 +15,7 @@ const Demo = () => {

List of Form

You can set Field as List

- { console.log('values:', values); @@ -62,7 +62,7 @@ const Demo = () => { ); }} - +

Out Of Form

diff --git a/examples/StateForm-redux.tsx b/examples/StateForm-redux.tsx index f3eac655..3c46c5fc 100644 --- a/examples/StateForm-redux.tsx +++ b/examples/StateForm-redux.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { connect, Provider } from 'react-redux'; import { createStore } from 'redux'; -import StateForm from '../src/'; +import Form from '../src/'; import Input from './components/Input'; import LabelField from './components/LabelField'; @@ -22,7 +22,7 @@ let App: any = ({ dispatch, fields }) => { console.log('=>', fields); return ( - { console.log('Value Change:', changedValues, allValues); @@ -42,8 +42,8 @@ let App: any = ({ dispatch, fields }) => { - - + + - + ); }; App = connect((fields: any) => ({ fields }))(App); diff --git a/examples/StateForm-renderProps.tsx b/examples/StateForm-renderProps.tsx index ca192011..cf911558 100644 --- a/examples/StateForm-renderProps.tsx +++ b/examples/StateForm-renderProps.tsx @@ -1,9 +1,9 @@ /* eslint-disable jsx-a11y/label-has-associated-control */ import React from 'react'; -import StateForm from '../src'; +import Form from '../src'; import Input from './components/Input'; -const { Field } = StateForm; +const { Field } = Form; const list = new Array(1111).fill(() => undefined); @@ -15,7 +15,7 @@ export default class Demo extends React.Component {

Render Props ({list.length} inputs)

Render Props is easy to use but bad performance

- +
{(values) => { return ( @@ -55,7 +55,7 @@ export default class Demo extends React.Component { ); }} - +
); } diff --git a/examples/StateForm-reset.tsx b/examples/StateForm-reset.tsx index 4db55c1b..32b41660 100644 --- a/examples/StateForm-reset.tsx +++ b/examples/StateForm-reset.tsx @@ -1,9 +1,9 @@ /* eslint-disable react/prop-types */ import React from 'react'; -import StateForm from '../src'; +import Form from '../src'; import Input from './components/Input'; -const { Field } = StateForm; +const { Field } = Form; function Item({ children, ...restProps }) { return ( @@ -24,11 +24,11 @@ function Item({ children, ...restProps }) { } const Demo = () => { - const [form] = StateForm.useForm(); + const [form] = Form.useForm(); return (

Reset / Set Form

- +
@@ -64,7 +64,7 @@ const Demo = () => { > Set Password with Errors - +
); }; diff --git a/examples/StateForm-useForm.tsx b/examples/StateForm-useForm.tsx index c2740df4..b66ed8c5 100644 --- a/examples/StateForm-useForm.tsx +++ b/examples/StateForm-useForm.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import StateForm from '../src'; +import Form from '../src'; import Input from './components/Input'; -const { Field, useForm } = StateForm; +const { Field, useForm } = Form; const list = new Array(0).fill(() => undefined); @@ -22,7 +22,7 @@ export default () => { Fill Values - +
@@ -43,7 +43,7 @@ export default () => { ))} - +
); }; diff --git a/examples/StateForm-validate-perf.tsx b/examples/StateForm-validate-perf.tsx index a2485d76..0523f493 100644 --- a/examples/StateForm-validate-perf.tsx +++ b/examples/StateForm-validate-perf.tsx @@ -1,7 +1,7 @@ /* eslint-disable react/prop-types */ import React from 'react'; -import StateForm, { FormInstance } from '../src/'; +import Form, { FormInstance } from '../src/'; import Input from './components/Input'; import LabelField from './components/LabelField'; import { ValidateMessages } from '../src/interface'; @@ -34,7 +34,7 @@ export default class Demo extends React.Component { return (

High Perf Validate Form

- Reset - +
); } diff --git a/examples/StateForm-validate.tsx b/examples/StateForm-validate.tsx index 253efa53..2302bac9 100644 --- a/examples/StateForm-validate.tsx +++ b/examples/StateForm-validate.tsx @@ -1,10 +1,10 @@ /* eslint-disable react/prop-types */ import React from 'react'; -import StateForm from '../src'; +import Form from '../src'; import Input from './components/Input'; -const { Field } = StateForm; +const { Field } = Form; const Error = ({ children }) => (
    @@ -35,7 +35,7 @@ export default class Demo extends React.Component { return (

    Validate Form

    - +
    {(values, form) => { const usernameError = form.getFieldError('username'); const passwordError = form.getFieldError('password'); @@ -161,7 +161,7 @@ export default class Demo extends React.Component { ); }} - +
    ); } diff --git a/examples/components/LabelField.tsx b/examples/components/LabelField.tsx index 41d753ea..13451c57 100644 --- a/examples/components/LabelField.tsx +++ b/examples/components/LabelField.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import StateForm, { FormInstance } from '../../src/'; +import Form, { FormInstance } from '../../src/'; import { FieldProps } from '../../src/Field'; -const { Field } = StateForm; +const { Field } = Form; const Error = ({ children }) => (
      diff --git a/src/Field.tsx b/src/Field.tsx index 7e0b644e..7583d0a9 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -328,6 +328,9 @@ class Field extends React.Component implements FieldEnti // Add trigger control[trigger] = (...args: any[]) => { + // Mark as touched + this.touched = true; + let newValue = (getValueFromEvent || defaultGetValueFromEvent)(...args); if (normalize) { @@ -340,9 +343,6 @@ class Field extends React.Component implements FieldEnti value: newValue, }); - // Mark as touched - this.touched = true; - if (originTriggerFunc) { originTriggerFunc(...args); } diff --git a/src/FieldContext.ts b/src/FieldContext.ts index 9f0819b0..11ccc319 100644 --- a/src/FieldContext.ts +++ b/src/FieldContext.ts @@ -1,10 +1,11 @@ import * as React from 'react'; +import warning from 'warning'; import { InternalFormInstance } from './interface'; export const HOOK_MARK = 'RC_FORM_INTERNAL_HOOKS'; const warningFunc: any = () => { - throw new Error('StateForm is not defined.'); + warning(false, 'Can not find FormContext. Please make sure you wrap Field under Form.'); }; const Context = React.createContext({ @@ -21,7 +22,19 @@ const Context = React.createContext({ setFieldsValue: warningFunc, validateFields: warningFunc, - getInternalHooks: warningFunc, + getInternalHooks: () => { + warningFunc(); + + return { + dispatch: warningFunc, + registerField: warningFunc, + useSubscribe: warningFunc, + setInitialValues: warningFunc, + setCallbacks: warningFunc, + getFields: warningFunc, + setValidateMessages: warningFunc, + }; + }, }); export default Context; diff --git a/src/Form.tsx b/src/Form.tsx index 7ddf42c8..053dc81d 100644 --- a/src/Form.tsx +++ b/src/Form.tsx @@ -13,7 +13,7 @@ import FormContext, { FormContextProps } from './FormContext'; type BaseFormProps = Omit, 'onSubmit'>; -export interface StateFormProps extends BaseFormProps { +export interface FormProps extends BaseFormProps { initialValues?: Store; form?: FormInstance; children?: (() => JSX.Element | React.ReactNode) | React.ReactNode; @@ -25,7 +25,7 @@ export interface StateFormProps extends BaseFormProps { onFinish?: (values: Store) => void; } -const StateForm: React.FunctionComponent = ( +const Form: React.FunctionComponent = ( { name, initialValues, @@ -37,7 +37,7 @@ const StateForm: React.FunctionComponent = ( onFieldsChange, onFinish, ...restProps - }: StateFormProps, + }: FormProps, ref, ) => { const formContext: FormContextProps = React.useContext(FormContext); @@ -128,4 +128,4 @@ const StateForm: React.FunctionComponent = ( ); }; -export default StateForm; +export default Form; diff --git a/src/List.tsx b/src/List.tsx index 87f4113e..35ffdbc7 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -26,8 +26,6 @@ interface ListRenderProps { } const List: React.FunctionComponent = ({ name, children }) => { - const [_, forceUpdate] = React.useState(); - // User should not pass `children` as other type. if (typeof children !== 'function') { warning(false, 'Form.List only accepts function as children.'); @@ -80,8 +78,15 @@ const List: React.FunctionComponent = ({ name, children }) => { nextValue.splice(index, 1); setFieldsValue(setValue({}, prefixName, [])); - setFields(fields); - forceUpdate({}); + + // Set value back. + // We should add current list name also to let it re-render + setFields([ + ...fields, + { + name: prefixName, + }, + ]); }, }; diff --git a/src/index.tsx b/src/index.tsx index b38ff66e..a2f10335 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,26 +3,26 @@ import { FormInstance } from './interface'; import Field from './Field'; import List from './List'; import useForm from './useForm'; -import FieldForm, { StateFormProps } from './Form'; +import FieldForm, { FormProps } from './Form'; import { FormProvider } from './FormContext'; -const InternalStateForm = React.forwardRef(FieldForm); +const InternalForm = React.forwardRef(FieldForm); -type InternalStateForm = typeof InternalStateForm; -interface RefStateForm extends InternalStateForm { +type InternalForm = typeof InternalForm; +interface RefForm extends InternalForm { FormProvider: typeof FormProvider; Field: typeof Field; List: typeof List; useForm: typeof useForm; } -const RefStateForm: RefStateForm = InternalStateForm as any; +const RefForm: RefForm = InternalForm as any; -RefStateForm.FormProvider = FormProvider; -RefStateForm.Field = Field; -RefStateForm.List = List; -RefStateForm.useForm = useForm; +RefForm.FormProvider = FormProvider; +RefForm.Field = Field; +RefForm.List = List; +RefForm.useForm = useForm; export { FormInstance, Field, List, useForm, FormProvider }; -export default RefStateForm; +export default RefForm; diff --git a/tests/context.test.js b/tests/context.test.js index 7fd0f007..cae66aae 100644 --- a/tests/context.test.js +++ b/tests/context.test.js @@ -2,7 +2,7 @@ import React from 'react'; import { mount } from 'enzyme'; import Form, { FormProvider } from '../src'; import InfoField from './common/InfoField'; -import { changeValue, matchError } from './common'; +import { changeValue, matchError, getField } from './common'; describe('context', () => { it('validateMessages', async () => { @@ -17,4 +17,63 @@ describe('context', () => { await changeValue(wrapper, ''); matchError(wrapper, "I'm global"); }); + + it('change event', async () => { + const onFormChange = jest.fn(); + + const wrapper = mount( + +
      + + +
      , + ); + + await changeValue(getField(wrapper), 'Light'); + expect(onFormChange).toHaveBeenCalledWith( + 'form1', + expect.objectContaining({ + changedFields: [ + { errors: [], name: ['username'], touched: true, validating: false, value: 'Light' }, + ], + forms: { + form1: expect.objectContaining({}), + }, + }), + ); + }); + + it('adjust sub form', async () => { + const onFormChange = jest.fn(); + + const wrapper = mount( + +
      + , + ); + + wrapper.setProps({ + children: ( + + + + ), + }); + + await changeValue(getField(wrapper), 'Bamboo'); + const { forms } = onFormChange.mock.calls[0][1]; + expect(Object.keys(forms)).toEqual(['form2']); + }); + + it('do nothing if no Provider in use', () => { + const wrapper = mount( +
      +
      +
      , + ); + + wrapper.setProps({ + children: null, + }); + }); }); diff --git a/tests/index.test.js b/tests/index.test.js index 5d8e3c61..30e2a722 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -174,4 +174,36 @@ describe('Basic', () => { ).toEqual('Light'); }); }); + + it('should throw if no Form in use', () => { + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + mount( + + + , + ); + + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: Can not find FormContext. Please make sure you wrap Field under Form.', + ); + + errorSpy.mockRestore(); + }); + + it('keep origin input function', async () => { + const onChange = jest.fn(); + const onValuesChange = jest.fn(); + const wrapper = mount( + + + + + , + ); + + await changeValue(getField(wrapper), 'Bamboo'); + expect(onValuesChange).toHaveBeenCalledWith({ username: 'Bamboo' }, { username: 'Bamboo' }); + expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ target: { value: 'Bamboo' } })); + }); }); From 8929e00a30a570b6e76e9ad2903b376056dcd69f Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 19 Jun 2019 16:32:14 +0800 Subject: [PATCH 4/6] onFinish check --- tests/index.test.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/index.test.js b/tests/index.test.js index 30e2a722..f6810a5e 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -2,7 +2,8 @@ import React from 'react'; import { mount } from 'enzyme'; import Form, { Field } from '../src'; import InfoField, { Input } from './common/InfoField'; -import { changeValue, getField } from './common'; +import { changeValue, getField, matchError } from './common'; +import timeout from './common/timeout'; describe('Basic', () => { describe('create form', () => { @@ -206,4 +207,31 @@ describe('Basic', () => { expect(onValuesChange).toHaveBeenCalledWith({ username: 'Bamboo' }, { username: 'Bamboo' }); expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ target: { value: 'Bamboo' } })); }); + + it('submit', async () => { + const onFinish = jest.fn(); + + const wrapper = mount( +
      + + + + +
      , + ); + + // Not trigger + wrapper.find('button').simulate('submit'); + await timeout(); + wrapper.update(); + matchError(wrapper, "'user' is required"); + expect(onFinish).not.toHaveBeenCalled(); + + // Trigger + await changeValue(getField(wrapper), 'Bamboo'); + wrapper.find('button').simulate('submit'); + await timeout(); + matchError(wrapper, false); + expect(onFinish).toHaveBeenCalledWith({ user: 'Bamboo' }); + }); }); From b3d06e2e23bd5f796a0724a8078b7706e725d137 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 19 Jun 2019 17:06:03 +0800 Subject: [PATCH 5/6] fix changedValues logic --- src/useForm.ts | 2 +- src/utils/valueUtil.ts | 2 +- tests/control.test.js | 20 ++++++++++++++ tests/index.test.js | 63 +++++++++++++++++++++++++++++++++++++++++- tests/validate.test.js | 20 ++++++++++++-- 5 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 tests/control.test.js diff --git a/src/useForm.ts b/src/useForm.ts index 19311d32..d31bd28f 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -139,7 +139,7 @@ export class FormStore { return this.store; } - return nameList.map((name: NamePath) => this.getFieldValue(name)); + return cloneByNamePathList(this.store, nameList.map(getNamePath)); }; private getFieldValue = (name: NamePath) => { diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 01f4efe2..d6852092 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -31,7 +31,7 @@ export function cloneByNamePathList(store: any, namePathList: InternalNamePath[] let newStore = {}; namePathList.forEach(namePath => { const value = getValue(store, namePath); - newStore = setValue(store, namePath, value); + newStore = setValue(newStore, namePath, value); }); return newStore; diff --git a/tests/control.test.js b/tests/control.test.js new file mode 100644 index 00000000..21e245c7 --- /dev/null +++ b/tests/control.test.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import Form from '../src'; +import InfoField from './common/InfoField'; + +describe('Control', () => { + it('fields', () => { + const wrapper = mount( +
      + + , + ); + + wrapper.setProps({ + fields: [{ name: 'username', value: 'Bamboo' }], + }); + + expect(wrapper.find('input').props().value).toEqual('Bamboo'); + }); +}); diff --git a/tests/index.test.js b/tests/index.test.js index f6810a5e..68a6d63c 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { mount } from 'enzyme'; -import Form, { Field } from '../src'; +import Form, { Field, useForm } from '../src'; import InfoField, { Input } from './common/InfoField'; import { changeValue, getField, matchError } from './common'; import timeout from './common/timeout'; @@ -39,6 +39,30 @@ describe('Basic', () => { }); }); + it('fields touched', async () => { + let form; + + const wrapper = mount( +
      +
      { + form = instance; + }} + > + + + +
      , + ); + + expect(form.isFieldsTouched()).toBeFalsy(); + expect(form.isFieldsTouched(['username', 'password'])).toBeFalsy(); + + await changeValue(getField(wrapper, 0), 'Bamboo'); + expect(form.isFieldsTouched()).toBeTruthy(); + expect(form.isFieldsTouched(['username', 'password'])).toBeTruthy(); + }); + describe('reset form', () => { function resetTest(name, ...args) { it(name, async () => { @@ -112,6 +136,20 @@ describe('Basic', () => { path2: 'Bamboo', }, }); + expect(form.getFieldsValue(['username'])).toEqual({ + username: 'Light', + }); + expect(form.getFieldsValue(['path1'])).toEqual({ + path1: { + path2: 'Bamboo', + }, + }); + expect(form.getFieldsValue(['username', ['path1', 'path2']])).toEqual({ + username: 'Light', + path1: { + path2: 'Bamboo', + }, + }); expect( getField(wrapper, 'username') .find('input') @@ -234,4 +272,27 @@ describe('Basic', () => { matchError(wrapper, false); expect(onFinish).toHaveBeenCalledWith({ user: 'Bamboo' }); }); + + it('getInternalHooks should not usable by user', () => { + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + let form; + mount( +
      +
      { + form = instance; + }} + /> +
      , + ); + + expect(form.getInternalHooks()).toEqual(null); + + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: `getInternalHooks` is internal usage. Should not call directly.', + ); + + errorSpy.mockRestore(); + }); }); diff --git a/tests/validate.test.js b/tests/validate.test.js index 2cc351cd..ebb81190 100644 --- a/tests/validate.test.js +++ b/tests/validate.test.js @@ -8,14 +8,28 @@ import timeout from './common/timeout'; describe('validate', () => { it('required', async () => { + let form; const wrapper = mount( - - - , +
      +
      { + form = instance; + }} + > + + +
      , ); await changeValue(wrapper, ''); matchError(wrapper, true); + expect(form.getFieldError('username')).toEqual(["'username' is required"]); + expect(form.getFieldsError()).toEqual([ + { + name: ['username'], + errors: ["'username' is required"], + }, + ]); }); describe('validateMessages', () => { From 9b2735473194e6eba09ae6800d3126ba6b07cbc3 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 19 Jun 2019 17:33:46 +0800 Subject: [PATCH 6/6] rest test --- src/utils/valueUtil.ts | 5 +---- tests/utils.test.js | 44 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 tests/utils.test.js diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index d6852092..9172a745 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -14,11 +14,8 @@ export function getNamePath(path: NamePath | null): (string | number)[] { return toArray(path); } -export function getValue(store: Store, namePath: InternalNamePath, defaultValues?: Store) { +export function getValue(store: Store, namePath: InternalNamePath) { const value = get(store, namePath); - if (value === undefined && defaultValues) { - return get(defaultValues, namePath); - } return value; } diff --git a/tests/utils.test.js b/tests/utils.test.js new file mode 100644 index 00000000..82232e4e --- /dev/null +++ b/tests/utils.test.js @@ -0,0 +1,44 @@ +import { isSimilar, setValues } from '../src/utils/valueUtil'; +import NameMap from '../src/utils/NameMap'; + +describe('utils', () => { + describe('valueUtil', () => { + it('isSimilar', () => { + expect(isSimilar(1, 2)).toBeFalsy(); + expect(isSimilar({}, {})).toBeTruthy(); + expect(isSimilar({ a: 1 }, { a: 2 })).toBeFalsy(); + expect(isSimilar({ a() {} }, { a() {} })).toBeTruthy(); + expect(isSimilar({ a: 1 }, {})).toBeFalsy(); + expect(isSimilar({}, { a: 1 })).toBeFalsy(); + expect(isSimilar({}, null)).toBeFalsy(); + expect(isSimilar(null, {})).toBeFalsy(); + }); + + it('setValues', () => { + expect(setValues({}, { a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 }); + expect(setValues([], [123])).toEqual([123]); + }); + }); + + describe('NameMap', () => { + it('update should clean if empty', () => { + const map = new NameMap(); + map.set(['user', 'name'], 'Bamboo'); + map.set(['user', 'age'], 14); + + expect(map.toJSON()).toEqual({ + 'user.name': 'Bamboo', + 'user.age': 14, + }); + + map.update(['user', 'age'], prevValue => { + expect(prevValue).toEqual(14); + return null; + }); + + expect(map.toJSON()).toEqual({ + 'user.name': 'Bamboo', + }); + }); + }); +});