From d7892873e9eee6e4c618d4289fcdbc9ee240f68f Mon Sep 17 00:00:00 2001 From: afc163 Date: Wed, 13 Jul 2022 18:16:21 +0800 Subject: [PATCH 01/65] 1.27.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eb697e89..4438fb72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.27.0", + "version": "1.27.1", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From d09343cbbb84e139b35eb2bcc1ff02a2976fb011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B6=E6=9E=AB?= <7971419+crazyair@users.noreply.github.com> Date: Mon, 26 Sep 2022 21:57:09 +0800 Subject: [PATCH 02/65] =?UTF-8?q?feat:=E5=85=BC=E5=AE=B9=20form=20?= =?UTF-8?q?=E7=AC=AC=E4=B8=80=E6=AC=A1=E4=B8=BA=20undefined=20(#507)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: done * feat: test * feat: test * feat: test * feat: test * feat: test * feat: test * feat: args * feat: test --- src/useWatch.ts | 7 ++++--- tests/useWatch.test.tsx | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/useWatch.ts b/src/useWatch.ts index a3f47462..2eec7df2 100644 --- a/src/useWatch.ts +++ b/src/useWatch.ts @@ -58,7 +58,8 @@ function useWatch(dependencies: NamePath, form?: TFo function useWatch(dependencies: NamePath, form?: FormInstance): ValueType; -function useWatch(dependencies: NamePath = [], form?: FormInstance) { +function useWatch(...args: [NamePath, FormInstance]) { + const [dependencies = [], form] = args; const [value, setValue] = useState(); const valueStr = useMemo(() => stringify(value), [value]); @@ -72,7 +73,7 @@ function useWatch(dependencies: NamePath = [], form?: FormInstance) { // Warning if not exist form instance if (process.env.NODE_ENV !== 'production') { warning( - isValidForm, + args.length === 2 ? (form ? isValidForm : true) : isValidForm, 'useWatch requires a form instance since it can not auto detect from context.', ); } @@ -111,7 +112,7 @@ function useWatch(dependencies: NamePath = [], form?: FormInstance) { // We do not need re-register since namePath content is the same // eslint-disable-next-line react-hooks/exhaustive-deps - [], + [isValidForm], ); return value; diff --git a/tests/useWatch.test.tsx b/tests/useWatch.test.tsx index 88f3afb7..83fce087 100644 --- a/tests/useWatch.test.tsx +++ b/tests/useWatch.test.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { mount } from 'enzyme'; import type { FormInstance } from '../src'; import { List } from '../src'; @@ -219,6 +219,7 @@ describe('useWatch', () => { expect(errorSpy).toHaveBeenCalledWith( 'Warning: useWatch requires a form instance since it can not auto detect from context.', ); + errorSpy.mockRestore(); }); it('no more render time', () => { @@ -392,4 +393,38 @@ describe('useWatch', () => { const str = stringify(obj); expect(typeof str === 'number').toBeTruthy(); }); + it('first undefined', () => { + const errorSpy = jest.spyOn(console, 'error'); + const Demo = () => { + const formRef = useRef(); + const name = Form.useWatch('name', formRef.current); + const [, setUpdate] = useState({}); + + return ( + <> +
setUpdate({})} /> +
{name}
+
+ + + +
+ + ); + }; + + const wrapper = mount(); + expect(wrapper.find('.value').text()).toEqual(''); + wrapper.find('.setUpdate').at(0).simulate('click'); + expect(wrapper.find('.value').text()).toEqual('default'); + wrapper + .find('input') + .at(0) + .simulate('change', { target: { value: 'bamboo' } }); + expect(wrapper.find('.value').text()).toEqual('bamboo'); + expect(errorSpy).not.toHaveBeenCalledWith( + 'Warning: useWatch requires a form instance since it can not auto detect from context.', + ); + errorSpy.mockRestore(); + }); }); From 582aa84599cb9b938b3f3c91bd0579ffac7099df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 27 Sep 2022 10:41:46 +0800 Subject: [PATCH 03/65] 1.27.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4438fb72..b7e6cbaa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.27.1", + "version": "1.27.2", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From 37746e8e54810a3a4234b30cb84c2ede5fab90dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Tue, 25 Oct 2022 10:55:52 +0800 Subject: [PATCH 04/65] fix: invalidate rule should not failed (#523) --- src/Field.tsx | 18 ++++++++++-------- tests/validate.test.tsx | 12 ++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Field.tsx b/src/Field.tsx index b848c738..9b229ba0 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -373,14 +373,16 @@ class Field extends React.Component implements F let filteredRules = this.getRules(); if (triggerName) { - filteredRules = filteredRules.filter((rule: RuleObject) => { - const { validateTrigger } = rule; - if (!validateTrigger) { - return true; - } - const triggerList = toArray(validateTrigger); - return triggerList.includes(triggerName); - }); + filteredRules = filteredRules + .filter(rule => rule) + .filter((rule: RuleObject) => { + const { validateTrigger } = rule; + if (!validateTrigger) { + return true; + } + const triggerList = toArray(validateTrigger); + return triggerList.includes(triggerName); + }); } const promise = validateRules( diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index dc759ac3..8c095af7 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -738,5 +738,17 @@ describe('Form.Validate', () => { await changeValue(getField(wrapper, 0), ['light']); matchError(wrapper, false); }); + + it('filter empty rule', async () => { + const wrapper = mount( +
+
+ + +
, + ); + await changeValue(wrapper, ''); + matchError(wrapper, true); + }); }); /* eslint-enable no-template-curly-in-string */ From a0fc461a88ee739860d72de32b34fae13905970b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 25 Oct 2022 11:04:44 +0800 Subject: [PATCH 05/65] 1.27.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7e6cbaa..502e2dbc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.27.2", + "version": "1.27.3", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From dd3cc360c45410af6218c6cf0191d64de617895c Mon Sep 17 00:00:00 2001 From: lijianan <574980606@qq.com> Date: Fri, 13 Jan 2023 10:40:01 +0800 Subject: [PATCH 06/65] Migrate to testing-lib (#552) * js => ts * Migrate to testing-lib --- jest.config.js | 3 - jest.config.ts | 7 + package.json | 5 +- src/utils/valueUtil.ts | 2 +- tests/common/index.ts | 6 +- tests/common/timeout.ts | 2 +- tests/{context.test.js => context.test.tsx} | 0 tests/{control.test.js => control.test.tsx} | 0 ...ndencies.test.js => dependencies.test.tsx} | 2 +- tests/{index.test.js => index.test.tsx} | 20 +- ...ialValue.test.js => initialValue.test.tsx} | 0 ...tion.test.js => async-validation.test.tsx} | 0 ...basic-form.test.js => basic-form.test.tsx} | 0 ...ean-field.test.js => clean-field.test.tsx} | 21 +- tests/legacy/dom-form.test.js | 5 - tests/legacy/dynamic-binding.test.js | 252 ----------------- tests/legacy/dynamic-binding.test.tsx | 255 ++++++++++++++++++ ...mic-rule.test.js => dynamic-rule.test.tsx} | 54 ++-- ...eld-props.test.js => field-props.test.tsx} | 49 ++-- tests/legacy/form.test.js | 40 --- tests/legacy/form.test.tsx | 23 ++ ...ch-field.test.js => switch-field.test.tsx} | 34 +-- ...-array.test.js => validate-array.test.tsx} | 48 +--- tests/{utils.test.js => utils.test.ts} | 0 24 files changed, 367 insertions(+), 461 deletions(-) delete mode 100644 jest.config.js create mode 100644 jest.config.ts rename tests/{context.test.js => context.test.tsx} (100%) rename tests/{control.test.js => control.test.tsx} (100%) rename tests/{dependencies.test.js => dependencies.test.tsx} (99%) rename tests/{index.test.js => index.test.tsx} (97%) rename tests/{initialValue.test.js => initialValue.test.tsx} (100%) rename tests/legacy/{async-validation.test.js => async-validation.test.tsx} (100%) rename tests/legacy/{basic-form.test.js => basic-form.test.tsx} (100%) rename tests/legacy/{clean-field.test.js => clean-field.test.tsx} (70%) delete mode 100644 tests/legacy/dom-form.test.js delete mode 100644 tests/legacy/dynamic-binding.test.js create mode 100644 tests/legacy/dynamic-binding.test.tsx rename tests/legacy/{dynamic-rule.test.js => dynamic-rule.test.tsx} (71%) rename tests/legacy/{field-props.test.js => field-props.test.tsx} (69%) delete mode 100644 tests/legacy/form.test.js create mode 100644 tests/legacy/form.test.tsx rename tests/legacy/{switch-field.test.js => switch-field.test.tsx} (75%) rename tests/legacy/{validate-array.test.js => validate-array.test.tsx} (59%) rename tests/{utils.test.js => utils.test.ts} (100%) diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 865b283d..00000000 --- a/jest.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - setupFilesAfterEnv: ['/tests/setupAfterEnv.ts'] -}; \ No newline at end of file diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 00000000..8d21bee3 --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,7 @@ +import type { Config } from 'jest'; + +const config: Config = { + setupFilesAfterEnv: ['/tests/setupAfterEnv.ts'], +}; + +export default config; diff --git a/package.json b/package.json index 502e2dbc..52b16aee 100644 --- a/package.json +++ b/package.json @@ -55,9 +55,9 @@ }, "devDependencies": { "@testing-library/jest-dom": "^5.16.4", - "@testing-library/react": "^13.0.0", + "@testing-library/react": "^12.1.5", "@types/enzyme": "^3.10.5", - "@types/jest": "^26.0.20", + "@types/jest": "^29.2.5", "@types/lodash": "^4.14.135", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", @@ -71,6 +71,7 @@ "father": "^2.13.6", "father-build": "^1.18.6", "gh-pages": "^3.1.0", + "jest": "^29.3.1", "np": "^5.0.3", "prettier": "^2.1.2", "react": "^16.14.0", diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 25527bd6..0e5f073e 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -90,7 +90,7 @@ export function matchNamePath( } // Like `shallowEqual`, but we not check the data which may cause re-render -type SimilarObject = string | number | {}; +type SimilarObject = string | number | object; export function isSimilar(source: SimilarObject, target: SimilarObject) { if (source === target) { return true; diff --git a/tests/common/index.ts b/tests/common/index.ts index 54dc9073..5aa3b8b5 100644 --- a/tests/common/index.ts +++ b/tests/common/index.ts @@ -1,5 +1,3 @@ -/* eslint-disable import/no-extraneous-dependencies */ - import { act } from 'react-dom/test-utils'; import type { ReactWrapper } from 'enzyme'; import timeout from './timeout'; @@ -42,7 +40,7 @@ export function matchError( } } -export function getField(wrapper, index: string | number = 0) { +export function getField(wrapper, index: string | number | string[] = 0) { if (typeof index === 'number') { return wrapper.find(Field).at(index); } @@ -88,5 +86,3 @@ export async function validateFields(form, ...args) { await form.validateFields(...args); }); } - -/* eslint-enable import/no-extraneous-dependencies */ diff --git a/tests/common/timeout.ts b/tests/common/timeout.ts index 9eecacec..a688bcf9 100644 --- a/tests/common/timeout.ts +++ b/tests/common/timeout.ts @@ -1,5 +1,5 @@ export default (timeout: number = 0) => { - return new Promise(resolve => { + return new Promise(resolve => { setTimeout(resolve, timeout); }); }; diff --git a/tests/context.test.js b/tests/context.test.tsx similarity index 100% rename from tests/context.test.js rename to tests/context.test.tsx diff --git a/tests/control.test.js b/tests/control.test.tsx similarity index 100% rename from tests/control.test.js rename to tests/control.test.tsx diff --git a/tests/dependencies.test.js b/tests/dependencies.test.tsx similarity index 99% rename from tests/dependencies.test.js rename to tests/dependencies.test.tsx index 779da32b..4268b4b2 100644 --- a/tests/dependencies.test.js +++ b/tests/dependencies.test.tsx @@ -33,7 +33,7 @@ describe('Form.Dependencies', () => { }); describe('initialValue', () => { - function test(name, formProps, fieldProps) { + function test(name, formProps, fieldProps = {}) { it(name, async () => { let validated = false; diff --git a/tests/index.test.js b/tests/index.test.tsx similarity index 97% rename from tests/index.test.js rename to tests/index.test.tsx index d24a6d20..38228982 100644 --- a/tests/index.test.js +++ b/tests/index.test.tsx @@ -1,6 +1,7 @@ import { mount } from 'enzyme'; import { resetWarned } from 'rc-util/lib/warning'; import React from 'react'; +import type { FormInstance } from '../src'; import Form, { Field, useForm } from '../src'; import { changeValue, getField, matchError } from './common'; import InfoField, { Input } from './common/InfoField'; @@ -406,7 +407,7 @@ describe('Form.Basic', () => {
, ); - expect(wrapper.find('.anything').props().light).toEqual('bamboo'); + expect((wrapper.find('.anything').props() as any).light).toEqual('bamboo'); }); describe('shouldUpdate', () => { @@ -514,7 +515,7 @@ describe('Form.Basic', () => { it('should trigger by setField', () => { const triggerUpdate = jest.fn(); - const formRef = React.createRef(); + const formRef = React.createRef(); const wrapper = mount(
@@ -632,6 +633,7 @@ describe('Form.Basic', () => { mount(
+ {/* @ts-ignore */}

Light

Bamboo

@@ -706,7 +708,7 @@ describe('Form.Basic', () => { }); it('should not crash when return value contains target field', async () => { - const CustomInput = ({ value, onChange }) => { + const CustomInput: React.FC = ({ value, onChange }) => { const onInputChange = e => { onChange({ value: e.target.value, @@ -749,7 +751,7 @@ describe('Form.Basic', () => { autoComplete="off" > - {(fields, { add, remove }) => ( + {fields => ( <> {fields.map(({ key, name, ...restField }) => ( { }; const wrapper = mount(); - expect(wrapper.find('input').first().getDOMNode().value).toBe('11'); + expect(wrapper.find('input').first().getDOMNode().value).toBe('11'); wrapper.find('.reset-btn').first().simulate('click'); expect(wrapper.find('input').length).toBe(0); }); it('setFieldsValue should work for multiple Select', () => { - const Select = ({ value, defaultValue }) => { + const Select: React.FC = ({ value, defaultValue }) => { return
{(value || defaultValue || []).toString()}
; }; @@ -808,7 +810,7 @@ describe('Form.Basic', () => { it('remount should not clear current value', () => { let refForm; - const Demo = ({ remount }) => { + const Demo: React.FC = ({ remount }) => { const [form] = Form.useForm(); refForm = form; @@ -840,9 +842,9 @@ describe('Form.Basic', () => { }); it('setFieldValue', () => { - const formRef = React.createRef(); + const formRef = React.createRef(); - const Demo = () => ( + const Demo: React.FC = () => ( {fields => diff --git a/tests/initialValue.test.js b/tests/initialValue.test.tsx similarity index 100% rename from tests/initialValue.test.js rename to tests/initialValue.test.tsx diff --git a/tests/legacy/async-validation.test.js b/tests/legacy/async-validation.test.tsx similarity index 100% rename from tests/legacy/async-validation.test.js rename to tests/legacy/async-validation.test.tsx diff --git a/tests/legacy/basic-form.test.js b/tests/legacy/basic-form.test.tsx similarity index 100% rename from tests/legacy/basic-form.test.js rename to tests/legacy/basic-form.test.tsx diff --git a/tests/legacy/clean-field.test.js b/tests/legacy/clean-field.test.tsx similarity index 70% rename from tests/legacy/clean-field.test.js rename to tests/legacy/clean-field.test.tsx index 46d1866c..c9807fb8 100644 --- a/tests/legacy/clean-field.test.js +++ b/tests/legacy/clean-field.test.tsx @@ -1,19 +1,16 @@ import React from 'react'; -import { mount } from 'enzyme'; +import { render } from '@testing-library/react'; +import type { FormInstance } from '../../src'; import Form, { Field } from '../../src'; import { Input } from '../common/InfoField'; describe('legacy.clean-field', () => { // https://github.com/ant-design/ant-design/issues/12560 it('clean field if did update removed', async () => { - let form; + const form = React.createRef(); - const Test = ({ show }) => ( - { - form = instance; - }} - > + const Test: React.FC = ({ show }) => ( + {show ? ( @@ -26,20 +23,20 @@ describe('legacy.clean-field', () => { ); - const wrapper = mount(); + const { rerender } = render(); try { - await form.validateFields(); + await form.current?.validateFields(); throw new Error('should not pass'); } catch ({ errorFields }) { expect(errorFields.length).toBe(1); expect(errorFields[0].name).toEqual(['age']); } - wrapper.setProps({ show: false }); + rerender(); try { - await form.validateFields(); + await form.current?.validateFields(); throw new Error('should not pass'); } catch ({ errorFields }) { expect(errorFields.length).toBe(1); diff --git a/tests/legacy/dom-form.test.js b/tests/legacy/dom-form.test.js deleted file mode 100644 index 62afb15a..00000000 --- a/tests/legacy/dom-form.test.js +++ /dev/null @@ -1,5 +0,0 @@ -// TODO: createDOMForm.spec.js - -describe('legacy.dom-form', () => { - it('validateFieldAndScroll', () => {}); -}); diff --git a/tests/legacy/dynamic-binding.test.js b/tests/legacy/dynamic-binding.test.js deleted file mode 100644 index b1b74ae1..00000000 --- a/tests/legacy/dynamic-binding.test.js +++ /dev/null @@ -1,252 +0,0 @@ -import React from 'react'; -import { mount } from 'enzyme'; -import Form, { Field } from '../../src'; -import { Input } from '../common/InfoField'; - -describe('legacy.dynamic-binding', () => { - const getInput = (wrapper, id) => wrapper.find(id).last(); - - it('normal input', async () => { - let form; - - const Test = ({ mode }) => ( -
{ - form = instance; - }} - > - text content - {mode ? ( - - - - ) : null} - text content - text content - text content - {mode ? null : ( - - - - )} - text content -
- ); - - const wrapper = mount(); - - getInput(wrapper, '#text').simulate('change', { target: { value: '123' } }); - wrapper.setProps({ mode: false }); - expect(getInput(wrapper, '#number').getDOMNode().value).toBe('123'); - expect(form.getFieldValue('name')).toBe('123'); - getInput(wrapper, '#number').simulate('change', { target: { value: '456' } }); - wrapper.setProps({ mode: true }); - expect(getInput(wrapper, '#text').getDOMNode().value).toBe('456'); - expect(form.getFieldValue('name')).toBe('456'); - - const values = await form.validateFields(); - expect(values.name).toBe('456'); - }); - - // [Legacy] We do not remove value in Field Form - it('hidden input', async () => { - let form; - - const Test = ({ mode }) => ( -
{ - form = instance; - }} - > - text content - {mode ? ( - - - - ) : null} - text content - text content - text content - {mode ? ( - - - - ) : null} - text content -
- ); - - const wrapper = mount(); - getInput(wrapper, '#text1').simulate('change', { target: { value: '123' } }); - getInput(wrapper, '#text2').simulate('change', { target: { value: '456' } }); - expect(getInput(wrapper, '#text1').getDOMNode().value).toBe('123'); - expect(getInput(wrapper, '#text2').getDOMNode().value).toBe('456'); - expect(form.getFieldValue('input1')).toBe('123'); - expect(form.getFieldValue('input2')).toBe('456'); - - // Different with `rc-form` - wrapper.setProps({ mode: false }); - expect(form.getFieldValue('input1')).toBeTruthy(); - expect(form.getFieldValue('input2')).toBeTruthy(); - - wrapper.setProps({ mode: true }); - expect(getInput(wrapper, '#text1').getDOMNode().value).toBe('123'); - expect(getInput(wrapper, '#text2').getDOMNode().value).toBe('456'); - expect(form.getFieldValue('input1')).toBe('123'); - expect(form.getFieldValue('input2')).toBe('456'); - - getInput(wrapper, '#text1').simulate('change', { target: { value: '789' } }); - expect(getInput(wrapper, '#text1').getDOMNode().value).toBe('789'); - expect(getInput(wrapper, '#text2').getDOMNode().value).toBe('456'); - expect(form.getFieldValue('input1')).toBe('789'); - expect(form.getFieldValue('input2')).toBe('456'); - - const values = await form.validateFields(); - expect(values.input1).toBe('789'); - expect(values.input2).toBe('456'); - }); - - it('nested fields', async () => { - let form; - - const Test = ({ mode }) => ( -
{ - form = instance; - }} - > - {mode ? ( - - - - ) : null} - text content - {mode ? null : ( - - - - )} -
- ); - - const wrapper = mount(); - - getInput(wrapper, '#text').simulate('change', { target: { value: '123' } }); - wrapper.setProps({ mode: false }); - expect(getInput(wrapper, '#number').getDOMNode().value).toBe('123'); - expect(form.getFieldValue(['name', 'xxx'])).toBe('123'); - - getInput(wrapper, '#number').simulate('change', { target: { value: '456' } }); - wrapper.setProps({ mode: true }); - expect(getInput(wrapper, '#text').getDOMNode().value).toBe('456'); - expect(form.getFieldValue(['name', 'xxx'])).toBe('456'); - - const values = await form.validateFields(); - expect(values.name.xxx).toBe('456'); - }); - - it('input with different keys', async () => { - let form; - - const Test = ({ mode }) => ( -
{ - form = instance; - }} - > - {mode ? ( - - - - ) : null} - {mode ? null : ( - - - - )} -
- ); - - const wrapper = mount(); - - getInput(wrapper, '#text').simulate('change', { target: { value: '123' } }); - wrapper.setProps({ mode: false }); - expect(getInput(wrapper, '#number').getDOMNode().value).toBe('123'); - expect(form.getFieldValue('name')).toBe('123'); - - getInput(wrapper, '#number').simulate('change', { target: { value: '456' } }); - wrapper.setProps({ mode: true }); - expect(getInput(wrapper, '#text').getDOMNode().value).toBe('456'); - expect(form.getFieldValue('name')).toBe('456'); - - const values = await form.validateFields(); - expect(values.name).toBe('456'); - }); - - it('submit without removed fields', async () => { - // [Legacy] Since we don't remove values, this test is no need anymore. - }); - - it('reset fields', async () => { - let form; - - const Test = ({ mode }) => ( -
{ - form = instance; - }} - > - text content - {mode ? ( - - - - ) : null} - text content - text content - text content - {mode ? ( - - - - ) : null} - text content -
- ); - - const wrapper = mount(); - - getInput(wrapper, '#text1').simulate('change', { target: { value: '123' } }); - getInput(wrapper, '#text2').simulate('change', { target: { value: '456' } }); - expect(getInput(wrapper, '#text1').getDOMNode().value).toBe('123'); - expect(getInput(wrapper, '#text2').getDOMNode().value).toBe('456'); - expect(form.getFieldValue('input1')).toBe('123'); - expect(form.getFieldValue('input2')).toBe('456'); - - // Different with `rc-form` test - wrapper.setProps({ mode: false }); - expect(form.getFieldValue('input1')).toBeTruthy(); - expect(form.getFieldValue('input2')).toBeTruthy(); - - form.resetFields(); - wrapper.setProps({ mode: true }); - expect(getInput(wrapper, '#text1').getDOMNode().value).toBe(''); - expect(getInput(wrapper, '#text2').getDOMNode().value).toBe(''); - expect(form.getFieldValue('input1')).toBe(undefined); - expect(form.getFieldValue('input2')).toBe(undefined); - - getInput(wrapper, '#text1').simulate('change', { target: { value: '789' } }); - expect(getInput(wrapper, '#text1').getDOMNode().value).toBe('789'); - expect(getInput(wrapper, '#text2').getDOMNode().value).toBe(''); - expect(form.getFieldValue('input1')).toBe('789'); - expect(form.getFieldValue('input2')).toBe(undefined); - - getInput(wrapper, '#text2').simulate('change', { target: { value: '456' } }); - expect(getInput(wrapper, '#text2').getDOMNode().value).toBe('456'); - expect(form.getFieldValue('input2')).toBe('456'); - - const values = await form.validateFields(); - expect(values.input1).toBe('789'); - expect(values.input2).toBe('456'); - }); -}); diff --git a/tests/legacy/dynamic-binding.test.tsx b/tests/legacy/dynamic-binding.test.tsx new file mode 100644 index 00000000..7aead6a1 --- /dev/null +++ b/tests/legacy/dynamic-binding.test.tsx @@ -0,0 +1,255 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import type { FormInstance } from '../../src'; +import Form, { Field } from '../../src'; +import { Input } from '../common/InfoField'; + +const getInput = (container: HTMLElement, id: string) => + container.querySelector(id); + +describe('legacy.dynamic-binding', () => { + it('normal input', async () => { + const form = React.createRef(); + + const Test: React.FC = ({ mode }) => ( +
+ text content + {mode ? ( + + + + ) : null} + text content + text content + text content + {mode ? null : ( + + + + )} + text content +
+ ); + + const { container, rerender } = render(); + + fireEvent.change(getInput(container, '#text'), { target: { value: '123' } }); + + rerender(); + + expect(getInput(container, '#number')?.value).toBe('123'); + expect(form.current?.getFieldValue('name')).toBe('123'); + + fireEvent.change(getInput(container, '#number'), { target: { value: '456' } }); + + rerender(); + + expect(getInput(container, '#text')?.value).toBe('456'); + expect(form.current?.getFieldValue('name')).toBe('456'); + const values = await form.current?.validateFields(); + expect(values.name).toBe('456'); + }); + + // [Legacy] We do not remove value in Field Form + it('hidden input', async () => { + const form = React.createRef(); + + const Test: React.FC = ({ mode }) => ( +
+ text content + {mode ? ( + + + + ) : null} + text content + text content + text content + {mode ? ( + + + + ) : null} + text content +
+ ); + + const { container, rerender } = render(); + + fireEvent.change(getInput(container, '#text1'), { target: { value: '123' } }); + fireEvent.change(getInput(container, '#text2'), { target: { value: '456' } }); + + expect(getInput(container, '#text1')?.value).toBe('123'); + expect(getInput(container, '#text2')?.value).toBe('456'); + expect(form.current?.getFieldValue('input1')).toBe('123'); + expect(form.current?.getFieldValue('input2')).toBe('456'); + + // Different with `rc-form` + + rerender(); + expect(form.current?.getFieldValue('input1')).toBeTruthy(); + expect(form.current?.getFieldValue('input2')).toBeTruthy(); + + rerender(); + + expect(getInput(container, '#text1')?.value).toBe('123'); + expect(getInput(container, '#text2')?.value).toBe('456'); + expect(form.current?.getFieldValue('input1')).toBe('123'); + expect(form.current?.getFieldValue('input2')).toBe('456'); + + fireEvent.change(getInput(container, '#text1'), { target: { value: '789' } }); + + expect(getInput(container, '#text1')?.value).toBe('789'); + expect(getInput(container, '#text2')?.value).toBe('456'); + expect(form.current?.getFieldValue('input1')).toBe('789'); + expect(form.current?.getFieldValue('input2')).toBe('456'); + + const values = await form.current?.validateFields(); + expect(values.input1).toBe('789'); + expect(values.input2).toBe('456'); + }); + + it('nested fields', async () => { + const form = React.createRef(); + + const Test: React.FC = ({ mode }) => ( +
+ {mode ? ( + + + + ) : null} + text content + {mode ? null : ( + + + + )} +
+ ); + + const { container, rerender } = render(); + + fireEvent.change(getInput(container, '#text'), { target: { value: '123' } }); + + rerender(); + expect(getInput(container, '#number')?.value).toBe('123'); + expect(form.current?.getFieldValue(['name', 'xxx'])).toBe('123'); + + fireEvent.change(getInput(container, '#number'), { target: { value: '456' } }); + + rerender(); + + expect(getInput(container, '#text')?.value).toBe('456'); + expect(form.current?.getFieldValue(['name', 'xxx'])).toBe('456'); + const values = await form.current?.validateFields(); + expect(values.name.xxx).toBe('456'); + }); + + it('input with different keys', async () => { + const form = React.createRef(); + + const Test: React.FC = ({ mode }) => ( +
+ {mode ? ( + + + + ) : null} + {mode ? null : ( + + + + )} +
+ ); + + const { container, rerender } = render(); + + fireEvent.change(getInput(container, '#text'), { target: { value: '123' } }); + + rerender(); + + expect(getInput(container, '#number')?.value).toBe('123'); + expect(form.current?.getFieldValue('name')).toBe('123'); + + fireEvent.change(getInput(container, '#number'), { target: { value: '456' } }); + + rerender(); + + expect(getInput(container, '#text')?.value).toBe('456'); + expect(form.current?.getFieldValue('name')).toBe('456'); + + const values = await form.current?.validateFields(); + expect(values.name).toBe('456'); + }); + + it('submit without removed fields', async () => { + // [Legacy] Since we don't remove values, this test is no need anymore. + }); + + it('reset fields', async () => { + const form = React.createRef(); + + const Test: React.FC = ({ mode }) => ( +
+ text content + {mode ? ( + + + + ) : null} + text content + text content + text content + {mode ? ( + + + + ) : null} + text content +
+ ); + + const { container, rerender } = render(); + + fireEvent.change(getInput(container, '#text1'), { target: { value: '123' } }); + fireEvent.change(getInput(container, '#text2'), { target: { value: '456' } }); + + expect(getInput(container, '#text1')?.value).toBe('123'); + expect(getInput(container, '#text2')?.value).toBe('456'); + + expect(form.current?.getFieldValue('input1')).toBe('123'); + expect(form.current?.getFieldValue('input2')).toBe('456'); + + // Different with `rc-form` test + + rerender(); + + expect(form.current?.getFieldValue('input1')).toBeTruthy(); + expect(form.current?.getFieldValue('input2')).toBeTruthy(); + + form.current?.resetFields(); + rerender(); + expect(getInput(container, '#text1')?.value).toBe(''); + expect(getInput(container, '#text2')?.value).toBe(''); + expect(form.current?.getFieldValue('input1')).toBe(undefined); + expect(form.current?.getFieldValue('input2')).toBe(undefined); + + fireEvent.change(getInput(container, '#text1'), { target: { value: '789' } }); + + expect(getInput(container, '#text1')?.value).toBe('789'); + expect(getInput(container, '#text2')?.value).toBe(''); + expect(form.current?.getFieldValue('input1')).toBe('789'); + expect(form.current?.getFieldValue('input2')).toBe(undefined); + + fireEvent.change(getInput(container, '#text2'), { target: { value: '456' } }); + + expect(getInput(container, '#text2')?.value).toBe('456'); + expect(form.current?.getFieldValue('input2')).toBe('456'); + + const values = await form.current?.validateFields(); + expect(values.input1).toBe('789'); + expect(values.input2).toBe('456'); + }); +}); diff --git a/tests/legacy/dynamic-rule.test.js b/tests/legacy/dynamic-rule.test.tsx similarity index 71% rename from tests/legacy/dynamic-rule.test.js rename to tests/legacy/dynamic-rule.test.tsx index b5f2b3cd..10e5fcdc 100644 --- a/tests/legacy/dynamic-rule.test.js +++ b/tests/legacy/dynamic-rule.test.tsx @@ -1,61 +1,45 @@ import React from 'react'; +import type { ReactWrapper } from 'enzyme'; import { mount } from 'enzyme'; +import type { FormInstance } from '../../src'; import Form, { Field } from '../../src'; import { Input } from '../common/InfoField'; import { changeValue, getField, validateFields, matchArray } from '../common'; -import timeout from '../common/timeout'; describe('legacy.dynamic-rule', () => { describe('should update errors', () => { - function doTest(name, renderFunc) { + const doTest = ( + name: string, + renderFunc: () => Promise>, ReactWrapper]>, + ) => { it(name, async () => { const [form, wrapper] = await renderFunc(); await changeValue(getField(wrapper, 'type'), 'test'); try { - await validateFields(form); + await validateFields(form.current); throw new Error('should not pass'); } catch ({ errorFields }) { - matchArray( - errorFields, - [ - { - name: ['val1'], - }, - ], - 'name', - ); + matchArray(errorFields, [{ name: ['val1'] }], 'name'); } await changeValue(getField(wrapper, 'type'), ''); try { - await validateFields(form); + await validateFields(form.current); throw new Error('should not pass'); } catch ({ errorFields }) { - matchArray( - errorFields, - [ - { - name: ['val2'], - }, - ], - 'name', - ); + matchArray(errorFields, [{ name: ['val2'] }], 'name'); } }); - } + }; // [Legacy] Test case doTest('render props', async () => { - let form; + const form = React.createRef(); const wrapper = mount(
-
{ - form = instance; - }} - > + {(_, { getFieldValue }) => ( @@ -75,19 +59,15 @@ describe('legacy.dynamic-rule', () => { wrapper.update(); - return [form, wrapper]; + return [form, wrapper] as const; }); doTest('use function rule', async () => { - let form; + const form = React.createRef(); const wrapper = mount(
- { - form = instance; - }} - > + @@ -109,7 +89,7 @@ describe('legacy.dynamic-rule', () => { wrapper.update(); - return [form, wrapper]; + return [form, wrapper] as const; }); }); }); diff --git a/tests/legacy/field-props.test.js b/tests/legacy/field-props.test.tsx similarity index 69% rename from tests/legacy/field-props.test.js rename to tests/legacy/field-props.test.tsx index 675b814a..816e76a1 100644 --- a/tests/legacy/field-props.test.js +++ b/tests/legacy/field-props.test.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { mount } from 'enzyme'; +import type { FormInstance } from '../../src'; import Form, { Field } from '../../src'; import { Input } from '../common/InfoField'; import { changeValue, getField, matchArray } from '../common'; @@ -7,15 +8,11 @@ import { changeValue, getField, matchArray } from '../common'; describe('legacy.field-props', () => { // https://github.com/ant-design/ant-design/issues/8985 it('support disordered array', async () => { - let form; + const form = React.createRef(); mount(
- { - form = instance; - }} - > + @@ -27,7 +24,7 @@ describe('legacy.field-props', () => { ); try { - await form.validateFields(); + await form.current?.validateFields(); throw new Error('Should not pass!'); } catch ({ errorFields }) { matchArray( @@ -42,15 +39,11 @@ describe('legacy.field-props', () => { }); it('getValueFromEvent', async () => { - let form; + const form = React.createRef(); const wrapper = mount(
- { - form = instance; - }} - > + `${e.target.value}1`}> @@ -59,18 +52,14 @@ describe('legacy.field-props', () => { ); await changeValue(getField(wrapper), '2'); - expect(form.getFieldValue('normal')).toBe('21'); + expect(form.current?.getFieldValue('normal')).toBe('21'); }); it('normalize', async () => { - let form; + const form = React.createRef(); const wrapper = mount(
- { - form = instance; - }} - > + v && v.toUpperCase()}> @@ -80,23 +69,15 @@ describe('legacy.field-props', () => { await changeValue(getField(wrapper), 'a'); - expect(form.getFieldValue('normal')).toBe('A'); - expect( - getField(wrapper) - .find('input') - .props().value, - ).toBe('A'); + expect(form.current?.getFieldValue('normal')).toBe('A'); + expect(getField(wrapper).find('input').props().value).toBe('A'); }); it('support jsx message', async () => { - let form; + const form = React.createRef(); const wrapper = mount(
- { - form = instance; - }} - > + 1 }]}> @@ -105,7 +86,7 @@ describe('legacy.field-props', () => { ); await changeValue(getField(wrapper), ''); - expect(form.getFieldError('required').length).toBe(1); - expect(form.getFieldError('required')[0].type).toBe('b'); + expect(form.current?.getFieldError('required').length).toBe(1); + expect((form.current?.getFieldError('required')[0] as any).type).toBe('b'); }); }); diff --git a/tests/legacy/form.test.js b/tests/legacy/form.test.js deleted file mode 100644 index 4e4c3d8d..00000000 --- a/tests/legacy/form.test.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { mount } from 'enzyme'; -import Form, { Field } from '../../src'; -import { Input } from '../common/InfoField'; -import { changeValue, getField } from '../common'; -import timeout from '../common/timeout'; - -describe('legacy.form', () => { - // https://github.com/ant-design/ant-design/issues/8386 - it('should work even set with undefined name', async () => { - let form; - mount( -
- { - form = instance; - }} - initialValues={{ normal: '1' }} - > - - - - -
, - ); - - form.setFieldsValue({ - normal: '2', - notExist: 'oh', - }); - - expect(form.getFieldValue('normal')).toBe('2'); - }); - - // [Legacy] Seems useless - it('can reset hidden fields', () => {}); - - // [Legacy] Should move to Ant Design - it('form name', () => {}); -}); diff --git a/tests/legacy/form.test.tsx b/tests/legacy/form.test.tsx new file mode 100644 index 00000000..0f960d84 --- /dev/null +++ b/tests/legacy/form.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import type { FormInstance } from '../../src'; +import Form, { Field } from '../../src'; +import { Input } from '../common/InfoField'; + +describe('legacy.form', () => { + // https://github.com/ant-design/ant-design/issues/8386 + it('should work even set with undefined name', async () => { + const form = React.createRef(); + render( +
+
+ + + +
+
, + ); + form.current?.setFieldsValue({ normal: '2', notExist: 'oh' }); + expect(form.current?.getFieldValue('normal')).toBe('2'); + }); +}); diff --git a/tests/legacy/switch-field.test.js b/tests/legacy/switch-field.test.tsx similarity index 75% rename from tests/legacy/switch-field.test.js rename to tests/legacy/switch-field.test.tsx index 52ee535b..f4e6b067 100644 --- a/tests/legacy/switch-field.test.js +++ b/tests/legacy/switch-field.test.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import { mount } from 'enzyme'; +import { render, fireEvent } from '@testing-library/react'; +import type { FormInstance } from '../../src'; import Form, { Field, useForm } from '../../src'; import { Input } from '../common/InfoField'; -import { changeValue, getField } from '../common'; -import timeout from '../common/timeout'; // https://github.com/ant-design/ant-design/issues/12560 describe('legacy.switch-field', () => { @@ -22,9 +21,9 @@ describe('legacy.switch-field', () => { // Prepare - let form; + let form: FormInstance = null; - const Demo = () => { + const Demo: React.FC = () => { [form] = useForm(); const [list, setList] = React.useState(['a', 'b', 'c']); const [one, two, three] = list; @@ -58,29 +57,14 @@ describe('legacy.switch-field', () => { }; it('Preserve right fields when switch them', async () => { - const wrapper = mount(); - - wrapper - .find('.one') - .last() - .simulate('change', { target: { value: 'value1' } }); + const { container } = render(); + fireEvent.change(container.querySelector('.one'), { target: { value: 'value1' } }); expect(Object.keys(form.getFieldsValue())).toEqual(expect.arrayContaining(['a'])); expect(form.getFieldValue('a')).toBe('value1'); - expect( - wrapper - .find('.one') - .last() - .getDOMNode().value, - ).toBe('value1'); - - wrapper.find('.sw').simulate('click'); + expect(container.querySelector('.one')?.value).toBe('value1'); + fireEvent.click(container.querySelector('.sw')); expect(Object.keys(form.getFieldsValue())).toEqual(expect.arrayContaining(['a'])); expect(form.getFieldValue('a')).toBe('value1'); - expect( - wrapper - .find('.two') - .last() - .getDOMNode().value, - ).toBe('value1'); + expect(container.querySelector('.two')?.value).toBe('value1'); }); }); diff --git a/tests/legacy/validate-array.test.js b/tests/legacy/validate-array.test.tsx similarity index 59% rename from tests/legacy/validate-array.test.js rename to tests/legacy/validate-array.test.tsx index da4a159f..6a81ddb8 100644 --- a/tests/legacy/validate-array.test.js +++ b/tests/legacy/validate-array.test.tsx @@ -1,32 +1,26 @@ import React from 'react'; -import { mount } from 'enzyme'; +import { render } from '@testing-library/react'; +import type { FormInstance } from '../../src'; import Form, { Field } from '../../src'; -import { Input } from '../common/InfoField'; -import { changeValue, getField, matchArray } from '../common'; -import timeout from '../common/timeout'; +import { matchArray } from '../common'; describe('legacy.validate-array', () => { - const MyInput = ({ value = [''], onChange, ...props }) => ( + const MyInput: React.FC = ({ value = [''], onChange, ...props }) => ( { onChange(e.target.value.split(',')); }} - value={value.join(',')} /> ); it('forceValidate works', async () => { - let form; + const form = React.createRef(); - mount( + render(
-
{ - form = instance; - }} - initialValues={{ url_array: ['test'] }} - > + { ); try { - await form.validateFields(); + await form.current?.validateFields(); throw new Error('Should not pass!'); } catch ({ errorFields }) { matchArray( @@ -58,32 +52,18 @@ describe('legacy.validate-array', () => { // https://github.com/ant-design/ant-design/issues/36436 it('antd issue #36436', async () => { - let form; - - mount( + const form = React.createRef(); + render(
- { - form = instance; - }} - > - + +
, ); - expect(async () => { - await form.validateFields(); + await form.current?.validateFields(); }).not.toThrow(); }); }); diff --git a/tests/utils.test.js b/tests/utils.test.ts similarity index 100% rename from tests/utils.test.js rename to tests/utils.test.ts From c3f3e8450f0a5a14f9be9560cb4a42ca3518faff Mon Sep 17 00:00:00 2001 From: lijianan <574980606@qq.com> Date: Thu, 2 Feb 2023 11:09:22 +0800 Subject: [PATCH 07/65] Migrate to testing-lib (#553) * add * add * fix type * add test case * fix test case * fix key * add * add type * add --- docs/examples/components/LabelField.tsx | 2 +- docs/examples/components/useDraggable.ts | 3 +- docs/examples/renderProps.tsx | 2 - docs/examples/validate.tsx | 15 +- tests/common/InfoField.tsx | 4 +- tests/common/index.ts | 18 +- tests/context.test.tsx | 24 +- tests/control.test.tsx | 18 +- tests/dependencies.test.tsx | 23 +- tests/index.test.tsx | 401 ++++++++++------------- tests/list.test.tsx | 178 +++++----- tests/preserve.test.tsx | 401 ++++++++++------------- tests/useWatch.test.tsx | 193 +++++------ tests/validate-warning.test.tsx | 62 +--- tests/validate.test.tsx | 5 +- 15 files changed, 566 insertions(+), 783 deletions(-) diff --git a/docs/examples/components/LabelField.tsx b/docs/examples/components/LabelField.tsx index 2608f2bd..1169d465 100644 --- a/docs/examples/components/LabelField.tsx +++ b/docs/examples/components/LabelField.tsx @@ -9,7 +9,7 @@ interface ErrorProps { children?: React.ReactNode[]; } -const Error = ({ children, warning }: ErrorProps) => ( +const Error: React.FC = ({ children, warning }) => (
    {children.map((error: React.ReactNode, index: number) => (
  • {error}
  • diff --git a/docs/examples/components/useDraggable.ts b/docs/examples/components/useDraggable.ts index 020bdec4..0dde2ffb 100644 --- a/docs/examples/components/useDraggable.ts +++ b/docs/examples/components/useDraggable.ts @@ -1,5 +1,6 @@ import { useRef } from 'react'; -import { DragObjectWithType, useDrag, useDrop } from 'react-dnd'; +import type { DragObjectWithType } from 'react-dnd'; +import { useDrag, useDrop } from 'react-dnd'; type DragWithIndex = DragObjectWithType & { index: number; diff --git a/docs/examples/renderProps.tsx b/docs/examples/renderProps.tsx index 13f6c399..4aa8dbcd 100644 --- a/docs/examples/renderProps.tsx +++ b/docs/examples/renderProps.tsx @@ -1,4 +1,3 @@ -/* eslint-disable jsx-a11y/label-has-associated-control */ import React from 'react'; import Form from 'rc-field-form'; import Input from './components/Input'; @@ -45,7 +44,6 @@ export default class Demo extends React.Component {
)} - {list.map((_, index) => ( diff --git a/docs/examples/validate.tsx b/docs/examples/validate.tsx index 059ebf1b..3a3b29f9 100644 --- a/docs/examples/validate.tsx +++ b/docs/examples/validate.tsx @@ -6,7 +6,11 @@ import Input from './components/Input'; const { Field } = Form; -const Error = ({ children }) => ( +interface ErrorProps { + children?: React.ReactNode[]; +} + +const Error: React.FC = ({ children }) => (
    {children.map((error, i) => (
  • {error}
  • @@ -19,14 +23,7 @@ const FieldState = ({ form, name }) => { const validating = form.isFieldValidating(name); return ( -
    +
    {touched ? Touched! : null} {validating ? Validating! : null}
    diff --git a/tests/common/InfoField.tsx b/tests/common/InfoField.tsx index ecf75ecd..6aa48051 100644 --- a/tests/common/InfoField.tsx +++ b/tests/common/InfoField.tsx @@ -6,7 +6,9 @@ interface InfoFieldProps extends FieldProps { children?: React.ReactElement; } -export const Input = ({ value = '', ...props }) => ; +export const Input: React.FC = ({ value = '', ...props }) => ( + +); /** * Return a wrapped Field with meta info diff --git a/tests/common/index.ts b/tests/common/index.ts index 5aa3b8b5..23af21c2 100644 --- a/tests/common/index.ts +++ b/tests/common/index.ts @@ -4,7 +4,7 @@ import timeout from './timeout'; import { Field } from '../../src'; import { getNamePath, matchNamePath } from '../../src/utils/valueUtil'; -export async function changeValue(wrapper, value) { +export async function changeValue(wrapper: ReactWrapper, value: string | string[]) { wrapper.find('input').simulate('change', { target: { value } }); await act(async () => { await timeout(); @@ -40,17 +40,15 @@ export function matchError( } } -export function getField(wrapper, index: string | number | string[] = 0) { +export function getField(wrapper: ReactWrapper, index: string | number | string[] = 0) { if (typeof index === 'number') { return wrapper.find(Field).at(index); } - const name = getNamePath(index); const fields = wrapper.find(Field); for (let i = 0; i < fields.length; i += 1) { const field = fields.at(i); - const fieldName = getNamePath(field.props().name); - + const fieldName = getNamePath((field.props() as any).name); if (matchNamePath(name, fieldName)) { return field; } @@ -58,7 +56,7 @@ export function getField(wrapper, index: string | number | string[] = 0) { return null; } -export function matchArray(source, target, matchKey) { +export function matchArray(source: any[], target: any[], matchKey: React.Key) { expect(matchKey).toBeTruthy(); try { @@ -66,10 +64,10 @@ export function matchArray(source, target, matchKey) { } catch (err) { throw new Error( ` -Array length not match. -source(${source.length}): ${JSON.stringify(source)} -target(${target.length}): ${JSON.stringify(target)} -`.trim(), + Array length not match. + source(${source.length}): ${JSON.stringify(source)} + target(${target.length}): ${JSON.stringify(target)} + `.trim(), ); } diff --git a/tests/context.test.tsx b/tests/context.test.tsx index d4a5c193..b1ceefa5 100644 --- a/tests/context.test.tsx +++ b/tests/context.test.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { mount } from 'enzyme'; +import { render } from '@testing-library/react'; +import type { FormInstance } from '../src'; import Form, { FormProvider } from '../src'; import InfoField from './common/InfoField'; import { changeValue, matchError, getField } from './common'; @@ -77,7 +79,7 @@ describe('Form.Context', () => { it('multiple context', async () => { const onFormChange = jest.fn(); - const Demo = changed => ( + const Demo: React.FC<{ changed?: boolean }> = ({ changed }) => ( {!changed ? ( @@ -105,17 +107,12 @@ describe('Form.Context', () => { it('submit', async () => { const onFormFinish = jest.fn(); - let form1; + const form = React.createRef(); const wrapper = mount(
    -
    { - form1 = instance; - }} - > +
    @@ -124,12 +121,12 @@ describe('Form.Context', () => { ); await changeValue(getField(wrapper), ''); - form1.submit(); + form.current?.submit(); await timeout(); expect(onFormFinish).not.toHaveBeenCalled(); await changeValue(getField(wrapper), 'Light'); - form1.submit(); + form.current?.submit(); await timeout(); expect(onFormFinish).toHaveBeenCalled(); @@ -140,14 +137,11 @@ describe('Form.Context', () => { }); it('do nothing if no Provider in use', () => { - const wrapper = mount( + const { rerender } = render(
    , ); - - wrapper.setProps({ - children: null, - }); + rerender(
    {null}
    ); }); }); diff --git a/tests/control.test.tsx b/tests/control.test.tsx index 751fdd8a..d7575217 100644 --- a/tests/control.test.tsx +++ b/tests/control.test.tsx @@ -3,25 +3,25 @@ import { mount } from 'enzyme'; import Form from '../src'; import InfoField from './common/InfoField'; import { changeValue, matchError } from './common'; +import { render } from '@testing-library/react'; describe('Form.Control', () => { it('fields', () => { - const wrapper = mount( + const { container, rerender } = render( , ); - - wrapper.setProps({ - fields: [{ name: 'username', value: 'Bamboo' }], - }); - wrapper.update(); - - expect(wrapper.find('input').props().value).toEqual('Bamboo'); + rerender( +
    + + , + ); + expect(container.querySelector('input')?.value).toBe('Bamboo'); }); it('fully test', async () => { - const Test = () => { + const Test: React.FC = () => { const [fields, setFields] = React.useState([]); return ( diff --git a/tests/dependencies.test.tsx b/tests/dependencies.test.tsx index 4268b4b2..c00e2f4b 100644 --- a/tests/dependencies.test.tsx +++ b/tests/dependencies.test.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { mount } from 'enzyme'; +import type { FormInstance } from '../src'; import Form, { Field } from '../src'; import timeout from './common/timeout'; import InfoField, { Input } from './common/InfoField'; @@ -7,15 +8,11 @@ import { changeValue, matchError, getField } from './common'; describe('Form.Dependencies', () => { it('touched', async () => { - let form = null; + const form = React.createRef(); const wrapper = mount(
    -
    { - form = instance; - }} - > + @@ -27,13 +24,13 @@ describe('Form.Dependencies', () => { matchError(getField(wrapper, 1), false); // Trigger if touched - form.setFields([{ name: 'field_2', touched: true }]); + form.current?.setFields([{ name: 'field_2', touched: true }]); await changeValue(getField(wrapper, 0), ''); matchError(getField(wrapper, 1), true); }); describe('initialValue', () => { - function test(name, formProps, fieldProps = {}) { + function test(name: string, formProps = {}, fieldProps = {}) { it(name, async () => { let validated = false; @@ -68,16 +65,12 @@ describe('Form.Dependencies', () => { }); it('nest dependencies', async () => { - let form = null; + const form = React.createRef(); let rendered = false; const wrapper = mount(
    -
    { - form = instance; - }} - > + @@ -94,7 +87,7 @@ describe('Form.Dependencies', () => {
    , ); - form.setFields([ + form.current?.setFields([ { name: 'field_1', touched: true }, { name: 'field_2', touched: true }, { name: 'field_3', touched: true }, diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 38228982..74c5fd78 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -1,4 +1,5 @@ import { mount } from 'enzyme'; +import { fireEvent, render } from '@testing-library/react'; import { resetWarned } from 'rc-util/lib/warning'; import React from 'react'; import type { FormInstance } from '../src'; @@ -6,74 +7,90 @@ import Form, { Field, useForm } from '../src'; import { changeValue, getField, matchError } from './common'; import InfoField, { Input } from './common/InfoField'; import timeout from './common/timeout'; +import type { Meta } from '@/interface'; describe('Form.Basic', () => { describe('create form', () => { - function renderContent() { - return ( -
    - - - - {() => null} - -
    - ); - } + const Content: React.FC = () => ( +
    + + + + {() => null} + +
    + ); it('sub component', () => { - const wrapper = mount({renderContent()}); - expect(wrapper.find('form')).toBeTruthy(); - expect(wrapper.find('input').length).toBe(2); + const { container } = render( +
    + + , + ); + expect(container.querySelector('form')).toBeTruthy(); + expect(container.querySelectorAll('input').length).toBe(2); }); describe('component', () => { it('without dom', () => { - const wrapper = mount(
    {renderContent()}
    ); - expect(wrapper.find('form').length).toBe(0); - expect(wrapper.find('input').length).toBe(2); + const { container } = render( +
    + + , + ); + expect(container.querySelectorAll('form').length).toBe(0); + expect(container.querySelectorAll('input').length).toBe(2); }); it('use string', () => { - const wrapper = mount(
    {renderContent()}
    ); - expect(wrapper.find('form').length).toBe(0); - expect(wrapper.find('pre').length).toBe(1); - expect(wrapper.find('input').length).toBe(2); + const { container } = render( +
    + + , + ); + expect(container.querySelectorAll('form').length).toBe(0); + expect(container.querySelectorAll('pre').length).toBe(1); + expect(container.querySelectorAll('input').length).toBe(2); }); it('use component', () => { - const MyComponent = ({ children }) =>
    {children}
    ; - const wrapper = mount(
    {renderContent()}
    ); - expect(wrapper.find('form').length).toBe(0); - expect(wrapper.find(MyComponent).length).toBe(1); - expect(wrapper.find('input').length).toBe(2); + const Component: React.FC = ({ children }) => ( +
    {children}
    + ); + const { container } = render( +
    + + , + ); + expect(container.querySelectorAll('form').length).toBe(0); + expect(container.querySelectorAll('.customize').length).toBe(1); + expect(container.querySelectorAll('input').length).toBe(2); }); }); describe('render props', () => { it('normal', () => { - const wrapper = mount(
    {renderContent}
    ); - expect(wrapper.find('form')).toBeTruthy(); - expect(wrapper.find('input').length).toBe(2); + const { container } = render( +
    + + , + ); + expect(container.querySelector('form')).toBeTruthy(); + expect(container.querySelectorAll('input').length).toBe(2); }); - it('empty', () => { - const wrapper = mount(
    {() => null}
    ); - expect(wrapper.find('form')).toBeTruthy(); + const { container } = render(
    {() => null}
    ); + expect(container.querySelector('form')).toBeTruthy(); }); }); }); it('fields touched', async () => { - let form; + const form = React.createRef(); const wrapper = mount(
    -
    { - form = instance; - }} - > + {() => null} @@ -81,36 +98,32 @@ describe('Form.Basic', () => {
    , ); - expect(form.isFieldsTouched()).toBeFalsy(); - expect(form.isFieldsTouched(['username', 'password'])).toBeFalsy(); + expect(form.current?.isFieldsTouched()).toBeFalsy(); + expect(form.current?.isFieldsTouched(['username', 'password'])).toBeFalsy(); await changeValue(getField(wrapper, 0), 'Bamboo'); - expect(form.isFieldsTouched()).toBeTruthy(); - expect(form.isFieldsTouched(['username', 'password'])).toBeTruthy(); - expect(form.isFieldsTouched(true)).toBeFalsy(); - expect(form.isFieldsTouched(['username', 'password'], true)).toBeFalsy(); + expect(form.current?.isFieldsTouched()).toBeTruthy(); + expect(form.current?.isFieldsTouched(['username', 'password'])).toBeTruthy(); + expect(form.current?.isFieldsTouched(true)).toBeFalsy(); + expect(form.current?.isFieldsTouched(['username', 'password'], true)).toBeFalsy(); await changeValue(getField(wrapper, 1), 'Light'); - expect(form.isFieldsTouched()).toBeTruthy(); - expect(form.isFieldsTouched(['username', 'password'])).toBeTruthy(); - expect(form.isFieldsTouched(true)).toBeTruthy(); - expect(form.isFieldsTouched(['username', 'password'], true)).toBeTruthy(); + expect(form.current?.isFieldsTouched()).toBeTruthy(); + expect(form.current?.isFieldsTouched(['username', 'password'])).toBeTruthy(); + expect(form.current?.isFieldsTouched(true)).toBeTruthy(); + expect(form.current?.isFieldsTouched(['username', 'password'], true)).toBeTruthy(); }); describe('reset form', () => { - function resetTest(name, ...args) { + function resetTest(name: string, ...args) { it(name, async () => { - let form; + const form = React.createRef(); const onReset = jest.fn(); const onMeta = jest.fn(); const wrapper = mount(
    - { - form = instance; - }} - > + { ); await changeValue(getField(wrapper, 'username'), 'Bamboo'); - expect(form.getFieldValue('username')).toEqual('Bamboo'); - expect(form.getFieldError('username')).toEqual([]); - expect(form.isFieldTouched('username')).toBeTruthy(); + expect(form.current?.getFieldValue('username')).toEqual('Bamboo'); + expect(form.current?.getFieldError('username')).toEqual([]); + expect(form.current?.isFieldTouched('username')).toBeTruthy(); expect(onMeta).toHaveBeenCalledWith( - expect.objectContaining({ - touched: true, - errors: [], - warnings: [], - }), + expect.objectContaining({ touched: true, errors: [], warnings: [] }), ); expect(onReset).not.toHaveBeenCalled(); onMeta.mockRestore(); onReset.mockRestore(); - form.resetFields(...args); - expect(form.getFieldValue('username')).toEqual(undefined); - expect(form.getFieldError('username')).toEqual([]); - expect(form.isFieldTouched('username')).toBeFalsy(); + form.current?.resetFields(...args); + expect(form.current?.getFieldValue('username')).toEqual(undefined); + expect(form.current?.getFieldError('username')).toEqual([]); + expect(form.current?.isFieldTouched('username')).toBeFalsy(); expect(onMeta).toHaveBeenCalledWith( - expect.objectContaining({ - touched: false, - errors: [], - warnings: [], - }), + expect.objectContaining({ touched: false, errors: [], warnings: [] }), ); expect(onReset).toHaveBeenCalled(); onMeta.mockRestore(); onReset.mockRestore(); await changeValue(getField(wrapper, 'username'), ''); - expect(form.getFieldValue('username')).toEqual(''); - expect(form.getFieldError('username')).toEqual(["'username' is required"]); - expect(form.isFieldTouched('username')).toBeTruthy(); + expect(form.current?.getFieldValue('username')).toEqual(''); + expect(form.current?.getFieldError('username')).toEqual(["'username' is required"]); + expect(form.current?.isFieldTouched('username')).toBeTruthy(); expect(onMeta).toHaveBeenCalledWith( expect.objectContaining({ touched: true, @@ -168,16 +173,12 @@ describe('Form.Basic', () => { onMeta.mockRestore(); onReset.mockRestore(); - form.resetFields(...args); - expect(form.getFieldValue('username')).toEqual(undefined); - expect(form.getFieldError('username')).toEqual([]); - expect(form.isFieldTouched('username')).toBeFalsy(); + form.current?.resetFields(...args); + expect(form.current?.getFieldValue('username')).toEqual(undefined); + expect(form.current?.getFieldError('username')).toEqual([]); + expect(form.current?.isFieldTouched('username')).toBeFalsy(); expect(onMeta).toHaveBeenCalledWith( - expect.objectContaining({ - touched: false, - errors: [], - warnings: [], - }), + expect.objectContaining({ touched: false, errors: [], warnings: [] }), ); expect(onReset).toHaveBeenCalled(); }); @@ -187,15 +188,11 @@ describe('Form.Basic', () => { resetTest('without field name'); it('not affect others', async () => { - let form; + const form = React.createRef(); const wrapper = mount(
    - { - form = instance; - }} - > + @@ -209,27 +206,26 @@ describe('Form.Basic', () => { await changeValue(getField(wrapper, 'username'), 'Bamboo'); await changeValue(getField(wrapper, 'password'), ''); - form.resetFields(['username']); - - expect(form.getFieldValue('username')).toEqual(undefined); - expect(form.getFieldError('username')).toEqual([]); - expect(form.isFieldTouched('username')).toBeFalsy(); - expect(form.getFieldValue('password')).toEqual(''); - expect(form.getFieldError('password')).toEqual(["'password' is required"]); - expect(form.isFieldTouched('password')).toBeTruthy(); + form.current?.resetFields(['username']); + + expect(form.current?.getFieldValue('username')).toEqual(undefined); + expect(form.current?.getFieldError('username')).toEqual([]); + expect(form.current?.isFieldTouched('username')).toBeFalsy(); + expect(form.current?.getFieldValue('password')).toEqual(''); + expect(form.current?.getFieldError('password')).toEqual(["'password' is required"]); + expect(form.current?.isFieldTouched('password')).toBeTruthy(); }); it('remove Field should trigger onMetaChange', () => { const onMetaChange = jest.fn(); - const wrapper = mount( + const { unmount } = render( , ); - - wrapper.unmount(); + unmount(); expect(onMetaChange).toHaveBeenCalledWith(expect.objectContaining({ destroy: true })); }); }); @@ -237,7 +233,7 @@ describe('Form.Basic', () => { it('should throw if no Form in use', () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - mount( + render( , @@ -269,7 +265,7 @@ describe('Form.Basic', () => { it('onValuesChange should not return fully value', async () => { const onValuesChange = jest.fn(); - const Demo = ({ showField = true }) => ( + const Demo: React.FC = ({ showField = true }) => (
    {showField && ( @@ -351,18 +347,14 @@ describe('Form.Basic', () => { it('getInternalHooks should not usable by user', () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - let form; - mount( + const form = React.createRef(); + render(
    - { - form = instance; - }} - /> +
    , ); - expect(form.getInternalHooks()).toEqual(null); + expect((form.current as any)?.getInternalHooks()).toEqual(null); expect(errorSpy).toHaveBeenCalledWith( 'Warning: `getInternalHooks` is internal usage. Should not call directly.', @@ -372,14 +364,10 @@ describe('Form.Basic', () => { }); it('valuePropName', async () => { - let form; + const form = React.createRef(); const wrapper = mount(
    - { - form = instance; - }} - > + @@ -389,11 +377,11 @@ describe('Form.Basic', () => { wrapper.find('input[type="checkbox"]').simulate('change', { target: { checked: true } }); await timeout(); - expect(form.getFieldsValue()).toEqual({ check: true }); + expect(form.current?.getFieldsValue()).toEqual({ check: true }); wrapper.find('input[type="checkbox"]').simulate('change', { target: { checked: false } }); await timeout(); - expect(form.getFieldsValue()).toEqual({ check: false }); + expect(form.current?.getFieldsValue()).toEqual({ check: false }); }); it('getValueProps', async () => { @@ -412,8 +400,8 @@ describe('Form.Basic', () => { describe('shouldUpdate', () => { it('work', async () => { - let isAllTouched; - let hasError; + let isAllTouched: boolean; + let hasError: number; const wrapper = mount( @@ -465,8 +453,8 @@ describe('Form.Basic', () => { return ( ); }} @@ -483,14 +471,10 @@ describe('Form.Basic', () => { describe('setFields', () => { it('should work', () => { - let form; + const form = React.createRef(); const wrapper = mount(
    - { - form = instance; - }} - > + @@ -498,26 +482,21 @@ describe('Form.Basic', () => {
    , ); - form.setFields([ - { - name: 'username', - touched: false, - validating: true, - errors: ['Set It!'], - }, + form.current?.setFields([ + { name: 'username', touched: false, validating: true, errors: ['Set It!'] }, ]); wrapper.update(); matchError(wrapper, 'Set It!'); expect(wrapper.find('.validating').length).toBeTruthy(); - expect(form.isFieldsTouched()).toBeFalsy(); + expect(form.current?.isFieldsTouched()).toBeFalsy(); }); it('should trigger by setField', () => { const triggerUpdate = jest.fn(); const formRef = React.createRef(); - const wrapper = mount( + render(
    prev.value !== next.value}> @@ -529,17 +508,17 @@ describe('Form.Basic', () => {
    , ); - wrapper.update(); + triggerUpdate.mockReset(); // Not trigger render formRef.current.setFields([{ name: 'others', value: 'no need to update' }]); - wrapper.update(); + expect(triggerUpdate).not.toHaveBeenCalled(); // Trigger render formRef.current.setFields([{ name: 'value', value: 'should update' }]); - wrapper.update(); + expect(triggerUpdate).toHaveBeenCalled(); }); }); @@ -548,7 +527,7 @@ describe('Form.Basic', () => { let called1 = false; let called2 = false; - mount( + render(
    {(_, meta) => { @@ -572,16 +551,12 @@ describe('Form.Basic', () => { }); it('setFieldsValue should clean up status', async () => { - let form; - let currentMeta; + const form = React.createRef(); + let currentMeta: Meta = null; const wrapper = mount(
    - { - form = instance; - }} - > + new Promise(() => {}) }]}> {(control, meta) => { currentMeta = meta; @@ -593,44 +568,40 @@ describe('Form.Basic', () => { ); // Init - expect(form.getFieldValue('normal')).toBe(undefined); - expect(form.isFieldTouched('normal')).toBeFalsy(); - expect(form.getFieldError('normal')).toEqual([]); + expect(form.current?.getFieldValue('normal')).toBe(undefined); + expect(form.current?.isFieldTouched('normal')).toBeFalsy(); + expect(form.current?.getFieldError('normal')).toEqual([]); expect(currentMeta.validating).toBeFalsy(); // Set it - form.setFieldsValue({ - normal: 'Light', - }); + form.current?.setFieldsValue({ normal: 'Light' }); - expect(form.getFieldValue('normal')).toBe('Light'); - expect(form.isFieldTouched('normal')).toBeTruthy(); - expect(form.getFieldError('normal')).toEqual([]); + expect(form.current?.getFieldValue('normal')).toBe('Light'); + expect(form.current?.isFieldTouched('normal')).toBeTruthy(); + expect(form.current?.getFieldError('normal')).toEqual([]); expect(currentMeta.validating).toBeFalsy(); // Input it await changeValue(getField(wrapper), 'Bamboo'); - expect(form.getFieldValue('normal')).toBe('Bamboo'); - expect(form.isFieldTouched('normal')).toBeTruthy(); - expect(form.getFieldError('normal')).toEqual([]); + expect(form.current?.getFieldValue('normal')).toBe('Bamboo'); + expect(form.current?.isFieldTouched('normal')).toBeTruthy(); + expect(form.current?.getFieldError('normal')).toEqual([]); expect(currentMeta.validating).toBeTruthy(); // Set it again - form.setFieldsValue({ - normal: 'Light', - }); + form.current?.setFieldsValue({ normal: 'Light' }); - expect(form.getFieldValue('normal')).toBe('Light'); - expect(form.isFieldTouched('normal')).toBeTruthy(); - expect(form.getFieldError('normal')).toEqual([]); + expect(form.current?.getFieldValue('normal')).toBe('Light'); + expect(form.current?.isFieldTouched('normal')).toBeTruthy(); + expect(form.current?.getFieldError('normal')).toEqual([]); expect(currentMeta.validating).toBeFalsy(); }); it('warning if invalidate element', () => { resetWarned(); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - mount( + render(
    {/* @ts-ignore */} @@ -652,14 +623,14 @@ describe('Form.Basic', () => { resetWarned(); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const Test = () => { + const Test: React.FC = () => { const [form] = useForm(); form.getFieldsValue(); return ; }; - mount(); + render(); jest.runAllTimers(); expect(errorSpy).toHaveBeenCalledWith( @@ -670,15 +641,11 @@ describe('Form.Basic', () => { }); it('filtering fields by meta', async () => { - let form; + const form = React.createRef(); const wrapper = mount(
    - { - form = instance; - }} - > + {() => null} @@ -687,37 +654,34 @@ describe('Form.Basic', () => { ); expect( - form.getFieldsValue(null, meta => { + form.current?.getFieldsValue(null, meta => { expect(Object.keys(meta)).toEqual(['touched', 'validating', 'errors', 'warnings', 'name']); return false; }), ).toEqual({}); - expect(form.getFieldsValue(null, () => true)).toEqual(form.getFieldsValue()); - expect(form.getFieldsValue(null, meta => meta.touched)).toEqual({}); + expect(form.current?.getFieldsValue(null, () => true)).toEqual(form.current?.getFieldsValue()); + expect(form.current?.getFieldsValue(null, meta => meta.touched)).toEqual({}); await changeValue(getField(wrapper, 0), 'Bamboo'); - expect(form.getFieldsValue(null, () => true)).toEqual(form.getFieldsValue()); - expect(form.getFieldsValue(null, meta => meta.touched)).toEqual({ + expect(form.current?.getFieldsValue(null, () => true)).toEqual(form.current?.getFieldsValue()); + expect(form.current?.getFieldsValue(null, meta => meta.touched)).toEqual({ username: 'Bamboo', }); - expect(form.getFieldsValue(['username'], meta => meta.touched)).toEqual({ + expect(form.current?.getFieldsValue(['username'], meta => meta.touched)).toEqual({ username: 'Bamboo', }); - expect(form.getFieldsValue(['password'], meta => meta.touched)).toEqual({}); + expect(form.current?.getFieldsValue(['password'], meta => meta.touched)).toEqual({}); }); it('should not crash when return value contains target field', async () => { const CustomInput: React.FC = ({ value, onChange }) => { - const onInputChange = e => { - onChange({ - value: e.target.value, - target: 'string', - }); + const onInputChange = (e: React.ChangeEvent) => { + onChange({ value: e.target.value, target: 'string' }); }; return ; }; - const wrapper = mount( + const { container } = render( @@ -725,12 +689,12 @@ describe('Form.Basic', () => { , ); expect(() => { - wrapper.find('Input').simulate('change', { event: { target: { value: 'Light' } } }); + fireEvent.change(container.querySelector('input'), { target: { value: 'Light' } }); }).not.toThrowError(); }); it('setFieldsValue for List should work', () => { - const Demo = () => { + const Demo: React.FC = () => { const [form] = useForm(); const handelReset = () => { @@ -774,11 +738,10 @@ describe('Form.Basic', () => { ); }; - - const wrapper = mount(); - expect(wrapper.find('input').first().getDOMNode().value).toBe('11'); - wrapper.find('.reset-btn').first().simulate('click'); - expect(wrapper.find('input').length).toBe(0); + const { container } = render(); + expect(container.querySelector('input')?.value).toBe('11'); + fireEvent.click(container.querySelector('.reset-btn')); + expect(container.querySelectorAll('input').length).toBe(0); }); it('setFieldsValue should work for multiple Select', () => { @@ -786,7 +749,7 @@ describe('Form.Basic', () => { return
    {(value || defaultValue || []).toString()}
    ; }; - const Demo = () => { + const Demo: React.FC = () => { const [formInstance] = Form.useForm(); React.useEffect(() => { @@ -802,18 +765,17 @@ describe('Form.Basic', () => { ); }; - const wrapper = mount(); - expect(wrapper.find('.select-div').text()).toBe('K1,K2'); + const { container } = render(); + expect(container.querySelector('.select-div').textContent).toBe('K1,K2'); }); // https://github.com/ant-design/ant-design/issues/34768 it('remount should not clear current value', () => { - let refForm; + let refForm: FormInstance = null; const Demo: React.FC = ({ remount }) => { const [form] = Form.useForm(); refForm = form; - let node = (
    @@ -829,16 +791,11 @@ describe('Form.Basic', () => { return node; }; - const wrapper = mount(); + const { container, rerender } = render(); refForm.setFieldsValue({ name: 'bamboo' }); - wrapper.update(); - - expect(wrapper.find('input').prop('value')).toEqual('bamboo'); - - wrapper.setProps({ remount: true }); - wrapper.update(); - - expect(wrapper.find('input').prop('value')).toEqual('bamboo'); + expect(container.querySelector('input').value).toBe('bamboo'); + rerender(); + expect(container.querySelector('input').value).toBe('bamboo'); }); it('setFieldValue', () => { @@ -849,37 +806,29 @@ describe('Form.Basic', () => { {fields => fields.map(field => ( - + )) } - ); - const wrapper = mount(); - expect(wrapper.find('input').map(input => input.prop('value'))).toEqual([ - 'bamboo', - 'little', - 'light', - 'nested', - ]); + const { container } = render(); + expect( + Array.from(container.querySelectorAll('input')).map(input => input?.value), + ).toEqual(['bamboo', 'little', 'light', 'nested']); // Set formRef.current.setFieldValue(['list', 1], 'tiny'); formRef.current.setFieldValue(['nest', 'target'], 'match'); - wrapper.update(); - expect(wrapper.find('input').map(input => input.prop('value'))).toEqual([ - 'bamboo', - 'tiny', - 'light', - 'match', - ]); + expect( + Array.from(container.querySelectorAll('input')).map(input => input?.value), + ).toEqual(['bamboo', 'tiny', 'light', 'match']); }); }); diff --git a/tests/list.test.tsx b/tests/list.test.tsx index 5d0016d4..06a66132 100644 --- a/tests/list.test.tsx +++ b/tests/list.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mount } from 'enzyme'; import type { ReactWrapper } from 'enzyme'; +import { render } from '@testing-library/react'; import { resetWarned } from 'rc-util/lib/warning'; import Form, { Field, List } from '../src'; import type { FormProps } from '../src'; @@ -13,41 +14,31 @@ import { changeValue, getField } from './common'; import timeout from './common/timeout'; describe('Form.List', () => { - let form; - - function generateForm( - renderList?: ( - fields: ListField[], - operations: ListOperations, - meta: Meta, - ) => JSX.Element | React.ReactNode, + const form = React.createRef(); + + const generateForm = ( + renderList?: (fields: ListField[], operations: ListOperations, meta: Meta) => React.ReactNode, formProps?: FormProps, listProps?: Partial, - ): [ReactWrapper, () => ReactWrapper] { + ): readonly [ReactWrapper, () => ReactWrapper] => { const wrapper = mount(
    -
    { - form = instance; - }} - {...formProps} - > + {renderList}
    , ); - - return [wrapper, () => getField(wrapper).find('div')]; - } + return [wrapper, () => getField(wrapper).find('div')] as const; + }; it('basic', async () => { const [, getList] = generateForm( fields => (
    {fields.map(field => ( - + ))} @@ -60,7 +51,7 @@ describe('Form.List', () => { }, ); - function matchKey(index, key) { + function matchKey(index: number, key: React.Key) { expect(getList().find(Field).at(index).key()).toEqual(key); } @@ -74,14 +65,12 @@ describe('Form.List', () => { await changeValue(getField(listNode, 1), '222'); await changeValue(getField(listNode, 2), '333'); - expect(form.getFieldsValue()).toEqual({ - list: ['111', '222', '333'], - }); + expect(form.current?.getFieldsValue()).toEqual({ list: ['111', '222', '333'] }); }); it('not crash', () => { // Empty only - mount( + render(
    {() => null}
    , @@ -90,7 +79,7 @@ describe('Form.List', () => { // Not a array const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); resetWarned(); - mount( + render(
    {() => null}
    , @@ -100,13 +89,13 @@ describe('Form.List', () => { }); it('operation', async () => { - let operation; + let operation: ListOperations; const [wrapper, getList] = generateForm((fields, opt) => { operation = opt; return (
    {fields.map(field => ( - + ))} @@ -114,7 +103,7 @@ describe('Form.List', () => { ); }); - function matchKey(index, key) { + function matchKey(index: number, key: React.Key) { expect(getList().find(Field).at(index).key()).toEqual(key); } @@ -133,9 +122,7 @@ describe('Form.List', () => { wrapper.update(); expect(getList().find(Field).length).toEqual(3); - expect(form.getFieldsValue()).toEqual({ - list: [undefined, '2', undefined], - }); + expect(form.current?.getFieldsValue()).toEqual({ list: [undefined, '2', undefined] }); matchKey(0, '0'); matchKey(1, '1'); @@ -199,12 +186,12 @@ describe('Form.List', () => { // Modify await changeValue(getField(getList(), 1), '222'); - expect(form.getFieldsValue()).toEqual({ + expect(form.current?.getFieldsValue()).toEqual({ list: [undefined, '222', undefined], }); - expect(form.isFieldTouched(['list', 0])).toBeFalsy(); - expect(form.isFieldTouched(['list', 1])).toBeTruthy(); - expect(form.isFieldTouched(['list', 2])).toBeFalsy(); + expect(form.current?.isFieldTouched(['list', 0])).toBeFalsy(); + expect(form.current?.isFieldTouched(['list', 1])).toBeTruthy(); + expect(form.current?.isFieldTouched(['list', 2])).toBeFalsy(); matchKey(0, '0'); matchKey(1, '1'); @@ -216,11 +203,11 @@ describe('Form.List', () => { }); wrapper.update(); expect(getList().find(Field).length).toEqual(2); - expect(form.getFieldsValue()).toEqual({ + expect(form.current?.getFieldsValue()).toEqual({ list: [undefined, undefined], }); - expect(form.isFieldTouched(['list', 0])).toBeFalsy(); - expect(form.isFieldTouched(['list', 2])).toBeFalsy(); + expect(form.current?.isFieldTouched(['list', 0])).toBeFalsy(); + expect(form.current?.isFieldTouched(['list', 2])).toBeFalsy(); matchKey(0, '0'); matchKey(1, '2'); @@ -245,13 +232,13 @@ describe('Form.List', () => { }); it('remove when the param is Array', () => { - let operation; + let operation: ListOperations; const [wrapper, getList] = generateForm((fields, opt) => { operation = opt; return (
    {fields.map(field => ( - + ))} @@ -259,7 +246,7 @@ describe('Form.List', () => { ); }); - function matchKey(index, key) { + function matchKey(index: number, key: React.Key) { expect(getList().find(Field).at(index).key()).toEqual(key); } @@ -323,14 +310,14 @@ describe('Form.List', () => { }); it('add when the second param is number', () => { - let operation; + let operation: ListOperations; const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const [wrapper, getList] = generateForm((fields, opt) => { operation = opt; return (
    {fields.map(field => ( - + ))} @@ -356,7 +343,7 @@ describe('Form.List', () => { wrapper.update(); expect(getList().find(Field).length).toEqual(3); - expect(form.getFieldsValue()).toEqual({ + expect(form.current?.getFieldsValue()).toEqual({ list: [undefined, '1', '2'], }); @@ -369,7 +356,7 @@ describe('Form.List', () => { wrapper.update(); expect(getList().find(Field).length).toEqual(5); - expect(form.getFieldsValue()).toEqual({ + expect(form.current?.getFieldsValue()).toEqual({ list: ['0', undefined, '1', '4', '2'], }); }); @@ -380,7 +367,7 @@ describe('Form.List', () => { fields => (
    {fields.map(field => ( - + ))} @@ -393,7 +380,7 @@ describe('Form.List', () => { await changeValue(getField(getList()), ''); - expect(form.getFieldError(['list', 0])).toEqual(["'list.0' is required"]); + expect(form.current?.getFieldError(['list', 0])).toEqual(["'list.0' is required"]); }); it('remove should keep error', async () => { @@ -401,7 +388,7 @@ describe('Form.List', () => { (fields, { remove }) => (
    {fields.map(field => ( - + ))} @@ -421,13 +408,13 @@ describe('Form.List', () => { expect(wrapper.find(Input)).toHaveLength(2); await changeValue(getField(getList(), 1), ''); - expect(form.getFieldError(['list', 1])).toEqual(["'list.1' is required"]); + expect(form.current?.getFieldError(['list', 1])).toEqual(["'list.1' is required"]); wrapper.find('button').simulate('click'); wrapper.update(); expect(wrapper.find(Input)).toHaveLength(1); - expect(form.getFieldError(['list', 0])).toEqual(["'list.1' is required"]); + expect(form.current?.getFieldError(['list', 0])).toEqual(["'list.1' is required"]); }); it('when param of remove is array', async () => { @@ -435,7 +422,7 @@ describe('Form.List', () => { (fields, { remove }) => (
    {fields.map(field => ( - + ))} @@ -455,19 +442,23 @@ describe('Form.List', () => { expect(wrapper.find(Input)).toHaveLength(3); await changeValue(getField(getList(), 0), ''); - expect(form.getFieldError(['list', 0])).toEqual(["'list.0' is required"]); + expect(form.current?.getFieldError(['list', 0])).toEqual(["'list.0' is required"]); await changeValue(getField(getList(), 1), 'test'); - expect(form.getFieldError(['list', 1])).toEqual(["'list.1' must be at least 5 characters"]); + expect(form.current?.getFieldError(['list', 1])).toEqual([ + "'list.1' must be at least 5 characters", + ]); await changeValue(getField(getList(), 2), ''); - expect(form.getFieldError(['list', 2])).toEqual(["'list.2' is required"]); + expect(form.current?.getFieldError(['list', 2])).toEqual(["'list.2' is required"]); wrapper.find('button').simulate('click'); wrapper.update(); expect(wrapper.find(Input)).toHaveLength(1); - expect(form.getFieldError(['list', 0])).toEqual(["'list.1' must be at least 5 characters"]); + expect(form.current?.getFieldError(['list', 0])).toEqual([ + "'list.1' must be at least 5 characters", + ]); expect(wrapper.find('input').props().value).toEqual('test'); }); @@ -476,7 +467,7 @@ describe('Form.List', () => { (fields, { add }) => (
    {fields.map(field => ( - + ))} @@ -505,16 +496,18 @@ describe('Form.List', () => { expect(wrapper.find(Input)).toHaveLength(3); await changeValue(getField(getList(), 0), ''); - expect(form.getFieldError(['list', 0])).toEqual(["'list.0' is required"]); + expect(form.current?.getFieldError(['list', 0])).toEqual(["'list.0' is required"]); wrapper.find('.button').simulate('click'); wrapper.find('.button1').simulate('click'); expect(wrapper.find(Input)).toHaveLength(5); - expect(form.getFieldError(['list', 1])).toEqual(["'list.0' is required"]); + expect(form.current?.getFieldError(['list', 1])).toEqual(["'list.0' is required"]); await changeValue(getField(getList(), 1), 'test'); - expect(form.getFieldError(['list', 1])).toEqual(["'list.1' must be at least 5 characters"]); + expect(form.current?.getFieldError(['list', 1])).toEqual([ + "'list.1' must be at least 5 characters", + ]); }); }); @@ -530,14 +523,14 @@ describe('Form.List', () => { // https://github.com/ant-design/ant-design/issues/25584 it('preserve should not break list', async () => { - let operation; + let operation: ListOperations; const [wrapper] = generateForm( (fields, opt) => { operation = opt; return (
    {fields.map(field => ( - + ))} @@ -570,9 +563,9 @@ describe('Form.List', () => { }); it('list support validator', async () => { - let operation; - let currentMeta; - let currentValue; + let operation: ListOperations; + let currentMeta: Meta; + let currentValue: any; const [wrapper] = generateForm( (_, opt, meta) => { @@ -611,7 +604,7 @@ describe('Form.List', () => { (fields, operation) => (
    {fields.map(field => ( - + ))} @@ -650,9 +643,11 @@ describe('Form.List', () => { ); // Not changed - expect(formRef.current.isFieldTouched('user')).toBeFalsy(); - expect(formRef.current.isFieldsTouched(['user'], false)).toBeFalsy(); - expect(formRef.current.isFieldsTouched(['user'], true)).toBeFalsy(); + expect(formRef.current?.isFieldTouched('user')).toBeFalsy(); + expect(formRef.current?.isFieldsTouched(['user'], false)).toBeFalsy(); + expect(formRef.current?.isFieldsTouched(['user'], true)).toBeFalsy(); + + // console.log(wrapper.html()); // Changed wrapper @@ -660,9 +655,14 @@ describe('Form.List', () => { .first() .simulate('change', { target: { value: '' } }); - expect(formRef.current.isFieldTouched('user')).toBeTruthy(); - expect(formRef.current.isFieldsTouched(['user'], false)).toBeTruthy(); - expect(formRef.current.isFieldsTouched(['user'], true)).toBeTruthy(); + // wrapper.update(); + + // console.log(wrapper.html()); + // expect(wrapper.html()).toMatchSnapshot(); + + expect(formRef.current?.isFieldTouched('user')).toBeTruthy(); + expect(formRef.current?.isFieldsTouched(['user'], false)).toBeTruthy(); + expect(formRef.current?.isFieldsTouched(['user'], true)).toBeTruthy(); }); it('List children change', () => { @@ -670,21 +670,19 @@ describe('Form.List', () => { fields => (
    {fields.map(field => ( - + ))}
    ), - { - initialValues: { list: ['light', 'bamboo'] }, - }, + { initialValues: { list: ['light', 'bamboo'] } }, ); // Not changed yet - expect(form.isFieldTouched('list')).toBeFalsy(); - expect(form.isFieldsTouched(['list'], false)).toBeFalsy(); - expect(form.isFieldsTouched(['list'], true)).toBeFalsy(); + expect(form.current?.isFieldTouched('list')).toBeFalsy(); + expect(form.current?.isFieldsTouched(['list'], false)).toBeFalsy(); + expect(form.current?.isFieldsTouched(['list'], true)).toBeFalsy(); // Change children value wrapper @@ -692,16 +690,16 @@ describe('Form.List', () => { .first() .simulate('change', { target: { value: 'little' } }); - expect(form.isFieldTouched('list')).toBeTruthy(); - expect(form.isFieldsTouched(['list'], false)).toBeTruthy(); - expect(form.isFieldsTouched(['list'], true)).toBeTruthy(); + expect(form.current?.isFieldTouched('list')).toBeTruthy(); + expect(form.current?.isFieldsTouched(['list'], false)).toBeTruthy(); + expect(form.current?.isFieldsTouched(['list'], true)).toBeTruthy(); }); it('List self change', () => { const [wrapper] = generateForm((fields, opt) => (
    {fields.map(field => ( - + ))} @@ -715,16 +713,16 @@ describe('Form.List', () => { )); // Not changed yet - expect(form.isFieldTouched('list')).toBeFalsy(); - expect(form.isFieldsTouched(['list'], false)).toBeFalsy(); - expect(form.isFieldsTouched(['list'], true)).toBeFalsy(); + expect(form.current?.isFieldTouched('list')).toBeFalsy(); + expect(form.current?.isFieldsTouched(['list'], false)).toBeFalsy(); + expect(form.current?.isFieldsTouched(['list'], true)).toBeFalsy(); // Change children value wrapper.find('button').simulate('click'); - expect(form.isFieldTouched('list')).toBeTruthy(); - expect(form.isFieldsTouched(['list'], false)).toBeTruthy(); - expect(form.isFieldsTouched(['list'], true)).toBeTruthy(); + expect(form.current?.isFieldTouched('list')).toBeTruthy(); + expect(form.current?.isFieldsTouched(['list'], false)).toBeTruthy(); + expect(form.current?.isFieldsTouched(['list'], true)).toBeTruthy(); }); }); @@ -733,7 +731,7 @@ describe('Form.List', () => { fields => (
    {fields.map(field => ( - + ))} @@ -743,7 +741,7 @@ describe('Form.List', () => { { initialValue: ['light', 'bamboo'] }, ); - expect(form.getFieldsValue()).toEqual({ + expect(form.current?.getFieldsValue()).toEqual({ list: ['light', 'bamboo'], }); }); diff --git a/tests/preserve.test.tsx b/tests/preserve.test.tsx index 042c14dc..6f87e059 100644 --- a/tests/preserve.test.tsx +++ b/tests/preserve.test.tsx @@ -1,23 +1,12 @@ -/* eslint-disable no-template-curly-in-string, arrow-body-style */ -import { mount } from 'enzyme'; import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; import type { FormInstance } from '../src'; import Form from '../src'; import InfoField, { Input } from './common/InfoField'; import timeout from './common/timeout'; describe('Form.Preserve', () => { - const Demo = ({ - removeField, - formPreserve, - fieldPreserve, - onFinish, - }: { - removeField: boolean; - formPreserve?: boolean; - fieldPreserve?: boolean; - onFinish: (values: object) => void; - }) => ( + const Demo: React.FC = ({ removeField, formPreserve, fieldPreserve, onFinish }) => (
    {!removeField && } @@ -26,16 +15,16 @@ describe('Form.Preserve', () => { it('field', async () => { const onFinish = jest.fn(); - const wrapper = mount(); - - async function matchTest(removeField: boolean, match: object) { + const { container, rerender } = render( + , + ); + const matchTest = async (removeField: boolean, match: object) => { onFinish.mockReset(); - wrapper.setProps({ removeField }); - wrapper.find('form').simulate('submit'); + rerender(); + fireEvent.submit(container.querySelector('form')); await timeout(); expect(onFinish).toHaveBeenCalledWith(match); - } - + }; await matchTest(false, { keep: 233, remove: 666 }); await matchTest(true, { keep: 233 }); await matchTest(false, { keep: 233, remove: 666 }); @@ -43,16 +32,16 @@ describe('Form.Preserve', () => { it('form', async () => { const onFinish = jest.fn(); - const wrapper = mount(); - - async function matchTest(removeField: boolean, match: object) { + const { container, rerender } = render( + , + ); + const matchTest = async (removeField: boolean, match: object) => { onFinish.mockReset(); - wrapper.setProps({ removeField }); - wrapper.find('form').simulate('submit'); + rerender(); + fireEvent.submit(container.querySelector('form')); await timeout(); expect(onFinish).toHaveBeenCalledWith(match); - } - + }; await matchTest(false, { keep: 233, remove: 666 }); await matchTest(true, { keep: 233 }); await matchTest(false, { keep: 233, remove: 666 }); @@ -61,119 +50,94 @@ describe('Form.Preserve', () => { it('keep preserve when other field exist the name', async () => { const formRef = React.createRef(); - const KeepDemo = ({ onFinish, keep }: { onFinish: (values: any) => void; keep: boolean }) => { - return ( - - - {() => { - return ( - <> - {keep && } - - - ); - }} - - - ); - }; + const KeepDemo: React.FC = ({ onFinish, keep }) => ( +
    + + {() => ( + <> + {keep && } + + + )} + +
    + ); const onFinish = jest.fn(); - const wrapper = mount(); + const { container, rerender } = render(); // Change value - wrapper - .find('input') - .first() - .simulate('change', { target: { value: 'light' } }); + fireEvent.change(container.querySelector('input'), { + target: { value: 'light' }, + }); - formRef.current.submit(); + formRef.current?.submit(); await timeout(); expect(onFinish).toHaveBeenCalledWith({ test: 'light' }); onFinish.mockReset(); // Remove preserve should not change the value - wrapper.setProps({ keep: false }); + rerender(); await timeout(); - formRef.current.submit(); + formRef.current?.submit(); await timeout(); expect(onFinish).toHaveBeenCalledWith({ test: 'light' }); }); it('form preserve but field !preserve', async () => { const onFinish = jest.fn(); - const wrapper = mount( + const { container, rerender } = render( , ); - - async function matchTest(removeField: boolean, match: object) { + const matchTest = async (removeField: boolean, match: object) => { onFinish.mockReset(); - wrapper.setProps({ removeField }); - wrapper.find('form').simulate('submit'); + rerender( + , + ); + fireEvent.submit(container.querySelector('form')); await timeout(); expect(onFinish).toHaveBeenCalledWith(match); - } - + }; await matchTest(true, { keep: 233 }); await matchTest(false, { keep: 233, remove: 666 }); }); describe('Form.List', () => { it('form preserve should not crash', async () => { - let form: FormInstance; - - const wrapper = mount( -
    { - form = instance; - }} - > + const form = React.createRef(); + + const { container } = render( + {(fields, { remove }) => { return (
    {fields.map(field => ( - + ))} -
    ); }}
    , ); - - wrapper.find('button').simulate('click'); - wrapper.update(); - - expect(form.getFieldsValue()).toEqual({ list: ['bamboo', 'little'] }); + fireEvent.click(container.querySelector('button')); + expect(form.current?.getFieldsValue()).toEqual({ list: ['bamboo', 'little'] }); }); it('warning when Form.List use preserve', () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - let form: FormInstance; - - const wrapper = mount( -
    { - form = instance; - }} - initialValues={{ list: ['bamboo'] }} - > + const form = React.createRef(); + const { container } = render( + {(fields, { remove }) => ( <> {fields.map(field => ( - + ))} @@ -197,106 +161,84 @@ describe('Form.Preserve', () => { errorSpy.mockRestore(); // Remove should not work - wrapper.find('button').simulate('click'); - expect(form.getFieldsValue()).toEqual({ list: [] }); + fireEvent.click(container.querySelector('button')); + expect(form.current?.getFieldsValue()).toEqual({ list: [] }); }); it('multiple level field can use preserve', async () => { - let form: FormInstance; - - const wrapper = mount( - { - form = instance; - }} - > + const form = React.createRef(); + + const { container } = render( + - {(fields, { remove }) => { - return ( - <> - {fields.map(field => ( -
    - - - - - {(_, __, { getFieldValue }) => - getFieldValue(['list', field.name, 'type']) === 'light' ? ( - - - - ) : ( - - - - ) - } - -
    - ))} - - - ); - }} + {(fields, { remove }) => ( + <> + {fields.map(field => ( +
    + + + + + {(_, __, { getFieldValue }) => + getFieldValue(['list', field.name, 'type']) === 'light' ? ( + + + + ) : ( + + + + ) + } + +
    + ))} + + + )}
    , ); // Change light value - wrapper - .find('input') - .last() - .simulate('change', { target: { value: '1128' } }); + fireEvent.change(container.querySelectorAll('input')[1], { + target: { value: '1128' }, + }); // Change type - wrapper - .find('input') - .first() - .simulate('change', { target: { value: 'bamboo' } }); + fireEvent.change(container.querySelectorAll('input')[0], { + target: { value: 'bamboo' }, + }); - // Change bamboo value - wrapper - .find('input') - .last() - .simulate('change', { target: { value: '903' } }); + // Change type + fireEvent.change(container.querySelectorAll('input')[1], { + target: { value: '903' }, + }); - expect(form.getFieldsValue()).toEqual({ list: [{ type: 'bamboo', bamboo: '903' }] }); + expect(form.current?.getFieldsValue()).toEqual({ list: [{ type: 'bamboo', bamboo: '903' }] }); // ============== Remove Test ============== // Remove field - wrapper.find('button').simulate('click'); - expect(form.getFieldsValue()).toEqual({ list: [] }); + fireEvent.click(container.querySelector('button')); + expect(form.current?.getFieldsValue()).toEqual({ list: [] }); }); }); it('nest render props should not clean full store', () => { - let form: FormInstance; - - const wrapper = mount( -
    { - form = instance; - }} - > + const form = React.createRef(); + + const { container, unmount } = render( + @@ -308,79 +250,76 @@ describe('Form.Preserve', () => { , ); - wrapper.find('input').simulate('change', { target: { value: 'bamboo' } }); - expect(form.getFieldsValue()).toEqual({ light: 'bamboo' }); + fireEvent.change(container.querySelector('input'), { + target: { value: 'bamboo' }, + }); + expect(form.current?.getFieldsValue()).toEqual({ light: 'bamboo' }); - wrapper.find('input').simulate('change', { target: { value: 'little' } }); - expect(form.getFieldsValue()).toEqual({ light: 'little' }); + fireEvent.change(container.querySelector('input'), { + target: { value: 'little' }, + }); + expect(form.current?.getFieldsValue()).toEqual({ light: 'little' }); - wrapper.unmount(); + unmount(); }); // https://github.com/ant-design/ant-design/issues/31297 describe('A -> B -> C should keep trigger refresh', () => { it('shouldUpdate', () => { - const DepDemo = () => { + const DepDemo: React.FC = () => { const [form] = Form.useForm(); - return (
    - - {() => { - return form.getFieldValue('name') === '1' ? ( + {() => + form.getFieldValue('name') === '1' ? ( - ) : null; - }} + ) : null + } - - {() => { - const password = form.getFieldValue('password'); - return password ? ( + {() => + form.getFieldValue('password') ? ( - ) : null; - }} + ) : null + }
    ); }; - const wrapper = mount(); + const { container } = render(); // Input name to show password - wrapper - .find('#name') - .last() - .simulate('change', { target: { value: '1' } }); - expect(wrapper.exists('#password')).toBeTruthy(); - expect(wrapper.exists('#password2')).toBeFalsy(); + fireEvent.change(container.querySelector('#name'), { + target: { value: '1' }, + }); + expect(container.querySelector('#password')).toBeTruthy(); + expect(container.querySelector('#password2')).toBeFalsy(); // Input password to show password2 - wrapper - .find('#password') - .last() - .simulate('change', { target: { value: '1' } }); - expect(wrapper.exists('#password2')).toBeTruthy(); + fireEvent.change(container.querySelector('#password'), { + target: { value: '1' }, + }); + expect(container.querySelector('#password2')).toBeTruthy(); // Change name to hide password - wrapper - .find('#name') - .last() - .simulate('change', { target: { value: '2' } }); - expect(wrapper.exists('#password')).toBeFalsy(); - expect(wrapper.exists('#password2')).toBeFalsy(); + fireEvent.change(container.querySelector('#name'), { + target: { value: '2' }, + }); + expect(container.querySelector('#password')).toBeFalsy(); + expect(container.querySelector('#password2')).toBeFalsy(); }); it('dependencies', () => { - const DepDemo = () => { + const DepDemo: React.FC = () => { const [form] = Form.useForm(); return ( @@ -413,37 +352,34 @@ describe('Form.Preserve', () => { ); }; - const wrapper = mount(); + const { container } = render(); // Input name to show password - wrapper - .find('#name') - .last() - .simulate('change', { target: { value: '1' } }); - expect(wrapper.exists('#password')).toBeTruthy(); - expect(wrapper.exists('#password2')).toBeFalsy(); + fireEvent.change(container.querySelector('#name'), { + target: { value: '1' }, + }); + expect(container.querySelector('#password')).toBeTruthy(); + expect(container.querySelector('#password2')).toBeFalsy(); // Input password to show password2 - wrapper - .find('#password') - .last() - .simulate('change', { target: { value: '1' } }); - expect(wrapper.exists('#password2')).toBeTruthy(); + fireEvent.change(container.querySelector('#password'), { + target: { value: '1' }, + }); + expect(container.querySelector('#password2')).toBeTruthy(); // Change name to hide password - wrapper - .find('#name') - .last() - .simulate('change', { target: { value: '2' } }); - expect(wrapper.exists('#password')).toBeFalsy(); - expect(wrapper.exists('#password2')).toBeFalsy(); + fireEvent.change(container.querySelector('#name'), { + target: { value: '2' }, + }); + expect(container.querySelector('#password')).toBeFalsy(); + expect(container.querySelector('#password2')).toBeFalsy(); }); }); it('should correct calculate preserve state', () => { let instance: FormInstance; - const VisibleDemo = ({ visible = true }: { visible?: boolean }) => { + const VisibleDemo: React.FC<{ visible?: boolean }> = ({ visible = true }) => { const [form] = Form.useForm(); instance = form; @@ -458,18 +394,13 @@ describe('Form.Preserve', () => { ); }; - const wrapper = mount(); + const { container, rerender } = render(); - wrapper.setProps({ - visible: false, - }); + rerender(); instance.setFieldsValue({ name: 'bamboo' }); - wrapper.setProps({ - visible: true, - }); + rerender(); - expect(wrapper.find('input').prop('value')).toEqual('bamboo'); + expect(container.querySelector('input')?.value).toEqual('bamboo'); }); }); -/* eslint-enable no-template-curly-in-string */ diff --git a/tests/useWatch.test.tsx b/tests/useWatch.test.tsx index 83fce087..7f4fc8c2 100644 --- a/tests/useWatch.test.tsx +++ b/tests/useWatch.test.tsx @@ -1,5 +1,5 @@ import React, { useRef, useState } from 'react'; -import { mount } from 'enzyme'; +import { render, fireEvent } from '@testing-library/react'; import type { FormInstance } from '../src'; import { List } from '../src'; import Form, { Field } from '../src'; @@ -9,13 +9,10 @@ import { Input } from './common/InfoField'; import { stringify } from '../src/useWatch'; describe('useWatch', () => { - let staticForm: FormInstance; - it('field initialValue', async () => { - const Demo = () => { + const Demo: React.FC = () => { const [form] = Form.useForm(); const nameValue = Form.useWatch('name', form); - return (
    @@ -28,17 +25,16 @@ describe('useWatch', () => { ); }; await act(async () => { - const wrapper = mount(); + const { container } = render(); await timeout(); - expect(wrapper.find('.values').text()).toEqual('bamboo'); + expect(container.querySelector('.values')?.textContent).toEqual('bamboo'); }); }); it('form initialValue', async () => { - const Demo = () => { + const Demo: React.FC = () => { const [form] = Form.useForm(); const nameValue = Form.useWatch(['name'], form); - return (
    @@ -51,25 +47,20 @@ describe('useWatch', () => { ); }; await act(async () => { - const wrapper = mount(); + const { container } = render(); await timeout(); - expect(wrapper.find('.values').text()).toEqual('bamboo'); + expect(container.querySelector('.values')?.textContent).toEqual('bamboo'); }); }); it('change value with form api', async () => { - const Demo = () => { + const staticForm = React.createRef(); + const Demo: React.FC = () => { const [form] = Form.useForm(); const nameValue = Form.useWatch(['name'], form); - return (
    - { - staticForm = instance; - }} - > + @@ -79,25 +70,24 @@ describe('useWatch', () => { ); }; await act(async () => { - const wrapper = mount(); + const { container } = render(); await timeout(); - staticForm.setFields([{ name: 'name', value: 'little' }]); - expect(wrapper.find('.values').text()).toEqual('little'); + staticForm.current?.setFields([{ name: 'name', value: 'little' }]); + expect(container.querySelector('.values').textContent)?.toEqual('little'); - staticForm.setFieldsValue({ name: 'light' }); - expect(wrapper.find('.values').text()).toEqual('light'); + staticForm.current?.setFieldsValue({ name: 'light' }); + expect(container.querySelector('.values').textContent)?.toEqual('light'); - staticForm.resetFields(); - expect(wrapper.find('.values').text()).toEqual(''); + staticForm.current?.resetFields(); + expect(container.querySelector('.values').textContent)?.toEqual(''); }); }); describe('unmount', () => { it('basic', async () => { - const Demo = ({ visible }: { visible: boolean }) => { + const Demo: React.FC<{ visible?: boolean }> = ({ visible }) => { const [form] = Form.useForm(); const nameValue = Form.useWatch(['name'], form); - return (
    @@ -113,23 +103,22 @@ describe('useWatch', () => { }; await act(async () => { - const wrapper = mount(); + const { container, rerender } = render(); await timeout(); - expect(wrapper.find('.values').text()).toEqual('bamboo'); + expect(container.querySelector('.values')?.textContent).toEqual('bamboo'); - wrapper.setProps({ visible: false }); - expect(wrapper.find('.values').text()).toEqual(''); + rerender(); + expect(container.querySelector('.values')?.textContent).toEqual(''); - wrapper.setProps({ visible: true }); - expect(wrapper.find('.values').text()).toEqual('bamboo'); + rerender(); + expect(container.querySelector('.values')?.textContent).toEqual('bamboo'); }); }); it('nest children component', async () => { - const DemoWatch = () => { + const DemoWatch: React.FC = () => { Form.useWatch(['name']); - return ( @@ -137,7 +126,7 @@ describe('useWatch', () => { ); }; - const Demo = ({ visible }: { visible: boolean }) => { + const Demo: React.FC<{ visible?: boolean }> = ({ visible }) => { const [form] = Form.useForm(); const nameValue = Form.useWatch(['name'], form); @@ -152,25 +141,23 @@ describe('useWatch', () => { }; await act(async () => { - const wrapper = mount(); + const { container, rerender } = render(); await timeout(); + expect(container.querySelector('.values')?.textContent).toEqual('bamboo'); - expect(wrapper.find('.values').text()).toEqual('bamboo'); + rerender(); + expect(container.querySelector('.values')?.textContent).toEqual(''); - wrapper.setProps({ visible: false }); - expect(wrapper.find('.values').text()).toEqual(''); - - wrapper.setProps({ visible: true }); - expect(wrapper.find('.values').text()).toEqual('bamboo'); + rerender(); + expect(container.querySelector('.values')?.textContent).toEqual('bamboo'); }); }); }); it('list', async () => { - const Demo = () => { + const Demo: React.FC = () => { const [form] = Form.useForm(); const users = Form.useWatch(['users'], form) || []; - return (
    {JSON.stringify(users)}
    @@ -196,25 +183,28 @@ describe('useWatch', () => { ); }; await act(async () => { - const wrapper = mount(); + const { container } = render(); await timeout(); - expect(wrapper.find('.values').text()).toEqual(JSON.stringify(['bamboo', 'light'])); - - wrapper.find('.remove').at(0).simulate('click'); + expect(container.querySelector('.values')?.textContent).toEqual( + JSON.stringify(['bamboo', 'light']), + ); + fireEvent.click(container.querySelector('.remove')); await timeout(); - expect(wrapper.find('.values').text()).toEqual(JSON.stringify(['light'])); + expect(container.querySelector('.values')?.textContent).toEqual( + JSON.stringify(['light']), + ); }); }); it('warning if not provide form', () => { - const errorSpy = jest.spyOn(console, 'error'); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const Demo = () => { + const Demo: React.FC = () => { Form.useWatch([]); return null; }; - mount(); + render(); expect(errorSpy).toHaveBeenCalledWith( 'Warning: useWatch requires a form instance since it can not auto detect from context.', @@ -225,7 +215,7 @@ describe('useWatch', () => { it('no more render time', () => { let renderTime = 0; - const Demo = () => { + const Demo: React.FC = () => { const [form] = Form.useForm(); const name = Form.useWatch('name', form); @@ -244,37 +234,18 @@ describe('useWatch', () => { ); }; - const wrapper = mount(); + const { container } = render(); expect(renderTime).toEqual(1); - wrapper - .find('input') - .first() - .simulate('change', { - target: { - value: 'bamboo', - }, - }); + const input = container.querySelectorAll('input'); + + fireEvent.change(input[0], { target: { value: 'bamboo' } }); expect(renderTime).toEqual(2); - wrapper - .find('input') - .last() - .simulate('change', { - target: { - value: '123', - }, - }); + fireEvent.change(input[1], { target: { value: '123' } }); expect(renderTime).toEqual(2); - wrapper - .find('input') - .last() - .simulate('change', { - target: { - value: '123456', - }, - }); + fireEvent.change(input[1], { target: { value: '123456' } }); expect(renderTime).toEqual(2); }); @@ -290,7 +261,7 @@ describe('useWatch', () => { demo1?: { demo2?: { demo3?: { demo4?: string } } }; }; - const Demo = () => { + const Demo: React.FC = () => { const [form] = Form.useForm(); const values = Form.useWatch([], form); const main = Form.useWatch('main', form); @@ -302,13 +273,12 @@ describe('useWatch', () => { const demo5 = Form.useWatch(['demo1', 'demo2', 'demo3', 'demo4', 'demo5'], form); const more = Form.useWatch(['age', 'name', 'gender'], form); const demo = Form.useWatch(['demo']); - return ( <>{JSON.stringify({ values, main, age, demo1, demo2, demo3, demo4, demo5, more, demo })} ); }; - mount(); + render(); }); // https://github.com/react-component/field-form/issues/431 @@ -316,18 +286,16 @@ describe('useWatch', () => { let updateA = 0; let updateB = 0; - const Demo = () => { + const Demo: React.FC = () => { const [form] = Form.useForm(); const userA = Form.useWatch(['a'], form); const userB = Form.useWatch(['b'], form); React.useEffect(() => { updateA += 1; - console.log('Update A', userA); }, [userA]); React.useEffect(() => { updateB += 1; - console.log('Update B', userB); }, [userB]); return ( @@ -342,19 +310,15 @@ describe('useWatch', () => { ); }; - const wrapper = mount(); - - console.log('Change!'); - wrapper - .find('input') - .first() - .simulate('change', { target: { value: 'bamboo' } }); - + const { container } = render(); + fireEvent.change(container.querySelector('input'), { + target: { value: 'bamboo' }, + }); expect(updateA > updateB).toBeTruthy(); }); it('mount while unmount', () => { - const Demo = () => { + const Demo: React.FC = () => { const [form] = Form.useForm(); const [type, setType] = useState(true); const name = Form.useWatch('name', form); @@ -378,14 +342,12 @@ describe('useWatch', () => { ); }; - - const wrapper = mount(); - wrapper - .find('input') - .first() - .simulate('change', { target: { value: 'bamboo' } }); - wrapper.find('button').at(0).simulate('click'); - expect(wrapper.find('.value').text()).toEqual('bamboo'); + const { container } = render(); + fireEvent.change(container.querySelector('input'), { + target: { value: 'bamboo' }, + }); + container.querySelector('button').click(); + expect(container.querySelector('.value')?.textContent).toEqual('bamboo'); }); it('stringify error', () => { const obj: any = {}; @@ -394,12 +356,11 @@ describe('useWatch', () => { expect(typeof str === 'number').toBeTruthy(); }); it('first undefined', () => { - const errorSpy = jest.spyOn(console, 'error'); - const Demo = () => { - const formRef = useRef(); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const Demo: React.FC = () => { + const formRef = useRef(); const name = Form.useWatch('name', formRef.current); const [, setUpdate] = useState({}); - return ( <>
    setUpdate({})} /> @@ -412,16 +373,14 @@ describe('useWatch', () => { ); }; - - const wrapper = mount(); - expect(wrapper.find('.value').text()).toEqual(''); - wrapper.find('.setUpdate').at(0).simulate('click'); - expect(wrapper.find('.value').text()).toEqual('default'); - wrapper - .find('input') - .at(0) - .simulate('change', { target: { value: 'bamboo' } }); - expect(wrapper.find('.value').text()).toEqual('bamboo'); + const { container } = render(); + expect(container.querySelector('.value')?.textContent).toEqual(''); + fireEvent.click(container.querySelector('.setUpdate')); + expect(container.querySelector('.value')?.textContent).toEqual('default'); + fireEvent.change(container.querySelector('input'), { + target: { value: 'bamboo' }, + }); + expect(container.querySelector('.value')?.textContent).toEqual('bamboo'); expect(errorSpy).not.toHaveBeenCalledWith( 'Warning: useWatch requires a form instance since it can not auto detect from context.', ); diff --git a/tests/validate-warning.test.tsx b/tests/validate-warning.test.tsx index 447153c3..31961acc 100644 --- a/tests/validate-warning.test.tsx +++ b/tests/validate-warning.test.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-template-curly-in-string */ import React from 'react'; import { mount } from 'enzyme'; import Form from '../src'; @@ -8,86 +7,51 @@ import type { FormInstance, Rule } from '../src/interface'; describe('Form.WarningValidate', () => { it('required', async () => { - let form: FormInstance; - + const form = React.createRef(); const wrapper = mount( -
    { - form = f; - }} - > - + + , ); - await changeValue(wrapper, ''); matchError(wrapper, false, "'name' is required"); - expect(form.getFieldWarning('name')).toEqual(["'name' is required"]); + expect(form.current?.getFieldWarning('name')).toEqual(["'name' is required"]); }); describe('validateFirst should not block error', () => { - function testValidateFirst( + const testValidateFirst = ( name: string, validateFirst: boolean | 'parallel', additionalRule?: Rule, errorMessage?: string, - ) { + ) => { it(name, async () => { - const rules = [ + const rules: Rule[] = [ additionalRule, - { - type: 'string', - len: 10, - warningOnly: true, - }, - { - type: 'url', - }, - { - type: 'string', - len: 20, - warningOnly: true, - }, + { type: 'string', len: 10, warningOnly: true }, + { type: 'url' }, + { type: 'string', len: 20, warningOnly: true }, ]; - const wrapper = mount(
    - r) as any} - > + , ); - await changeValue(wrapper, 'bamboo'); matchError(wrapper, errorMessage || "'name' is not a valid url", false); }); - } - + }; testValidateFirst('default', true); testValidateFirst( 'default', true, - { - type: 'string', - len: 3, - }, + { type: 'string', len: 3 }, "'name' must be exactly 3 characters", ); testValidateFirst('parallel', 'parallel'); }); }); -/* eslint-enable no-template-curly-in-string */ diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index 8c095af7..dfe7f8df 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-template-curly-in-string */ import React from 'react'; import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; @@ -6,6 +5,7 @@ import Form, { Field, useForm } from '../src'; import InfoField, { Input } from './common/InfoField'; import { changeValue, matchError, getField } from './common'; import timeout from './common/timeout'; +import type { ValidateMessages } from '@/interface'; describe('Form.Validate', () => { it('required', async () => { @@ -49,7 +49,7 @@ describe('Form.Validate', () => { }); describe('validateMessages', () => { - function renderForm(messages, fieldProps = {}) { + function renderForm(messages: ValidateMessages, fieldProps = {}) { return mount(
    @@ -751,4 +751,3 @@ describe('Form.Validate', () => { matchError(wrapper, true); }); }); -/* eslint-enable no-template-curly-in-string */ From 8d2874c0f0f39b4b8be979249edaff548812360d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 8 Feb 2023 17:12:12 +0800 Subject: [PATCH 08/65] chore: warning for dynamic watch namePath. close ant-design/ant-design#40605 --- src/useWatch.ts | 17 ++++++++++++++++- tests/useWatch.test.tsx | 21 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/useWatch.ts b/src/useWatch.ts index 2eec7df2..13977820 100644 --- a/src/useWatch.ts +++ b/src/useWatch.ts @@ -2,7 +2,7 @@ import type { FormInstance } from '.'; import { FieldContext } from '.'; import warning from 'rc-util/lib/warning'; import { HOOK_MARK } from './FieldContext'; -import type { InternalFormInstance, NamePath, Store } from './interface'; +import type { InternalFormInstance, InternalNamePath, NamePath, Store } from './interface'; import { useState, useContext, useEffect, useRef, useMemo } from 'react'; import { getNamePath, getValue } from './utils/valueUtil'; @@ -17,6 +17,19 @@ export function stringify(value: any) { } } +const useWatchWarning = + process.env.NODE_ENV !== 'production' + ? (namePath: InternalNamePath) => { + const fullyStr = namePath.join('__RC_FIELD_FORM_SPLIT__'); + const nameStrRef = useRef(fullyStr); + + warning( + nameStrRef.current === fullyStr, + '`useWatch` is not support dynamic `namePath`. Please provide static instead.', + ); + } + : () => {}; + function useWatch< TDependencies1 extends keyof GetGeneric, TForm extends FormInstance, @@ -82,6 +95,8 @@ function useWatch(...args: [NamePath, FormInstance]) { const namePathRef = useRef(namePath); namePathRef.current = namePath; + useWatchWarning(namePath); + useEffect( () => { // Skip if not exist form instance diff --git a/tests/useWatch.test.tsx b/tests/useWatch.test.tsx index 7f4fc8c2..a53f6a00 100644 --- a/tests/useWatch.test.tsx +++ b/tests/useWatch.test.tsx @@ -386,4 +386,25 @@ describe('useWatch', () => { ); errorSpy.mockRestore(); }); + + it('dynamic change warning', () => { + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const Demo: React.FC = () => { + const [form] = Form.useForm(); + const [watchPath, setWatchPath] = React.useState('light'); + Form.useWatch(watchPath, form); + + React.useEffect(() => { + setWatchPath('bamboo'); + }, []); + + return ; + }; + render(); + + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: `useWatch` is not support dynamic `namePath`. Please provide static instead.', + ); + errorSpy.mockRestore(); + }); }); From 7bd75b50a95716e97a3494cd097b35e9b9208a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 8 Feb 2023 17:14:53 +0800 Subject: [PATCH 09/65] 1.27.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 52b16aee..8729c7bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.27.3", + "version": "1.27.4", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From 515d7cff3c5d0ce26779721a9ece25a8b00f7d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kiner-tang=28=E6=96=87=E8=BE=89=29?= <1127031143@qq.com> Date: Thu, 9 Mar 2023 13:59:00 +0800 Subject: [PATCH 10/65] feat: support validated status when trigger validate (#579) * feat: support validated status when trigger validate * feat: support validated status when trigger validate * feat: update test case * feat: update test case --- src/Field.tsx | 3 ++- src/interface.ts | 1 + tests/context.test.tsx | 20 ++++++++++++++++++ tests/index.test.tsx | 9 +++++++- tests/validate.test.tsx | 46 ++++++++++++++++++++++++++++++++++++++++- 5 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/Field.tsx b/src/Field.tsx index 9b229ba0..60d72a2c 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -133,7 +133,7 @@ class Field extends React.Component implements F */ private dirty: boolean = false; - private validatePromise: Promise | null = null; + private validatePromise: Promise | null; private prevValidating: boolean; @@ -475,6 +475,7 @@ class Field extends React.Component implements F errors: this.errors, warnings: this.warnings, name: this.getNamePath(), + validated: this.validatePromise === null, }; return meta; diff --git a/src/interface.ts b/src/interface.ts index c01f0839..b1b46d37 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -13,6 +13,7 @@ export interface Meta { errors: string[]; warnings: string[]; name: InternalNamePath; + validated: boolean; } export interface InternalFieldData extends Meta { diff --git a/tests/context.test.tsx b/tests/context.test.tsx index b1ceefa5..6481891b 100644 --- a/tests/context.test.tsx +++ b/tests/context.test.tsx @@ -44,6 +44,26 @@ describe('Form.Context', () => { touched: true, validating: false, value: 'Light', + validated: false, + }, + ], + forms: { + form1: expect.objectContaining({}), + }, + }), + ); + expect(onFormChange).toHaveBeenCalledWith( + 'form1', + expect.objectContaining({ + changedFields: [ + { + errors: [], + warnings: [], + name: ['username'], + touched: true, + validating: false, + value: 'Light', + validated: true, }, ], forms: { diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 74c5fd78..a05ad09b 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -655,7 +655,14 @@ describe('Form.Basic', () => { expect( form.current?.getFieldsValue(null, meta => { - expect(Object.keys(meta)).toEqual(['touched', 'validating', 'errors', 'warnings', 'name']); + expect(Object.keys(meta)).toEqual([ + 'touched', + 'validating', + 'errors', + 'warnings', + 'name', + 'validated', + ]); return false; }), ).toEqual({}); diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index dfe7f8df..5061b5c6 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import Form, { Field, useForm } from '../src'; @@ -750,4 +750,48 @@ describe('Form.Validate', () => { await changeValue(wrapper, ''); matchError(wrapper, true); }); + it('validated status should be true when trigger validate', async () => { + const validateTrigger = jest.fn(); + const validateNoTrigger = jest.fn(); + const App = ({ trigger = true }) => { + const ref = React.useRef(null); + useEffect(() => { + if (!trigger) return; + ref.current!.validateFields(); + }, [trigger]); + return ( +
    + + { + if (trigger) { + validateTrigger(meta.validated); + } else { + validateNoTrigger(meta.validated); + } + }} + rules={[ + { + type: 'email', + message: 'Please input your e-mail', + }, + { + required: true, + message: 'Please input your value', + }, + ]} + /> + +
    + ); + }; + const wrapper = mount(); + await timeout(); + expect(validateNoTrigger).not.toHaveBeenCalled(); + wrapper.setProps({ trigger: true }); + await timeout(); + expect(validateTrigger).toBeCalledWith(true); + }); }); From f2d8adec19338209f992081ee6611503e87c5117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 9 Mar 2023 14:01:51 +0800 Subject: [PATCH 11/65] 1.28.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8729c7bc..1636a618 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.27.4", + "version": "1.28.0", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From 680c53416af1881df7a2964fd6b6aa42cddb0d07 Mon Sep 17 00:00:00 2001 From: Rex Zeng Date: Mon, 13 Mar 2023 11:52:23 +0800 Subject: [PATCH 12/65] feat(useWatch): notify watch in preserve mode (#577) --- docs/examples/useWatch.tsx | 7 ++++-- src/interface.ts | 11 ++++++++- src/useForm.ts | 5 ++-- src/useWatch.ts | 48 +++++++++++++++++++++++++++----------- src/utils/typeUtil.ts | 6 +++++ tests/useWatch.test.tsx | 30 ++++++++++++++++++++++++ 6 files changed, 89 insertions(+), 18 deletions(-) diff --git a/docs/examples/useWatch.tsx b/docs/examples/useWatch.tsx index 3a3d3a88..1475b789 100644 --- a/docs/examples/useWatch.tsx +++ b/docs/examples/useWatch.tsx @@ -13,6 +13,7 @@ type FieldType = { demo2?: string; id?: number; demo1?: { demo2?: { demo3?: { demo4?: string } } }; + hidden?: string; }; const Demo = React.memo(() => { @@ -48,12 +49,13 @@ export default () => { const demo4 = Form.useWatch(['demo1', 'demo2', 'demo3', 'demo4'], form); const demo5 = Form.useWatch(['demo1', 'demo2', 'demo3', 'demo4', 'demo5'], form); const more = Form.useWatch(['age', 'name', 'gender'], form); - console.log('main watch', values, demo1, demo2, main, age, demo3, demo4, demo5, more); + const hidden = Form.useWatch(['hidden'], { form, preserve: true }); + console.log('main watch', values, demo1, demo2, main, age, demo3, demo4, demo5, more, hidden); return ( <>
    console.log('submit values', v)} > no render @@ -115,6 +117,7 @@ export default () => { + ); }; diff --git a/src/interface.ts b/src/interface.ts index b1b46d37..3413d602 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -194,7 +194,16 @@ export interface Callbacks { onFinishFailed?: (errorInfo: ValidateErrorEntity) => void; } -export type WatchCallBack = (values: Store, namePathList: InternalNamePath[]) => void; +export type WatchCallBack = ( + values: Store, + allValues: Store, + namePathList: InternalNamePath[], +) => void; + +export interface WatchOptions { + form?: Form; + preserve?: boolean; +} export interface InternalHooks { dispatch: (action: ReducerAction) => void; diff --git a/src/useForm.ts b/src/useForm.ts index 1adf9967..b4d0103f 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -200,9 +200,10 @@ export class FormStore { // No need to cost perf when nothing need to watch if (this.watchList.length) { const values = this.getFieldsValue(); + const allValues = this.getFieldsValue(true); this.watchList.forEach(callback => { - callback(values, namePath); + callback(values, allValues, namePath); }); } }; @@ -548,7 +549,7 @@ export class FormStore { const namePathList: InternalNamePath[] = []; fields.forEach((fieldData: FieldData) => { - const { name, errors, ...data } = fieldData; + const { name, ...data } = fieldData; const namePath = getNamePath(name); namePathList.push(namePath); diff --git a/src/useWatch.ts b/src/useWatch.ts index 13977820..afaa9846 100644 --- a/src/useWatch.ts +++ b/src/useWatch.ts @@ -2,9 +2,16 @@ import type { FormInstance } from '.'; import { FieldContext } from '.'; import warning from 'rc-util/lib/warning'; import { HOOK_MARK } from './FieldContext'; -import type { InternalFormInstance, InternalNamePath, NamePath, Store } from './interface'; +import type { + InternalFormInstance, + InternalNamePath, + NamePath, + Store, + WatchOptions, +} from './interface'; import { useState, useContext, useEffect, useRef, useMemo } from 'react'; import { getNamePath, getValue } from './utils/valueUtil'; +import { isFormInstance } from './utils/typeUtil'; type ReturnPromise = T extends Promise ? ValueType : never; type GetGeneric = ReturnPromise>; @@ -38,7 +45,7 @@ function useWatch< TDependencies4 extends keyof GetGeneric[TDependencies1][TDependencies2][TDependencies3], >( dependencies: [TDependencies1, TDependencies2, TDependencies3, TDependencies4], - form?: TForm, + form?: TForm | WatchOptions, ): GetGeneric[TDependencies1][TDependencies2][TDependencies3][TDependencies4]; function useWatch< @@ -48,7 +55,7 @@ function useWatch< TDependencies3 extends keyof GetGeneric[TDependencies1][TDependencies2], >( dependencies: [TDependencies1, TDependencies2, TDependencies3], - form?: TForm, + form?: TForm | WatchOptions, ): GetGeneric[TDependencies1][TDependencies2][TDependencies3]; function useWatch< @@ -57,22 +64,34 @@ function useWatch< TDependencies2 extends keyof GetGeneric[TDependencies1], >( dependencies: [TDependencies1, TDependencies2], - form?: TForm, + form?: TForm | WatchOptions, ): GetGeneric[TDependencies1][TDependencies2]; function useWatch, TForm extends FormInstance>( dependencies: TDependencies | [TDependencies], - form?: TForm, + form?: TForm | WatchOptions, ): GetGeneric[TDependencies]; -function useWatch(dependencies: [], form?: TForm): GetGeneric; +function useWatch( + dependencies: [], + form?: TForm | WatchOptions, +): GetGeneric; -function useWatch(dependencies: NamePath, form?: TForm): any; +function useWatch( + dependencies: NamePath, + form?: TForm | WatchOptions, +): any; -function useWatch(dependencies: NamePath, form?: FormInstance): ValueType; +function useWatch( + dependencies: NamePath, + form?: FormInstance | WatchOptions, +): ValueType; + +function useWatch(...args: [NamePath, FormInstance | WatchOptions]) { + const [dependencies = [], _form = {}] = args; + const options = isFormInstance(_form) ? { form: _form } : _form; + const form = options.form; -function useWatch(...args: [NamePath, FormInstance]) { - const [dependencies = [], form] = args; const [value, setValue] = useState(); const valueStr = useMemo(() => stringify(value), [value]); @@ -107,8 +126,8 @@ function useWatch(...args: [NamePath, FormInstance]) { const { getFieldsValue, getInternalHooks } = formInstance; const { registerWatch } = getInternalHooks(HOOK_MARK); - const cancelRegister = registerWatch(store => { - const newValue = getValue(store, namePathRef.current); + const cancelRegister = registerWatch((values, allValues) => { + const newValue = getValue(options.preserve ? allValues : values, namePathRef.current); const nextValueStr = stringify(newValue); // Compare stringify in case it's nest object @@ -119,7 +138,10 @@ function useWatch(...args: [NamePath, FormInstance]) { }); // TODO: We can improve this perf in future - const initialValue = getValue(getFieldsValue(), namePathRef.current); + const initialValue = getValue( + options.preserve ? getFieldsValue(true) : getFieldsValue(), + namePathRef.current, + ); setValue(initialValue); return cancelRegister; diff --git a/src/utils/typeUtil.ts b/src/utils/typeUtil.ts index bb319571..49128367 100644 --- a/src/utils/typeUtil.ts +++ b/src/utils/typeUtil.ts @@ -1,3 +1,5 @@ +import type { FormInstance, InternalFormInstance } from '../interface'; + export function toArray(value?: T | T[] | null): T[] { if (value === undefined || value === null) { return []; @@ -5,3 +7,7 @@ export function toArray(value?: T | T[] | null): T[] { return Array.isArray(value) ? value : [value]; } + +export function isFormInstance(form: T | FormInstance): form is FormInstance { + return form && !!(form as InternalFormInstance)._init; +} diff --git a/tests/useWatch.test.tsx b/tests/useWatch.test.tsx index a53f6a00..6e22819b 100644 --- a/tests/useWatch.test.tsx +++ b/tests/useWatch.test.tsx @@ -407,4 +407,34 @@ describe('useWatch', () => { ); errorSpy.mockRestore(); }); + + it('useWatch with preserve option', async () => { + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + const Demo: React.FC = () => { + const [form] = Form.useForm(); + const nameValuePreserve = Form.useWatch('name', { + form, + preserve: true, + }); + const nameValue = Form.useWatch('name', form); + React.useEffect(() => { + console.log(nameValuePreserve, nameValue); + }, [nameValuePreserve, nameValue]); + return ( +
    + +
    {nameValuePreserve}
    +
    + ); + }; + await act(async () => { + const { container } = render(); + await timeout(); + expect(logSpy).toHaveBeenCalledWith('bamboo', undefined); // initialValue + fireEvent.click(container.querySelector('.test-btn')); + await timeout(); + expect(logSpy).toHaveBeenCalledWith('light', undefined); // after setFieldValue + }); + }); }); From c9490df783e14a35cc6a9e4375c680725885b77a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 13 Mar 2023 11:55:35 +0800 Subject: [PATCH 13/65] 1.29.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1636a618..9c7493f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.28.0", + "version": "1.29.0", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From 691099ca21c782ea78a1e197dd42b6ad9eb7dac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Tue, 14 Mar 2023 16:09:09 +0800 Subject: [PATCH 14/65] fix: onFieldsChange not trigger when validating (#580) * fix: missing one fields change * test: add test case --- src/useForm.ts | 3 +++ tests/validate.test.tsx | 54 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/useForm.ts b/src/useForm.ts index b4d0103f..1d497e85 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -954,6 +954,9 @@ export class FormStore { // Do not throw in console returnPromise.catch(e => e); + // `validating` changed. Trigger `onFieldsChange` + this.triggerOnFieldsChange(namePathList); + return returnPromise as Promise; }; diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index 5061b5c6..f97f5be1 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -794,4 +794,58 @@ describe('Form.Validate', () => { await timeout(); expect(validateTrigger).toBeCalledWith(true); }); + + it('should trigger onFieldsChange 3 times', async () => { + const onFieldsChange = jest.fn(); + + const wrapper = mount( + + + + + , + ); + + await changeValue(getField(wrapper, 'test'), ''); + + await timeout(); + + // `validated: false` -> `validated: false` -> `validated: true` + // `validating: false` -> `validating: true` -> `validating: false` + expect(onFieldsChange).toHaveBeenCalledTimes(3); + + expect(onFieldsChange).toHaveBeenNthCalledWith( + 1, + [ + expect.objectContaining({ + name: ['test'], + validated: false, + validating: false, + }), + ], + expect.anything(), + ); + expect(onFieldsChange).toHaveBeenNthCalledWith( + 2, + [ + expect.objectContaining({ + name: ['test'], + validated: false, + validating: true, + }), + ], + expect.anything(), + ); + expect(onFieldsChange).toHaveBeenNthCalledWith( + 3, + [ + expect.objectContaining({ + name: ['test'], + validated: true, + validating: false, + }), + ], + expect.anything(), + ); + }); }); From 5969e7fa94d3c930408342d5d4d8ad9c2cfaa18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 14 Mar 2023 16:16:14 +0800 Subject: [PATCH 15/65] 1.29.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9c7493f3..3885e60e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.29.0", + "version": "1.29.1", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From cbc2bc676c84903d09296657bbd563a7705bfc3d Mon Sep 17 00:00:00 2001 From: Wxh16144 Date: Mon, 27 Mar 2023 17:34:12 +0800 Subject: [PATCH 16/65] chore: disable validate waring log (#563) * chore: Improve DX revert: https://github.com/react-component/field-form/commit/ec3a5bc4611ffe14a90c10a78dbe065af93a0cf2#diff-1f7e42a89a341b70a9a47c7cab68b92d5cbf8448be6179a7ae5f1887da340bdeL28-L93 close: https://github.com/ant-design/ant-design/issues/40497 * test: add case * chore: update * chore: remove redundant logic * Revert "chore: remove redundant logic" This reverts commit 32b061d43fed49da7f9f52dea33d8c37ca06f989. * Revert "chore: update" This reverts commit d7b7ffa6dad0bbce9d51fc15746f7c2c766cb208. * Revert "test: add case" This reverts commit 4b69a0b6c09f3e3de56f34835a6ee617cb6a5878. * Revert "chore: Improve DX" This reverts commit 104043c0be01210ee50389dbab066e1436d0ee4b. * chore: update logic * Revert "Revert "chore: remove redundant logic"" This reverts commit 9f2a1f249222e6706e771a24295f8f8cfc292860. * chore: update --- src/utils/validateUtil.ts | 3 +++ src/utils/valueUtil.ts | 21 ++++----------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/utils/validateUtil.ts b/src/utils/validateUtil.ts index 36071eaa..7270ac1e 100644 --- a/src/utils/validateUtil.ts +++ b/src/utils/validateUtil.ts @@ -41,6 +41,9 @@ async function validateRule( // https://github.com/react-component/field-form/issues/313 delete (cloneRule as any).ruleIndex; + // https://github.com/ant-design/ant-design/issues/40497#issuecomment-1422282378 + AsyncValidator.warning = () => void 0; + if (cloneRule.validator) { const originValidator = cloneRule.validator; cloneRule.validator = (...args) => { diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 0e5f073e..4dbcf94b 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -1,9 +1,11 @@ -import get from 'rc-util/lib/utils/get'; -import set from 'rc-util/lib/utils/set'; +import getValue from 'rc-util/lib/utils/get'; +import setValue from 'rc-util/lib/utils/set'; import type { InternalNamePath, NamePath, Store, StoreValue, EventArgs } from '../interface'; import { toArray } from './typeUtil'; import cloneDeep from '../utils/cloneDeep'; +export { getValue, setValue }; + /** * Convert name to internal supported format. * This function should keep since we still thinking if need support like `a.b.c` format. @@ -15,21 +17,6 @@ export function getNamePath(path: NamePath | null): InternalNamePath { return toArray(path); } -export function getValue(store: Store, namePath: InternalNamePath) { - const value = get(store, namePath); - return value; -} - -export function setValue( - store: Store, - namePath: InternalNamePath, - value: StoreValue, - removeIfUndefined = false, -): Store { - const newStore = set(store, namePath, value, removeIfUndefined); - return newStore; -} - export function cloneByNamePathList(store: Store, namePathList: InternalNamePath[]): Store { let newStore = {}; namePathList.forEach(namePath => { From fe5e6e56297caa8d411bf6121ce4b73806431643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Tue, 11 Apr 2023 11:22:51 +0800 Subject: [PATCH 17/65] fix: Form.List nested not support `preserve=false` (#583) * test: test driven * fix: List should also support isFieldList --- src/List.tsx | 5 +++++ tests/preserve.test.tsx | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/List.tsx b/src/List.tsx index 7a2ae9a5..64db2e20 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -29,6 +29,9 @@ export interface ListProps { operations: ListOperations, meta: Meta, ) => JSX.Element | React.ReactNode; + + /** @private Passed by Form.List props. Do not use since it will break by path check. */ + isListField?: boolean; } const List: React.FunctionComponent = ({ @@ -37,6 +40,7 @@ const List: React.FunctionComponent = ({ children, rules, validateTrigger, + isListField, }) => { const context = React.useContext(FieldContext); const keyRef = React.useRef({ @@ -87,6 +91,7 @@ const List: React.FunctionComponent = ({ validateTrigger={validateTrigger} initialValue={initialValue} isList + isListField={isListField} > {({ value = [], onChange }, meta) => { const { getFieldValue } = context; diff --git a/tests/preserve.test.tsx b/tests/preserve.test.tsx index 6f87e059..e4951f2d 100644 --- a/tests/preserve.test.tsx +++ b/tests/preserve.test.tsx @@ -403,4 +403,46 @@ describe('Form.Preserve', () => { expect(container.querySelector('input')?.value).toEqual('bamboo'); }); + + it('nest Form.List should clear correctly', async () => { + const { container } = render( +
    + + {(fields, { remove }) => { + return fields.map(field => ( +
    +
    + )); + }} +
    +
    , + ); + + expect(container.querySelector('input').value).toEqual('bamboo'); + + // Clean + fireEvent.click(container.querySelector('button')); + expect(container.querySelector('input')).toBeFalsy(); + }); }); From c2a6427c2a401f32aae7c52c4265202b4422b5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 11 Apr 2023 11:38:50 +0800 Subject: [PATCH 18/65] 1.29.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3885e60e..d9951884 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.29.1", + "version": "1.29.2", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From b7019878162cbc3480a878dc3269bf5c8f676ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kiner-tang=28=E6=96=87=E8=BE=89=29?= <1127031143@qq.com> Date: Mon, 24 Apr 2023 22:00:03 +0800 Subject: [PATCH 19/65] fix: validated should be reset when trigger reset event (#585) --- src/Field.tsx | 2 +- tests/validate.test.tsx | 33 ++++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/Field.tsx b/src/Field.tsx index 60d72a2c..6b356ccb 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -253,7 +253,7 @@ class Field extends React.Component implements F // Clean up state this.touched = false; this.dirty = false; - this.validatePromise = null; + this.validatePromise = undefined; this.errors = EMPTY_ERRORS; this.warnings = EMPTY_ERRORS; this.triggerMetaEvent(); diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index f97f5be1..ef4a5e40 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -797,14 +797,28 @@ describe('Form.Validate', () => { it('should trigger onFieldsChange 3 times', async () => { const onFieldsChange = jest.fn(); + const onMetaChange = jest.fn(); - const wrapper = mount( -
    - - - -
    , - ); + const App = () => { + const ref = React.useRef(null); + return ( +
    + { + onMetaChange(meta.validated); + }} + > + + + +
    + ); + }; + const wrapper = mount(); await changeValue(getField(wrapper, 'test'), ''); @@ -847,5 +861,10 @@ describe('Form.Validate', () => { ], expect.anything(), ); + // should reset validated and validating when reset btn had been clicked + wrapper.find('#reset').simulate('reset'); + await timeout(); + expect(onMetaChange).toHaveBeenNthCalledWith(3, true); + expect(onMetaChange).toHaveBeenNthCalledWith(4, false); }); }); From c6dab30db4de5e0e914034280f4b4b7172ed7f9d Mon Sep 17 00:00:00 2001 From: Even sky Date: Mon, 24 Apr 2023 22:00:15 +0800 Subject: [PATCH 20/65] fix: form onValues second params values (#582) * fix: form onValues second params values close https://github.com/ant-design/ant-design/issues/41053 * test: improve list case * fix: review * fix: list nest list * test: add list case * fix: up --- src/Field.tsx | 13 +++++++++++-- src/List.tsx | 3 ++- tests/list.test.tsx | 45 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/Field.tsx b/src/Field.tsx index 6b356ccb..806708fb 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -18,6 +18,7 @@ import type { RuleError, } from './interface'; import FieldContext, { HOOK_MARK } from './FieldContext'; +import ListContext from './ListContext'; import { toArray } from './utils/typeUtil'; import { validateRules } from './utils/validateUtil'; import { @@ -625,7 +626,7 @@ class Field extends React.Component implements F function WrapperField({ name, ...restProps }: FieldProps) { const fieldContext = React.useContext(FieldContext); - + const listContext = React.useContext(ListContext); const namePath = name !== undefined ? getNamePath(name) : undefined; let key: string = 'keep'; @@ -644,7 +645,15 @@ function WrapperField({ name, ...restProps }: FieldProps) warning(false, '`preserve` should not apply on Form.List fields.'); } - return ; + return ( + + ); } export default WrapperField; diff --git a/src/List.tsx b/src/List.tsx index 64db2e20..839e8908 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -43,6 +43,7 @@ const List: React.FunctionComponent = ({ isListField, }) => { const context = React.useContext(FieldContext); + const wrapperListContext = React.useContext(ListContext); const keyRef = React.useRef({ keys: [], id: 0, @@ -91,7 +92,7 @@ const List: React.FunctionComponent = ({ validateTrigger={validateTrigger} initialValue={initialValue} isList - isListField={isListField} + isListField={isListField ?? !!wrapperListContext} > {({ value = [], onChange }, meta) => { const { getFieldValue } = context; diff --git a/tests/list.test.tsx b/tests/list.test.tsx index 06a66132..c808f046 100644 --- a/tests/list.test.tsx +++ b/tests/list.test.tsx @@ -628,6 +628,51 @@ describe('Form.List', () => { expect(onValuesChange).toHaveBeenCalledWith(expect.anything(), { list: [{ first: 'light' }] }); }); + it('Nest list remove should trigger correct onValuesChange when no spread field props', () => { + const onValuesChange = jest.fn(); + + const [wrapper] = generateForm( + (fields, operation) => ( +
    + {fields.map(field => ( +
    + + + + + + +
    + ))} +
    + ), + { + onValuesChange, + initialValues: { + list: [{ first: 'light' }], + }, + }, + ); + wrapper.find('button').first().simulate('click'); + expect(onValuesChange).toHaveBeenCalledWith(expect.anything(), { + list: [{ first: 'light' }, undefined], + }); + wrapper.find('button').last().simulate('click'); + expect(onValuesChange).toHaveBeenCalledWith(expect.anything(), { list: [{ first: 'light' }] }); + }); + describe('isFieldTouched edge case', () => { it('virtual object', () => { const formRef = React.createRef(); From 7a273d05a838023efd6023dcf810c94d2d81019a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 24 Apr 2023 22:04:30 +0800 Subject: [PATCH 21/65] 1.30.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d9951884..1d95d927 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.29.2", + "version": "1.30.0", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From bfdcd2cd7d11d659d2ae9c5226d6732932504d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Thu, 11 May 2023 14:29:09 +0800 Subject: [PATCH 22/65] feat: Support `validateOnly` (#586) * chore: init ts def * chore: support validateOnly for only check * feat: validateOnly support * test: add test case * chore: fix ts --- docs/demo/validateOnly.md | 3 ++ docs/examples/validateOnly.tsx | 76 ++++++++++++++++++++++++++++++++++ src/Field.tsx | 11 +++-- src/interface.ts | 23 +++++++--- src/useForm.ts | 17 +++++--- src/utils/validateUtil.ts | 6 +-- tests/validate.test.tsx | 25 ++++++++++- 7 files changed, 143 insertions(+), 18 deletions(-) create mode 100644 docs/demo/validateOnly.md create mode 100644 docs/examples/validateOnly.tsx diff --git a/docs/demo/validateOnly.md b/docs/demo/validateOnly.md new file mode 100644 index 00000000..2be92f39 --- /dev/null +++ b/docs/demo/validateOnly.md @@ -0,0 +1,3 @@ +## validateOnly + + diff --git a/docs/examples/validateOnly.tsx b/docs/examples/validateOnly.tsx new file mode 100644 index 00000000..df561830 --- /dev/null +++ b/docs/examples/validateOnly.tsx @@ -0,0 +1,76 @@ +/* eslint-disable react/prop-types, @typescript-eslint/consistent-type-imports */ + +import React from 'react'; +import Form from 'rc-field-form'; +import type { FormInstance } from 'rc-field-form'; +import Input from './components/Input'; +import LabelField from './components/LabelField'; + +function useSubmittable(form: FormInstance) { + const [submittable, setSubmittable] = React.useState(false); + const store = Form.useWatch([], form); + + React.useEffect(() => { + form + .validateFields({ + validateOnly: true, + }) + .then( + () => { + setSubmittable(true); + }, + () => { + setSubmittable(false); + }, + ); + }, [store]); + + return submittable; +} + +export default () => { + const [form] = Form.useForm(); + + const canSubmit = useSubmittable(form); + + const onValidateOnly = async () => { + const result = await form.validateFields({ + validateOnly: true, + }); + console.log('Validate:', result); + }; + + return ( + <> +
    + Promise.reject('Warn Name!') }, + ]} + > + + + Promise.reject('Warn Age!') }, + ]} + > + + + + +
    + + + ); +}; diff --git a/src/Field.tsx b/src/Field.tsx index 806708fb..ce706ff9 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -10,7 +10,7 @@ import type { NotifyInfo, Rule, Store, - ValidateOptions, + InternalValidateOptions, InternalFormInstance, RuleObject, StoreValue, @@ -358,11 +358,13 @@ class Field extends React.Component implements F } }; - public validateRules = (options?: ValidateOptions): Promise => { + public validateRules = (options?: InternalValidateOptions): Promise => { // We should fixed namePath & value to avoid developer change then by form function const namePath = this.getNamePath(); const currentValue = this.getValue(); + const { triggerName, validateOnly = false } = options || {}; + // Force change to async to avoid rule OOD under renderProps field const rootPromise = Promise.resolve().then(() => { if (!this.mounted) { @@ -370,7 +372,6 @@ class Field extends React.Component implements F } const { validateFirst = false, messageVariables } = this.props; - const { triggerName } = (options || {}) as ValidateOptions; let filteredRules = this.getRules(); if (triggerName) { @@ -423,6 +424,10 @@ class Field extends React.Component implements F return promise; }); + if (validateOnly) { + return rootPromise; + } + this.validatePromise = rootPromise; this.dirty = true; this.errors = EMPTY_ERRORS; diff --git a/src/interface.ts b/src/interface.ts index 3413d602..2fd74a4c 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -102,7 +102,7 @@ export interface FieldEntity { isListField: () => boolean; isList: () => boolean; isPreserve: () => boolean; - validateRules: (options?: ValidateOptions) => Promise; + validateRules: (options?: InternalValidateOptions) => Promise; getMeta: () => Meta; getNamePath: () => InternalNamePath; getErrors: () => string[]; @@ -127,6 +127,18 @@ export interface RuleError { } export interface ValidateOptions { + /** + * Validate only and not trigger UI and Field status update + */ + validateOnly?: boolean; +} + +export type ValidateFields = { + (opt?: ValidateOptions): Promise; + (nameList?: NamePath[], opt?: ValidateOptions): Promise; +}; + +export interface InternalValidateOptions extends ValidateOptions { triggerName?: string; validateMessages?: ValidateMessages; /** @@ -136,11 +148,10 @@ export interface ValidateOptions { recursive?: boolean; } -export type InternalValidateFields = ( - nameList?: NamePath[], - options?: ValidateOptions, -) => Promise; -export type ValidateFields = (nameList?: NamePath[]) => Promise; +export type InternalValidateFields = { + (options?: InternalValidateOptions): Promise; + (nameList?: NamePath[], options?: InternalValidateOptions): Promise; +}; // >>>>>> Info interface ValueUpdateInfo { diff --git a/src/useForm.ts b/src/useForm.ts index 1d497e85..f0af0bad 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -20,7 +20,7 @@ import type { StoreValue, ValidateErrorEntity, ValidateMessages, - ValidateOptions, + InternalValidateOptions, ValuedNotifyInfo, WatchCallBack, } from './interface'; @@ -836,12 +836,19 @@ export class FormStore { }; // =========================== Validate =========================== - private validateFields: InternalValidateFields = ( - nameList?: NamePath[], - options?: ValidateOptions, - ) => { + private validateFields: InternalValidateFields = (arg1?: any, arg2?: any) => { this.warningUnhooked(); + let nameList: NamePath[]; + let options: InternalValidateOptions; + + if (Array.isArray(arg1) || typeof arg1 === 'string' || typeof arg2 === 'string') { + nameList = arg1; + options = arg2; + } else { + options = arg1; + } + const provideNameList = !!nameList; const namePathList: InternalNamePath[] | undefined = provideNameList ? nameList.map(getNamePath) diff --git a/src/utils/validateUtil.ts b/src/utils/validateUtil.ts index 7270ac1e..dcff5ea3 100644 --- a/src/utils/validateUtil.ts +++ b/src/utils/validateUtil.ts @@ -3,7 +3,7 @@ import * as React from 'react'; import warning from 'rc-util/lib/warning'; import type { InternalNamePath, - ValidateOptions, + InternalValidateOptions, RuleObject, StoreValue, RuleError, @@ -31,7 +31,7 @@ async function validateRule( name: string, value: StoreValue, rule: RuleObject, - options: ValidateOptions, + options: InternalValidateOptions, messageVariables?: Record, ): Promise { const cloneRule = { ...rule }; @@ -123,7 +123,7 @@ export function validateRules( namePath: InternalNamePath, value: StoreValue, rules: RuleObject[], - options: ValidateOptions, + options: InternalValidateOptions, validateFirst: boolean | 'parallel', messageVariables?: Record, ) { diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index ef4a5e40..7d36aca8 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -1,11 +1,12 @@ import React, { useEffect } from 'react'; +import { render } from '@testing-library/react'; import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import Form, { Field, useForm } from '../src'; import InfoField, { Input } from './common/InfoField'; import { changeValue, matchError, getField } from './common'; import timeout from './common/timeout'; -import type { ValidateMessages } from '@/interface'; +import type { FormInstance, ValidateMessages } from '../src/interface'; describe('Form.Validate', () => { it('required', async () => { @@ -867,4 +868,26 @@ describe('Form.Validate', () => { expect(onMetaChange).toHaveBeenNthCalledWith(3, true); expect(onMetaChange).toHaveBeenNthCalledWith(4, false); }); + + it('validateOnly', async () => { + const formRef = React.createRef(); + const { container } = render( +
    + + + +
    , + ); + + // Validate only + const result = await formRef.current.validateFields({ validateOnly: true }).catch(e => e); + await timeout(); + expect(result.errorFields).toHaveLength(1); + expect(container.querySelector('.errors').textContent).toBeFalsy(); + + // Normal validate + await formRef.current.validateFields().catch(e => e); + await timeout(); + expect(container.querySelector('.errors').textContent).toEqual(`'test' is required`); + }); }); From ffd4b68767984c2fec7af4f0999191fb71762368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 11 May 2023 14:30:55 +0800 Subject: [PATCH 23/65] 1.31.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1d95d927..6b8845ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.30.0", + "version": "1.31.0", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From 8bc948fe09d278fe8054a65ef53ccf5ad529da77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Fri, 26 May 2023 10:25:08 +0800 Subject: [PATCH 24/65] refactor: Use rc-util for merge (#590) * refactor: use rc-util * chore: bump rc-util * chore: update deps --- package.json | 2 +- src/useForm.ts | 11 +++++------ src/utils/cloneDeep.ts | 25 ------------------------- src/utils/validateUtil.ts | 4 ++-- src/utils/valueUtil.ts | 38 +------------------------------------- tests/utils.test.ts | 24 +----------------------- 6 files changed, 10 insertions(+), 94 deletions(-) delete mode 100644 src/utils/cloneDeep.ts diff --git a/package.json b/package.json index 6b8845ef..87e06a77 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "dependencies": { "@babel/runtime": "^7.18.0", "async-validator": "^4.1.0", - "rc-util": "^5.8.0" + "rc-util": "^5.32.2" }, "devDependencies": { "@testing-library/jest-dom": "^5.16.4", diff --git a/src/useForm.ts b/src/useForm.ts index f0af0bad..c6945349 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -25,7 +25,7 @@ import type { WatchCallBack, } from './interface'; import { allPromiseFinish } from './utils/asyncUtil'; -import cloneDeep from './utils/cloneDeep'; +import { merge } from 'rc-util/lib/utils/set'; import { defaultValidateMessages } from './utils/messages'; import NameMap from './utils/NameMap'; import { @@ -35,7 +35,6 @@ import { getValue, matchNamePath, setValue, - setValues, } from './utils/valueUtil'; type InvalidateFieldEntity = { INVALIDATE_NAME_PATH: InternalNamePath }; @@ -141,7 +140,7 @@ export class FormStore { private setInitialValues = (initialValues: Store, init: boolean) => { this.initialValues = initialValues || {}; if (init) { - let nextStore = setValues({}, initialValues, this.store); + let nextStore = merge(initialValues, this.store); // We will take consider prev form unmount fields. // When the field is not `preserve`, we need fill this with initialValues instead of store. @@ -170,7 +169,7 @@ export class FormStore { const initValue = getValue(this.initialValues, namePath); // Not cloneDeep when without `namePath` - return namePath.length ? cloneDeep(initValue) : initValue; + return namePath.length ? merge(initValue) : initValue; }; private setCallbacks = (callbacks: Callbacks) => { @@ -523,7 +522,7 @@ export class FormStore { const prevStore = this.store; if (!nameList) { - this.updateStore(setValues({}, this.initialValues)); + this.updateStore(merge(this.initialValues)); this.resetWithFieldInitialValue(); this.notifyObservers(prevStore, null, { type: 'reset' }); this.notifyWatch(); @@ -743,7 +742,7 @@ export class FormStore { const prevStore = this.store; if (store) { - const nextStore = setValues(this.store, store); + const nextStore = merge(this.store, store); this.updateStore(nextStore); } diff --git a/src/utils/cloneDeep.ts b/src/utils/cloneDeep.ts deleted file mode 100644 index d12467b2..00000000 --- a/src/utils/cloneDeep.ts +++ /dev/null @@ -1,25 +0,0 @@ -function cloneDeep(val) { - if (Array.isArray(val)) { - return cloneArrayDeep(val); - } else if (typeof val === 'object' && val !== null) { - return cloneObjectDeep(val); - } - return val; -} - -function cloneObjectDeep(val) { - if (Object.getPrototypeOf(val) === Object.prototype) { - const res = {}; - for (const key in val) { - res[key] = cloneDeep(val[key]); - } - return res; - } - return val; -} - -function cloneArrayDeep(val) { - return val.map(item => cloneDeep(item)); -} - -export default cloneDeep; diff --git a/src/utils/validateUtil.ts b/src/utils/validateUtil.ts index dcff5ea3..866e0e85 100644 --- a/src/utils/validateUtil.ts +++ b/src/utils/validateUtil.ts @@ -9,7 +9,7 @@ import type { RuleError, } from '../interface'; import { defaultValidateMessages } from './messages'; -import { setValues } from './valueUtil'; +import { merge } from 'rc-util/lib/utils/set'; // Remove incorrect original ts define const AsyncValidator: any = RawAsyncValidator; @@ -67,7 +67,7 @@ async function validateRule( [name]: [cloneRule], }); - const messages = setValues({}, defaultValidateMessages, options.validateMessages); + const messages = merge(defaultValidateMessages, options.validateMessages); validator.messages(messages); let result = []; diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 4dbcf94b..93a0f246 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -1,8 +1,7 @@ import getValue from 'rc-util/lib/utils/get'; import setValue from 'rc-util/lib/utils/set'; -import type { InternalNamePath, NamePath, Store, StoreValue, EventArgs } from '../interface'; +import type { InternalNamePath, NamePath, Store, EventArgs } from '../interface'; import { toArray } from './typeUtil'; -import cloneDeep from '../utils/cloneDeep'; export { getValue, setValue }; @@ -31,41 +30,6 @@ export function containsNamePath(namePathList: InternalNamePath[], namePath: Int return namePathList && namePathList.some(path => matchNamePath(path, namePath)); } -function isObject(obj: StoreValue) { - return typeof obj === 'object' && obj !== null && Object.getPrototypeOf(obj) === Object.prototype; -} - -/** - * Copy values into store and return a new values object - * ({ a: 1, b: { c: 2 } }, { a: 4, b: { d: 5 } }) => { a: 4, b: { c: 2, d: 5 } } - */ -function internalSetValues(store: T, values: T): T { - const newStore: T = (Array.isArray(store) ? [...store] : { ...store }) as T; - - if (!values) { - return newStore; - } - - Object.keys(values).forEach(key => { - const prevValue = newStore[key]; - const value = values[key]; - - // If both are object (but target is not array), we use recursion to set deep value - const recursive = isObject(prevValue) && isObject(value); - - newStore[key] = recursive ? internalSetValues(prevValue, value || {}) : cloneDeep(value); // Clone deep for arrays - }); - - return newStore; -} - -export function setValues(store: T, ...restValues: T[]): T { - return restValues.reduce( - (current: T, newStore: T): T => internalSetValues(current, newStore), - store, - ); -} - export function matchNamePath( namePath: InternalNamePath, changedNamePath: InternalNamePath | null, diff --git a/tests/utils.test.ts b/tests/utils.test.ts index 7bc47894..d40b2dc8 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -1,6 +1,5 @@ -import { move, isSimilar, setValues } from '../src/utils/valueUtil'; +import { move, isSimilar } from '../src/utils/valueUtil'; import NameMap from '../src/utils/NameMap'; -import cloneDeep from '../src/utils/cloneDeep'; describe('utils', () => { describe('arrayMove', () => { @@ -31,19 +30,6 @@ describe('utils', () => { expect(isSimilar({}, null)).toBeFalsy(); expect(isSimilar(null, {})).toBeFalsy(); }); - - describe('setValues', () => { - it('basic', () => { - expect(setValues({}, { a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 }); - expect(setValues([], [123])).toEqual([123]); - }); - - it('Correct handle class instance', () => { - const out = setValues({}, { a: 1, b: { c: new Date() } }); - expect(out.a).toEqual(1); - expect(out.b.c instanceof Date).toBeTruthy(); - }); - }); }); describe('NameMap', () => { @@ -72,12 +58,4 @@ describe('utils', () => { }); }); }); - - describe('clone deep', () => { - it('should not deep clone Class', () => { - const data = { a: new Date() }; - const clonedData = cloneDeep(data); - expect(data.a === clonedData.a).toBeTruthy(); - }); - }); }); From 8e88e234eb690d8b08915f373a7501dc7f379195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 26 May 2023 10:28:33 +0800 Subject: [PATCH 25/65] 1.32.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 87e06a77..71be9879 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.31.0", + "version": "1.32.0", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From ed62e2bba4d907a355635c5793213a5d80765f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kiner-tang=28=E6=96=87=E8=BE=89=29?= <1127031143@qq.com> Date: Mon, 12 Jun 2023 11:09:45 +0800 Subject: [PATCH 26/65] feat: optimize type definition (#592) --- src/interface.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interface.ts b/src/interface.ts index 2fd74a4c..d00582f4 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -232,11 +232,11 @@ export interface InternalHooks { } /** Only return partial when type is not any */ -type RecursivePartial = T extends object +type RecursivePartial = NonNullable extends object ? { - [P in keyof T]?: T[P] extends (infer U)[] + [P in keyof T]?: NonNullable extends (infer U)[] ? RecursivePartial[] - : T[P] extends object + : NonNullable extends object ? RecursivePartial : T[P]; } From b48c22277cb8342039ba85f0472d6dc92bb18096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 12 Jun 2023 11:11:44 +0800 Subject: [PATCH 27/65] 1.32.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 71be9879..d744375f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.32.0", + "version": "1.32.1", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From 3ddb3aabb920fd900356c481d3cef7df56f17831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Wed, 14 Jun 2023 14:05:18 +0800 Subject: [PATCH 28/65] fix: ListContext makes nest Form event bubble out of range (#593) * test: test driven * fix: ListConext should not pass --- src/Form.tsx | 5 ++++- tests/list.test.tsx | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Form.tsx b/src/Form.tsx index de30b6f9..370185cb 100644 --- a/src/Form.tsx +++ b/src/Form.tsx @@ -12,6 +12,7 @@ import FieldContext, { HOOK_MARK } from './FieldContext'; import type { FormContextProps } from './FormContext'; import FormContext from './FormContext'; import { isSimilar } from './utils/valueUtil'; +import ListContext from './ListContext'; type BaseFormProps = Omit, 'onSubmit' | 'children'>; @@ -147,7 +148,9 @@ const Form: React.ForwardRefRenderFunction = ( ); const wrapperNode = ( - {childrenNode} + + {childrenNode} + ); if (Component === false) { diff --git a/tests/list.test.tsx b/tests/list.test.tsx index c808f046..60c4cac4 100644 --- a/tests/list.test.tsx +++ b/tests/list.test.tsx @@ -827,4 +827,32 @@ describe('Form.List', () => { expect(wrapper.find('.internal-rest').text()).toEqual('user'); expect(wrapper.find('input').prop('value')).toEqual('bamboo'); }); + + it('list should not pass context', () => { + const onValuesChange = jest.fn(); + + const InnerForm = () => ( +
    + + + + + + +
    + ); + + const wrapper = mount( +
    + {() => } +
    , + ); + + wrapper + .find('input') + .first() + .simulate('change', { target: { value: 'little' } }); + + expect(onValuesChange).toHaveBeenCalledWith({ name: 'little' }, { name: 'little', age: 2 }); + }); }); From 47c3373a3534d6b765d162015384ffa44ac0b8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 14 Jun 2023 14:07:44 +0800 Subject: [PATCH 29/65] 1.32.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d744375f..86f80dc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.32.1", + "version": "1.32.2", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From 5cea6b9801ecabcf68f863d835aa21a6dad8f3b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kiner-tang=28=E6=96=87=E8=BE=89=29?= <1127031143@qq.com> Date: Sun, 25 Jun 2023 10:20:02 +0800 Subject: [PATCH 30/65] refactor: solve field-form circular dependency issue (#595) --- src/useWatch.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/useWatch.ts b/src/useWatch.ts index afaa9846..23c41bc6 100644 --- a/src/useWatch.ts +++ b/src/useWatch.ts @@ -1,8 +1,7 @@ -import type { FormInstance } from '.'; -import { FieldContext } from '.'; import warning from 'rc-util/lib/warning'; -import { HOOK_MARK } from './FieldContext'; +import FieldContext, { HOOK_MARK } from './FieldContext'; import type { + FormInstance, InternalFormInstance, InternalNamePath, NamePath, From 04b1667b32013338ad32c09895a5b84714c5d589 Mon Sep 17 00:00:00 2001 From: afc163 Date: Sun, 25 Jun 2023 10:56:56 +0800 Subject: [PATCH 31/65] 1.33.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 86f80dc5..75c85401 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.32.2", + "version": "1.33.0", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From e0590b6cdca4a87db2ad671b68296a6b5a42cb91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Fri, 30 Jun 2023 10:12:40 +0800 Subject: [PATCH 32/65] fix: should not trigger `onFieldsChange` when `submit` (#598) * test: test driven * fix: not trigger if no need * fix: check logic --- docs/examples/basic.tsx | 10 +++++++++- src/useForm.ts | 16 ++++++++++++++-- tests/validate.test.tsx | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/docs/examples/basic.tsx b/docs/examples/basic.tsx index 6e170fbf..a6322660 100644 --- a/docs/examples/basic.tsx +++ b/docs/examples/basic.tsx @@ -6,7 +6,13 @@ export default () => { const [form] = Form.useForm(); return ( -
    + { + console.error('fields:', fields); + }} + > @@ -32,6 +38,8 @@ export default () => { ) : null; }} + +
    ); }; diff --git a/src/useForm.ts b/src/useForm.ts index c6945349..3558f4b5 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -830,7 +830,10 @@ export class FormStore { const changedFields = fields.filter(({ name: fieldName }) => containsNamePath(namePathList, fieldName as InternalNamePath), ); - onFieldsChange(changedFields, fields); + + if (changedFields.length) { + onFieldsChange(changedFields, fields); + } } }; @@ -856,6 +859,10 @@ export class FormStore { // Collect result in promise list const promiseList: Promise[] = []; + // We temp save the path which need trigger for `onFieldsChange` + const TMP_SPLIT = String(Date.now()); + const validateNamePathList = new Set(); + this.getFieldEntities(true).forEach((field: FieldEntity) => { // Add field if not provide `nameList` if (!provideNameList) { @@ -883,6 +890,8 @@ export class FormStore { } const fieldNamePath = field.getNamePath(); + validateNamePathList.add(fieldNamePath.join(TMP_SPLIT)); + // Add field validate rule in to promise list if (!provideNameList || containsNamePath(namePathList, fieldNamePath)) { const promise = field.validateRules({ @@ -961,7 +970,10 @@ export class FormStore { returnPromise.catch(e => e); // `validating` changed. Trigger `onFieldsChange` - this.triggerOnFieldsChange(namePathList); + const triggerNamePathList = namePathList.filter(namePath => + validateNamePathList.has(namePath.join(TMP_SPLIT)), + ); + this.triggerOnFieldsChange(triggerNamePathList); return returnPromise as Promise; }; diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index 7d36aca8..7c8fe212 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -869,6 +869,43 @@ describe('Form.Validate', () => { expect(onMetaChange).toHaveBeenNthCalledWith(4, false); }); + it('should not trigger onFieldsChange if no rules', async () => { + const onFieldsChange = jest.fn(); + const onFinish = jest.fn(); + + const App = () => { + return ( +
    + + {fields => + fields.map(field => ( + + + + )) + } + +
    + ); + }; + const wrapper = mount(); + + wrapper.find('form').simulate('submit'); + + await timeout(); + + expect(onFieldsChange).not.toHaveBeenCalled(); + expect(onFinish).toHaveBeenCalledWith({ + list: ['hello'], + }); + }); + it('validateOnly', async () => { const formRef = React.createRef(); const { container } = render( From 4114eecccf4bf6836970db56e616337b07c36060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 30 Jun 2023 10:40:23 +0800 Subject: [PATCH 33/65] 1.34.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 75c85401..026e3b34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.33.0", + "version": "1.34.0", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From 10a2efe688c22b629fdfff73c3b8613b2a59058f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kiner-tang=28=E6=96=87=E8=BE=89=29?= <1127031143@qq.com> Date: Sun, 2 Jul 2023 14:41:38 +0800 Subject: [PATCH 34/65] fix: setFieldsValue bug with union type of Array and null (#599) --- src/interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interface.ts b/src/interface.ts index d00582f4..575d468c 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -240,7 +240,7 @@ type RecursivePartial = NonNullable extends object ? RecursivePartial : T[P]; } - : any; + : T; export interface FormInstance { // Origin Form API From 18c4e0bbb024371749b5c325e0ab0bb0b0c542d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 3 Jul 2023 14:23:15 +0800 Subject: [PATCH 35/65] 1.34.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 026e3b34..480c2bab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.34.0", + "version": "1.34.1", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From 437f3c0982f4a1334eb10ad3872afc2e2fdabebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Fri, 14 Jul 2023 20:51:14 +0800 Subject: [PATCH 36/65] fix: onMetaChange trigger when meta not changed (#604) * test: test driven * fix: meta loop call --- src/Field.tsx | 20 ++++++++++++++++++-- tests/index.test.tsx | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/Field.tsx b/src/Field.tsx index ce706ff9..92ff9756 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -1,5 +1,6 @@ import toChildrenArray from 'rc-util/lib/Children/toArray'; import warning from 'rc-util/lib/warning'; +import isEqual from 'rc-util/lib/isEqual'; import * as React from 'react'; import type { FieldEntity, @@ -54,6 +55,8 @@ interface ChildProps { [name: string]: any; } +export type MetaEvent = Meta & { destroy?: boolean }; + export interface InternalFieldProps { children?: | React.ReactElement @@ -77,7 +80,7 @@ export interface InternalFieldProps { messageVariables?: Record; initialValue?: any; onReset?: () => void; - onMetaChange?: (meta: Meta & { destroy?: boolean }) => void; + onMetaChange?: (meta: MetaEvent) => void; preserve?: boolean; /** @private Passed by Form.List props. Do not use since it will break by path check. */ @@ -221,10 +224,23 @@ class Field extends React.Component implements F })); }; + // Event should only trigger when meta changed + private metaCache: MetaEvent = null; + public triggerMetaEvent = (destroy?: boolean) => { const { onMetaChange } = this.props; - onMetaChange?.({ ...this.getMeta(), destroy }); + if (onMetaChange) { + const meta = { ...this.getMeta(), destroy }; + + if (!isEqual(this.metaCache, meta)) { + onMetaChange(meta); + } + + this.metaCache = meta; + } else { + this.metaCache = null; + } }; // ========================= Field Entity Interfaces ========================= diff --git a/tests/index.test.tsx b/tests/index.test.tsx index a05ad09b..3c21133d 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -838,4 +838,29 @@ describe('Form.Basic', () => { Array.from(container.querySelectorAll('input')).map(input => input?.value), ).toEqual(['bamboo', 'tiny', 'light', 'match']); }); + + it('onMetaChange should only trigger when meta changed', () => { + const onMetaChange = jest.fn(); + const formRef = React.createRef(); + + const Demo: React.FC = () => ( +
    + false}> + {() => null} + +
    + ); + + render(); + + formRef.current?.setFieldsValue({}); + onMetaChange.mockReset(); + + // Re-render should not trigger `onMetaChange` + for (let i = 0; i < 10; i += 1) { + formRef.current?.setFieldsValue({}); + } + + expect(onMetaChange).toHaveBeenCalledTimes(0); + }); }); From 135549eb1195a94c7a13a8b8d866c1b14fd1e6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 14 Jul 2023 20:54:11 +0800 Subject: [PATCH 37/65] 1.34.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 480c2bab..6e6b24d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.34.1", + "version": "1.34.2", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From e8ce5038a6d0dabf7f13194e5b25a69ec64c1a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Wed, 26 Jul 2023 20:50:29 +0800 Subject: [PATCH 38/65] feat: support Form.STRICT (#605) * feat: support Form.STRICT * refactor: use config --- src/interface.ts | 7 ++++++- src/useForm.ts | 34 ++++++++++++++++++++++++++++------ tests/list.test.tsx | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/interface.ts b/src/interface.ts index 575d468c..223a954a 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -242,11 +242,16 @@ type RecursivePartial = NonNullable extends object } : T; +export type FilterFunc = (meta: Meta) => boolean; + +export type GetFieldsValueConfig = { strict?: boolean; filter?: FilterFunc }; + export interface FormInstance { // Origin Form API getFieldValue: (name: NamePath) => StoreValue; getFieldsValue: (() => Values) & - ((nameList: NamePath[] | true, filterFunc?: (meta: Meta) => boolean) => any); + ((nameList: NamePath[] | true, filterFunc?: FilterFunc) => any) & + ((config: GetFieldsValueConfig) => any); getFieldError: (name: NamePath) => string[]; getFieldsError: (nameList?: NamePath[]) => FieldError[]; getFieldWarning: (name: NamePath) => string[]; diff --git a/src/useForm.ts b/src/useForm.ts index 3558f4b5..bb57a0f5 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -23,6 +23,8 @@ import type { InternalValidateOptions, ValuedNotifyInfo, WatchCallBack, + FilterFunc, + GetFieldsValueConfig, } from './interface'; import { allPromiseFinish } from './utils/asyncUtil'; import { merge } from 'rc-util/lib/utils/set'; @@ -265,15 +267,31 @@ export class FormStore { }); }; - private getFieldsValue = (nameList?: NamePath[] | true, filterFunc?: (meta: Meta) => boolean) => { + private getFieldsValue = ( + nameList?: NamePath[] | true | GetFieldsValueConfig, + filterFunc?: FilterFunc, + ) => { this.warningUnhooked(); - if (nameList === true && !filterFunc) { + // Fill args + let mergedNameList: NamePath[] | true; + let mergedFilterFunc: FilterFunc; + let mergedStrict: boolean; + + if (nameList === true || Array.isArray(nameList)) { + mergedNameList = nameList; + mergedFilterFunc = filterFunc; + } else if (nameList && typeof nameList === 'object') { + mergedStrict = nameList.strict; + mergedFilterFunc = nameList.filter; + } + + if (mergedNameList === true && !mergedFilterFunc) { return this.store; } const fieldEntities = this.getFieldEntitiesForNamePathList( - Array.isArray(nameList) ? nameList : null, + Array.isArray(mergedNameList) ? mergedNameList : null, ); const filteredNameList: NamePath[] = []; @@ -283,15 +301,19 @@ export class FormStore { // Ignore when it's a list item and not specific the namePath, // since parent field is already take in count - if (!nameList && (entity as FieldEntity).isListField?.()) { + if (mergedStrict) { + if ((entity as FieldEntity).isList?.()) { + return; + } + } else if (!mergedNameList && (entity as FieldEntity).isListField?.()) { return; } - if (!filterFunc) { + if (!mergedFilterFunc) { filteredNameList.push(namePath); } else { const meta: Meta = 'getMeta' in entity ? entity.getMeta() : null; - if (filterFunc(meta)) { + if (mergedFilterFunc(meta)) { filteredNameList.push(namePath); } } diff --git a/tests/list.test.tsx b/tests/list.test.tsx index 60c4cac4..574e7a75 100644 --- a/tests/list.test.tsx +++ b/tests/list.test.tsx @@ -855,4 +855,37 @@ describe('Form.List', () => { expect(onValuesChange).toHaveBeenCalledWith({ name: 'little' }, { name: 'little', age: 2 }); }); + + it('getFieldsValue with Strict mode', () => { + const formRef = React.createRef(); + + const initialValues = { list: [{ bamboo: 1, light: 3 }], little: 9 }; + + mount( +
    +
    + + + + + {fields => + fields.map(field => ( + + + + )) + } + +
    +
    , + ); + + // expect(formRef.current.getFieldsValue()).toEqual(initialValues); + + // Strict only return field not list + expect(formRef.current.getFieldsValue({ strict: true })).toEqual({ + list: [{ bamboo: 1 }], + little: 9, + }); + }); }); From e2ef99255881c6dddd4fd97c8afe4a8d48cef42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 26 Jul 2023 20:51:46 +0800 Subject: [PATCH 39/65] 1.35.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e6b24d4..96ca837d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.34.2", + "version": "1.35.0", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From d5d3ca3551efd200a355275317289228d5beacd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B6=E6=9E=AB?= <7971419+crazyair@users.noreply.github.com> Date: Mon, 31 Jul 2023 09:44:56 +0800 Subject: [PATCH 40/65] feat: Add name type check (#603) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: test * feat: test * feat: test * feat: test * feat: test * feat: test * feat: 优化写法 * feat: test * feat: 支持 list * feat: 类型优化 * feat: 类型优化 * feat: test * feat: test * feat: test * feat: test * feat: test * feat: test * feat: test * feat: test * feat: test * feat: test * feat: test * feat: test * feat: test * feat: test * feat: ts * feat: 备注 * feat: ts * chore: update desc * feat: 重新整理类型 * feat: 重新整理类型 * feat: 重新整理类型 * feat: 添加注释 * feat: 兼容推导对象 * feat: add test * feat: test * chore: update ts * feat: test * chore: update ts * feat: test * feat: 过滤方法 * feat: 支持 bool * feat: test * feat: test * feat: test * feat: test * feat: test * feat: test * feat: testa * feat: testa * chore: comment it * feat: test * feat: test * feat: test * feat: test * feat: test * feat: test --------- Co-authored-by: 二货机器人 --- docs/examples/basic.tsx | 12 +++- package.json | 2 +- src/Field.tsx | 2 +- src/List.tsx | 10 ++-- src/interface.ts | 3 +- src/namePathType.ts | 31 +++++++++++ tests/nameTypeCheck.test.tsx | 103 +++++++++++++++++++++++++++++++++++ 7 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 src/namePathType.ts create mode 100644 tests/nameTypeCheck.test.tsx diff --git a/docs/examples/basic.tsx b/docs/examples/basic.tsx index a6322660..48a46fc1 100644 --- a/docs/examples/basic.tsx +++ b/docs/examples/basic.tsx @@ -2,6 +2,12 @@ import Form, { Field } from 'rc-field-form'; import React from 'react'; import Input from './components/Input'; +type FormData = { + name?: string; + password?: string; + password2?: string; +}; + export default () => { const [form] = Form.useForm(); @@ -13,11 +19,11 @@ export default () => { console.error('fields:', fields); }} > - + name="name"> - + dependencies={['name']}> {() => { return form.getFieldValue('name') === '1' ? ( @@ -32,7 +38,7 @@ export default () => { const password = form.getFieldValue('password'); console.log('>>>', password); return password ? ( - + name={['password2']}> ) : null; diff --git a/package.json b/package.json index 96ca837d..7512ec85 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,6 @@ "react-dom": "^16.14.0", "react-redux": "^4.4.10", "redux": "^3.7.2", - "typescript": "^4.6.3" + "typescript": "^5.1.6" } } diff --git a/src/Field.tsx b/src/Field.tsx index 92ff9756..0bf85866 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -96,7 +96,7 @@ export interface InternalFieldProps { export interface FieldProps extends Omit, 'name' | 'fieldContext'> { - name?: NamePath; + name?: NamePath; } export interface FieldState { diff --git a/src/List.tsx b/src/List.tsx index 839e8908..99548bfa 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -19,8 +19,8 @@ export interface ListOperations { move: (from: number, to: number) => void; } -export interface ListProps { - name: NamePath; +export interface ListProps { + name: NamePath; rules?: ValidatorRule[]; validateTrigger?: string | string[] | false; initialValue?: any[]; @@ -34,14 +34,14 @@ export interface ListProps { isListField?: boolean; } -const List: React.FunctionComponent = ({ +function List({ name, initialValue, children, rules, validateTrigger, isListField, -}) => { +}: ListProps) { const context = React.useContext(FieldContext); const wrapperListContext = React.useContext(ListContext); const keyRef = React.useRef({ @@ -197,6 +197,6 @@ const List: React.FunctionComponent = ({ ); -}; +} export default List; diff --git a/src/interface.ts b/src/interface.ts index 223a954a..d418188d 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,8 +1,9 @@ import type { ReactElement } from 'react'; +import type { DeepNamePath } from './namePathType'; import type { ReducerAction } from './useForm'; export type InternalNamePath = (string | number)[]; -export type NamePath = string | number | InternalNamePath; +export type NamePath = DeepNamePath; export type StoreValue = any; export type Store = Record; diff --git a/src/namePathType.ts b/src/namePathType.ts new file mode 100644 index 00000000..6aa94426 --- /dev/null +++ b/src/namePathType.ts @@ -0,0 +1,31 @@ +/** + * Store: The store type from `FormInstance` + * ParentNamePath: Auto generate by nest logic. Do not fill manually. + */ +export type DeepNamePath< + Store = any, + ParentNamePath extends any[] = [], +> = ParentNamePath['length'] extends 10 + ? never + : // Follow code is batch check if `Store` is base type + true extends (Store extends string | number | boolean ? true : false) + ? ParentNamePath['length'] extends 0 + ? Store // Return `string | number | boolean` instead of array if `ParentNamePath` is empty + : never + : true extends (Store extends (string | number | boolean)[] ? true : false) + ? ParentNamePath['length'] extends 0 + ? Store // Return `(string | number | boolean)[]` instead of array if `ParentNamePath` is empty + : [...ParentNamePath, number] // Connect path + : Store extends any[] // Check if `Store` is `any[]` + ? // Connect path. e.g. { a: { b: string }[] } + // Get: [a] | [ a,number] | [ a ,number , b] + [...ParentNamePath, number] | DeepNamePath + : { + // Convert `Store` to . We mark key a `FieldKey` + [FieldKey in keyof Store]: Store[FieldKey] extends Function + ? never + : + | (ParentNamePath['length'] extends 0 ? FieldKey : never) // If `ParentNamePath` is empty, it can use `FieldKey` without array path + | [...ParentNamePath, FieldKey] // Exist `ParentNamePath`, connect it + | DeepNamePath[FieldKey], [...ParentNamePath, FieldKey]>; // If `Store[FieldKey]` is object + }[keyof Store]; diff --git a/tests/nameTypeCheck.test.tsx b/tests/nameTypeCheck.test.tsx new file mode 100644 index 00000000..e15c0f3f --- /dev/null +++ b/tests/nameTypeCheck.test.tsx @@ -0,0 +1,103 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React from 'react'; +import { render } from '@testing-library/react'; +import Form, { Field, List } from '../src'; +import type { NamePath } from '../src/interface'; + +describe('nameTypeCheck', () => { + it('typescript', () => { + type FieldType = { + a: string; + b?: string[]; + c?: { c1?: string; c2?: string[]; c3?: boolean[] }[]; + d?: { d1?: string[]; d2?: string }; + e?: { e1?: { e2?: string; e3?: string[]; e4: { e5: { e6: string } } } }; + list?: { age?: string }[]; + }; + + type fieldType = NamePath; + + const Demo: React.FC = () => { + return ( +
    + {/* 无类型 */} + + + + + + + + + + {/* */} + {/* 有类型 */} + name={'a'} /> + name={'b'} /> + name={'c'} /> + name={'d'} /> + name={'e'} /> + name={['a']} /> + name={['b']} /> + name={['c']} /> + name={['d']} /> + name={['e']} /> + name={['b', 1]} /> + name={['c', 1]} /> + name={['c', 1, 'c1']} /> + name={['c', 1, 'c2']} /> + name={['c', 1, 'c2', 1]} /> + name={['c', 1, 'c3']} /> + name={['c', 1, 'c3', 1]} /> + name={['d', 'd1']} /> + name={['d', 'd1', 1]} /> + name={['d', 'd2']} /> + name={['e', 'e1']} /> + name={['e', 'e1', 'e2']} /> + name={['e', 'e1', 'e3']} /> + name={['e', 'e1', 'e3', 1]} /> + name={['e', 'e1', 'e4']} /> + name={['e', 'e1', 'e4', 'e5']} /> + name={['e', 'e1', 'e4', 'e5', 'e6']} /> + {/* list */} + name={'list'}> + {fields => { + return fields.map(field => ( + {...field} name={[1, 'age']} key={field.key} /> + )); + }} + + + ); + }; + render(); + }); + it('type inference', () => { + interface Props { + data?: T[]; + list?: { name?: NamePath }[]; + } + function func(props: Props) { + return props; + } + func({ data: [{ a: { b: 'c' } }], list: [{ name: ['a', 'b'] }] }); + }); + it('more type', () => { + // Moment + type t1 = NamePath<{ a: { b: string; func: Moment } }>; + // Function + type t2 = NamePath<{ a: { b: string; func: () => { c: string } } }>; + + interface Moment { + func2: Function; + format: (format?: string) => string; + } + }); + it('tree', () => { + type t1 = NamePath<{ a: TreeNode }>; + + interface TreeNode { + child: TreeNode[]; + } + }); +}); From d38ef7fbe8793854cd288e271003ea5c14e3fbc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 31 Jul 2023 09:58:12 +0800 Subject: [PATCH 41/65] 1.36.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7512ec85..75a64941 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.35.0", + "version": "1.36.0", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From c35e33599b875a2b872c77f2931ba2f2243a20ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B6=E6=9E=AB?= <7971419+crazyair@users.noreply.github.com> Date: Tue, 1 Aug 2023 09:47:39 +0800 Subject: [PATCH 42/65] Compatible base `namePath` (#607) * feat: test * feat: test --- src/namePathType.ts | 11 +++++------ tests/nameTypeCheck.test.tsx | 4 +++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/namePathType.ts b/src/namePathType.ts index 6aa94426..67a0245f 100644 --- a/src/namePathType.ts +++ b/src/namePathType.ts @@ -1,3 +1,4 @@ +type BaseNamePath = string | number | boolean | (string | number | boolean)[]; /** * Store: The store type from `FormInstance` * ParentNamePath: Auto generate by nest logic. Do not fill manually. @@ -8,14 +9,12 @@ export type DeepNamePath< > = ParentNamePath['length'] extends 10 ? never : // Follow code is batch check if `Store` is base type - true extends (Store extends string | number | boolean ? true : false) + true extends (Store extends BaseNamePath ? true : false) ? ParentNamePath['length'] extends 0 - ? Store // Return `string | number | boolean` instead of array if `ParentNamePath` is empty + ? Store | BaseNamePath // Return `BaseNamePath` instead of array if `ParentNamePath` is empty + : Store extends any[] + ? [...ParentNamePath, number] // Connect path : never - : true extends (Store extends (string | number | boolean)[] ? true : false) - ? ParentNamePath['length'] extends 0 - ? Store // Return `(string | number | boolean)[]` instead of array if `ParentNamePath` is empty - : [...ParentNamePath, number] // Connect path : Store extends any[] // Check if `Store` is `any[]` ? // Connect path. e.g. { a: { b: string }[] } // Get: [a] | [ a,number] | [ a ,number , b] diff --git a/tests/nameTypeCheck.test.tsx b/tests/nameTypeCheck.test.tsx index e15c0f3f..4f8a17bd 100644 --- a/tests/nameTypeCheck.test.tsx +++ b/tests/nameTypeCheck.test.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import React from 'react'; +import React, { useMemo } from 'react'; import { render } from '@testing-library/react'; import Form, { Field, List } from '../src'; import type { NamePath } from '../src/interface'; @@ -20,6 +20,8 @@ describe('nameTypeCheck', () => { const Demo: React.FC = () => { return (
    + + {/* 无类型 */} From a1523e23bb78f6907efcdd523cc5b7f86adbcb79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 1 Aug 2023 09:55:39 +0800 Subject: [PATCH 43/65] 1.36.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 75a64941..2075cdf2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.36.0", + "version": "1.36.1", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From 0bdb7d4a0ddf9354279f949b87d6b0393bb36544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B6=E6=9E=AB?= <7971419+crazyair@users.noreply.github.com> Date: Fri, 4 Aug 2023 10:06:56 +0800 Subject: [PATCH 44/65] fix: type is unknown (#609) * fix: type is unknown * feat: test --- src/namePathType.ts | 2 ++ tests/nameTypeCheck.test.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/namePathType.ts b/src/namePathType.ts index 67a0245f..8e54c6f1 100644 --- a/src/namePathType.ts +++ b/src/namePathType.ts @@ -19,6 +19,8 @@ export type DeepNamePath< ? // Connect path. e.g. { a: { b: string }[] } // Get: [a] | [ a,number] | [ a ,number , b] [...ParentNamePath, number] | DeepNamePath + : keyof Store extends never // unknown + ? Store : { // Convert `Store` to . We mark key a `FieldKey` [FieldKey in keyof Store]: Store[FieldKey] extends Function diff --git a/tests/nameTypeCheck.test.tsx b/tests/nameTypeCheck.test.tsx index 4f8a17bd..9179a9ac 100644 --- a/tests/nameTypeCheck.test.tsx +++ b/tests/nameTypeCheck.test.tsx @@ -89,6 +89,8 @@ describe('nameTypeCheck', () => { type t1 = NamePath<{ a: { b: string; func: Moment } }>; // Function type t2 = NamePath<{ a: { b: string; func: () => { c: string } } }>; + // known + const t3: NamePath = 'a'; interface Moment { func2: Function; From ce7b4b13c3f50aa093589970a9839fe415ff9ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 4 Aug 2023 10:08:47 +0800 Subject: [PATCH 45/65] 1.36.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2075cdf2..3ed7a8af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.36.1", + "version": "1.36.2", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From 7e6ce154782f37943e16915ba3c535a1d025340a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 8 Aug 2023 19:14:58 +0800 Subject: [PATCH 46/65] fix: loop logic --- src/useForm.ts | 19 +++-------------- src/utils/valueUtil.ts | 32 ++++++++++++++++++++++++----- tests/validate.test.tsx | 45 +++++++++++++++++++++++++++-------------- 3 files changed, 60 insertions(+), 36 deletions(-) diff --git a/src/useForm.ts b/src/useForm.ts index bb57a0f5..bbb76faf 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -885,27 +885,14 @@ export class FormStore { const TMP_SPLIT = String(Date.now()); const validateNamePathList = new Set(); + const recursive = options?.recursive; + this.getFieldEntities(true).forEach((field: FieldEntity) => { // Add field if not provide `nameList` if (!provideNameList) { namePathList.push(field.getNamePath()); } - /** - * Recursive validate if configured. - * TODO: perf improvement @zombieJ - */ - if (options?.recursive && provideNameList) { - const namePath = field.getNamePath(); - if ( - // nameList[i] === undefined 说明是以 nameList 开头的 - // ['name'] -> ['name','list'] - namePath.every((nameUnit, i) => nameList[i] === nameUnit || nameList[i] === undefined) - ) { - namePathList.push(namePath); - } - } - // Skip if without rule if (!field.props.rules || !field.props.rules.length) { return; @@ -915,7 +902,7 @@ export class FormStore { validateNamePathList.add(fieldNamePath.join(TMP_SPLIT)); // Add field validate rule in to promise list - if (!provideNameList || containsNamePath(namePathList, fieldNamePath)) { + if (!provideNameList || containsNamePath(namePathList, fieldNamePath, recursive)) { const promise = field.validateRules({ validateMessages: { ...defaultValidateMessages, diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 93a0f246..eb64966a 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -26,18 +26,40 @@ export function cloneByNamePathList(store: Store, namePathList: InternalNamePath return newStore; } -export function containsNamePath(namePathList: InternalNamePath[], namePath: InternalNamePath) { - return namePathList && namePathList.some(path => matchNamePath(path, namePath)); +/** + * Check if `namePathList` includes `namePath`. + * @param namePathList A list of `InternalNamePath[]` + * @param namePath Compare `InternalNamePath` + * @param partialMatch True will make `[a, b]` match `[a, b, c]` + */ +export function containsNamePath( + namePathList: InternalNamePath[], + namePath: InternalNamePath, + partialMatch = false, +) { + return namePathList && namePathList.some(path => matchNamePath(namePath, path, partialMatch)); } +/** + * Check if `namePath` is super set or equal of `subNamePath`. + * @param namePath A list of `InternalNamePath[]` + * @param subNamePath Compare `InternalNamePath` + * @param partialMatch True will make `[a, b]` match `[a, b, c]` + */ export function matchNamePath( namePath: InternalNamePath, - changedNamePath: InternalNamePath | null, + subNamePath: InternalNamePath | null, + partialMatch = false, ) { - if (!namePath || !changedNamePath || namePath.length !== changedNamePath.length) { + if (!namePath || !subNamePath) { return false; } - return namePath.every((nameUnit, i) => changedNamePath[i] === nameUnit); + + if (!partialMatch && namePath.length !== subNamePath.length) { + return false; + } + + return subNamePath.every((nameUnit, i) => namePath[i] === nameUnit); } // Like `shallowEqual`, but we not check the data which may cause re-render diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index 7c8fe212..4ca78bef 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { render } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import Form, { Field, useForm } from '../src'; @@ -694,8 +694,8 @@ describe('Form.Validate', () => { }); it('validate support recursive', async () => { - let form; - const wrapper = mount( + let form: FormInstance; + const { container } = render(
    { @@ -708,24 +708,39 @@ describe('Form.Validate', () => {
    , ); - wrapper - .find('input') - .at(0) - .simulate('change', { target: { value: '' } }); - await act(async () => { - await timeout(); - }); - wrapper.update(); + async function changeEmptyValue(input: HTMLElement) { + fireEvent.change(input, { + target: { + value: '2', + }, + }); + fireEvent.change(input, { + target: { + value: '', + }, + }); + + await act(async () => { + await timeout(); + }); + } + + await changeEmptyValue(container.querySelector('input')); try { - const values = await form.validateFields(['username'], { recursive: true }); - expect(values.username.do).toBe(''); + await form.validateFields([['username']], { recursive: true } as any); + + // Should not reach this + expect(false).toBeTruthy(); } catch (error) { expect(error.errorFields.length).toBe(2); + expect(error.errorFields[0].errors).toEqual(["'username.do' is required"]); + expect(error.errorFields[1].errors).toEqual(["'username.list' is required"]); } - const values = await form.validateFields(['username']); - expect(values.username.do).toBe(''); + await act(async () => { + await timeout(); + }); }); it('not trigger validator', async () => { From 0b33b0eea494762101dea85ceecd31c7d91700d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 8 Aug 2023 19:18:01 +0800 Subject: [PATCH 47/65] chore: update ts --- src/interface.ts | 10 +++++----- tests/validate.test.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/interface.ts b/src/interface.ts index d418188d..17c115f5 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -132,6 +132,11 @@ export interface ValidateOptions { * Validate only and not trigger UI and Field status update */ validateOnly?: boolean; + /** + * Recursive validate. It will validate all the name path that contains the provided one. + * e.g. [['a']] will validate ['a'] , ['a', 'b'] and ['a', 1]. + */ + recursive?: boolean; } export type ValidateFields = { @@ -142,11 +147,6 @@ export type ValidateFields = { export interface InternalValidateOptions extends ValidateOptions { triggerName?: string; validateMessages?: ValidateMessages; - /** - * Recursive validate. It will validate all the name path that contains the provided one. - * e.g. ['a'] will validate ['a'] , ['a', 'b'] and ['a', 1]. - */ - recursive?: boolean; } export type InternalValidateFields = { diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index 4ca78bef..b866f6f4 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -728,7 +728,7 @@ describe('Form.Validate', () => { await changeEmptyValue(container.querySelector('input')); try { - await form.validateFields([['username']], { recursive: true } as any); + await form.validateFields([['username']], { recursive: true }); // Should not reach this expect(false).toBeTruthy(); From 58787a478f4aa3663efef9e9e121aec98e0886f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 9 Aug 2023 14:53:45 +0800 Subject: [PATCH 48/65] test: add values check --- tests/validate.test.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index b866f6f4..0a06940d 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -708,7 +708,7 @@ describe('Form.Validate', () => {
    , ); - async function changeEmptyValue(input: HTMLElement) { + async function changeInputValue(input: HTMLElement, value = '') { fireEvent.change(input, { target: { value: '2', @@ -716,7 +716,7 @@ describe('Form.Validate', () => { }); fireEvent.change(input, { target: { - value: '', + value, }, }); @@ -725,7 +725,7 @@ describe('Form.Validate', () => { }); } - await changeEmptyValue(container.querySelector('input')); + await changeInputValue(container.querySelector('input')); try { await form.validateFields([['username']], { recursive: true }); @@ -741,6 +741,13 @@ describe('Form.Validate', () => { await act(async () => { await timeout(); }); + + // Passed + await changeInputValue(container.querySelectorAll('input')[0], 'do'); + await changeInputValue(container.querySelectorAll('input')[1], 'list'); + + const passedValues = await form.validateFields([['username']], { recursive: true }); + expect(passedValues).toEqual({ username: { do: 'do', list: 'list' } }); }); it('not trigger validator', async () => { From 188e23c8570ce1ef2eb5579159a319da545ec2ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 9 Aug 2023 15:30:37 +0800 Subject: [PATCH 49/65] 1.37.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ed7a8af..42677160 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.36.2", + "version": "1.37.0", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From 476604dbbffa32cd46b02a1c3b2fc704fbdb0a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Fri, 11 Aug 2023 14:12:33 +0800 Subject: [PATCH 50/65] chore: update to use rc-test & father@4 (#611) * chore: update rc-test * chore: update father * chore: update ci * test: part * test: part * test: part * test: part * test: part * chore: tmp * test: more * test: more * test: more * test: more * test: more * chore: tmp * chore: tmp * chore: clean up * test: more * chore: clean up * test: part * test: part * chore: clean up * chore: bump ci * chore: bump ci * chore: bump ci * docs: use dumi2 * chore: tmp file * chore: bump ci * chore: fix now * chore: bump co * chore: rm npm ci * test: rm umi test * chore: back of ci --- .dumirc.ts | 23 +++ .fatherrc.ts | 14 +- .github/workflows/main.yml | 2 +- .gitignore | 1 + .umirc.ts | 19 --- docs/demo/basic.md | 2 +- docs/demo/component.md | 2 +- docs/demo/context.md | 2 +- docs/demo/deps.md | 2 +- docs/demo/initialValues.md | 2 +- docs/demo/layout.md | 2 +- docs/demo/list.md | 2 +- docs/demo/preserve.md | 2 +- docs/demo/redux.md | 2 +- docs/demo/renderProps.md | 2 +- docs/demo/reset.md | 2 +- docs/demo/stateForm-list-draggable.md | 2 +- docs/demo/useForm.md | 2 +- docs/demo/useWatch-list.md | 2 +- docs/demo/useWatch.md | 2 +- docs/demo/validate-perf.md | 2 +- docs/demo/validate.md | 2 +- docs/demo/validateOnly.md | 2 +- jest.config.ts | 6 +- now.json | 2 +- package.json | 30 ++-- src/index.tsx | 4 +- src/interface.ts | 2 +- tests/common/InfoField.tsx | 2 +- tests/common/index.ts | 75 +++++---- tests/common/timeout.ts | 2 +- tests/context.test.tsx | 39 ++--- tests/control.test.tsx | 9 +- tests/dependencies.test.tsx | 52 +++--- tests/field.test.tsx | 29 +++- tests/index.test.tsx | 168 ++++++++++--------- tests/initialValue.test.tsx | 127 +++++++------- tests/legacy/async-validation.test.tsx | 28 ++-- tests/legacy/basic-form.test.tsx | 20 +-- tests/legacy/dynamic-rule.test.tsx | 37 ++--- tests/legacy/field-props.test.tsx | 22 +-- tests/list.test.tsx | 205 +++++++++-------------- tests/strict.test.tsx | 8 +- tests/useWatch.test.tsx | 97 ++++++----- tests/validate-warning.test.tsx | 16 +- tests/validate.test.tsx | 221 +++++++++++++------------ 46 files changed, 662 insertions(+), 634 deletions(-) create mode 100644 .dumirc.ts delete mode 100644 .umirc.ts diff --git a/.dumirc.ts b/.dumirc.ts new file mode 100644 index 00000000..d94ccdc4 --- /dev/null +++ b/.dumirc.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'dumi'; +import path from 'path'; + +const isProdSite = + // 不是预览模式 同时是生产环境 + process.env.PREVIEW !== 'true' && process.env.NODE_ENV === 'production'; + +const name = 'field-form'; + +export default defineConfig({ + alias: { + 'rc-field-form$': path.resolve('src'), + 'rc-field-form/es': path.resolve('src'), + }, + mfsu: false, + favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'], + themeConfig: { + name: 'FieldForm', + logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', + }, + base: isProdSite ? `/${name}/` : '/', + publicPath: isProdSite ? `/${name}/` : '/', +}); \ No newline at end of file diff --git a/.fatherrc.ts b/.fatherrc.ts index d02c60aa..96268ae1 100644 --- a/.fatherrc.ts +++ b/.fatherrc.ts @@ -1,9 +1,5 @@ -export default { - cjs: 'babel', - esm: { type: 'babel', importLibToEs: true }, - runtimeHelpers: true, - preCommit: { - eslint: true, - prettier: true, - }, -}; +import { defineConfig } from 'father'; + +export default defineConfig({ + plugins: ['@rc-component/father-plugin'], +}); diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3571ed27..5839a123 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/setup-node@v1 with: - node-version: '12' + node-version: '16' - name: cache package-lock.json uses: actions/cache@v2 diff --git a/.gitignore b/.gitignore index 38e890c0..cee8485a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ .umi-production .umi-test .env.local +.dumi/ \ No newline at end of file diff --git a/.umirc.ts b/.umirc.ts deleted file mode 100644 index 88ff7af0..00000000 --- a/.umirc.ts +++ /dev/null @@ -1,19 +0,0 @@ -// more config: https://d.umijs.org/config -import { defineConfig } from 'dumi'; - -export default defineConfig({ - title: 'rc-field-form', - favicon: - 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', - logo: - 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', - outputPath: '.doc', - exportStatic: {}, - styles: [ - ` - .markdown table { - width: auto !important; - } - `, - ] -}); diff --git a/docs/demo/basic.md b/docs/demo/basic.md index ad6f032c..04e2e0e8 100644 --- a/docs/demo/basic.md +++ b/docs/demo/basic.md @@ -1,4 +1,4 @@ ## basic - + diff --git a/docs/demo/component.md b/docs/demo/component.md index 144e3d79..5b085d7a 100644 --- a/docs/demo/component.md +++ b/docs/demo/component.md @@ -1,3 +1,3 @@ ## component - + diff --git a/docs/demo/context.md b/docs/demo/context.md index 91ad6f99..c18805bf 100644 --- a/docs/demo/context.md +++ b/docs/demo/context.md @@ -1,3 +1,3 @@ ## context - + diff --git a/docs/demo/deps.md b/docs/demo/deps.md index 8bce3c21..2233a9fa 100644 --- a/docs/demo/deps.md +++ b/docs/demo/deps.md @@ -1,4 +1,4 @@ ## deps - + diff --git a/docs/demo/initialValues.md b/docs/demo/initialValues.md index 575c4ca9..671db850 100644 --- a/docs/demo/initialValues.md +++ b/docs/demo/initialValues.md @@ -1,4 +1,4 @@ ## initialValues - + diff --git a/docs/demo/layout.md b/docs/demo/layout.md index 3ea2171a..447af4d9 100644 --- a/docs/demo/layout.md +++ b/docs/demo/layout.md @@ -1,3 +1,3 @@ ## layout - + diff --git a/docs/demo/list.md b/docs/demo/list.md index c2bbd28c..4d7b093f 100644 --- a/docs/demo/list.md +++ b/docs/demo/list.md @@ -1,3 +1,3 @@ ## list - + diff --git a/docs/demo/preserve.md b/docs/demo/preserve.md index 36d3c2c3..655b30c1 100644 --- a/docs/demo/preserve.md +++ b/docs/demo/preserve.md @@ -1,4 +1,4 @@ ## preserve - + diff --git a/docs/demo/redux.md b/docs/demo/redux.md index ff6243b1..b7cc731f 100644 --- a/docs/demo/redux.md +++ b/docs/demo/redux.md @@ -1,3 +1,3 @@ ## redux - + diff --git a/docs/demo/renderProps.md b/docs/demo/renderProps.md index a7955e94..497cb2e9 100644 --- a/docs/demo/renderProps.md +++ b/docs/demo/renderProps.md @@ -1,3 +1,3 @@ ## renderProps - + diff --git a/docs/demo/reset.md b/docs/demo/reset.md index 90bbf4d7..c95458fb 100644 --- a/docs/demo/reset.md +++ b/docs/demo/reset.md @@ -1,3 +1,3 @@ ## reset - + diff --git a/docs/demo/stateForm-list-draggable.md b/docs/demo/stateForm-list-draggable.md index e376d85e..73402537 100644 --- a/docs/demo/stateForm-list-draggable.md +++ b/docs/demo/stateForm-list-draggable.md @@ -1,3 +1,3 @@ ## stateForm-list-draggable - + diff --git a/docs/demo/useForm.md b/docs/demo/useForm.md index 757af1e7..d3a61cfc 100644 --- a/docs/demo/useForm.md +++ b/docs/demo/useForm.md @@ -1,3 +1,3 @@ ## useForm - + diff --git a/docs/demo/useWatch-list.md b/docs/demo/useWatch-list.md index 1a14f737..b118b10a 100644 --- a/docs/demo/useWatch-list.md +++ b/docs/demo/useWatch-list.md @@ -1,3 +1,3 @@ ## useWatch-list - + diff --git a/docs/demo/useWatch.md b/docs/demo/useWatch.md index 04e70536..96b3ab1d 100644 --- a/docs/demo/useWatch.md +++ b/docs/demo/useWatch.md @@ -1,3 +1,3 @@ ## useWatch - + diff --git a/docs/demo/validate-perf.md b/docs/demo/validate-perf.md index c99a0ab1..fcec6883 100644 --- a/docs/demo/validate-perf.md +++ b/docs/demo/validate-perf.md @@ -1,3 +1,3 @@ ## validate-perf - + diff --git a/docs/demo/validate.md b/docs/demo/validate.md index ab665b6c..4bab53f2 100644 --- a/docs/demo/validate.md +++ b/docs/demo/validate.md @@ -1,3 +1,3 @@ ## validate - + diff --git a/docs/demo/validateOnly.md b/docs/demo/validateOnly.md index 2be92f39..4b1aaf29 100644 --- a/docs/demo/validateOnly.md +++ b/docs/demo/validateOnly.md @@ -1,3 +1,3 @@ ## validateOnly - + diff --git a/jest.config.ts b/jest.config.ts index 8d21bee3..84f2eee7 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,7 +1,3 @@ -import type { Config } from 'jest'; - -const config: Config = { +export default { setupFilesAfterEnv: ['/tests/setupAfterEnv.ts'], }; - -export default config; diff --git a/now.json b/now.json index a0d5ebdf..17cb30ca 100644 --- a/now.json +++ b/now.json @@ -5,7 +5,7 @@ { "src": "package.json", "use": "@now/static-build", - "config": { "distDir": ".doc" } + "config": { "distDir": "dist" } } ], "routes": [ diff --git a/package.json b/package.json index 42677160..e2e6afe9 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,10 @@ "start": "dumi dev", "docs:build": "dumi build", "docs:deploy": "gh-pages -d docs-dist", - "compile": "father-build", + "compile": "father build", "deploy": "npm run docs:build && npm run docs:deploy", "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"", - "test": "father test", + "test": "rc-test", "test:coverage": "umi-test --coverage", "prepublishOnly": "npm run compile && np --no-cleanup --yolo --no-publish", "lint": "eslint src/ --ext .tsx,.ts", @@ -54,32 +54,28 @@ "rc-util": "^5.32.2" }, "devDependencies": { - "@testing-library/jest-dom": "^5.16.4", - "@testing-library/react": "^12.1.5", - "@types/enzyme": "^3.10.5", + "@rc-component/father-plugin": "^1.0.0", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^14.0.0", "@types/jest": "^29.2.5", "@types/lodash": "^4.14.135", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@umijs/fabric": "^2.5.2", - "@umijs/test": "^3.2.27", - "dumi": "^1.1.0", - "enzyme": "^3.1.0", - "enzyme-adapter-react-16": "^1.0.2", - "enzyme-to-json": "^3.1.4", + "dumi": "^2.0.0", "eslint": "^7.18.0", - "father": "^2.13.6", - "father-build": "^1.18.6", + "father": "^4.0.0", "gh-pages": "^3.1.0", - "jest": "^29.3.1", + "jest": "^29.0.0", "np": "^5.0.3", "prettier": "^2.1.2", - "react": "^16.14.0", + "rc-test": "^7.0.15", + "react": "^18.0.0", "react-dnd": "^8.0.3", "react-dnd-html5-backend": "^8.0.3", - "react-dom": "^16.14.0", - "react-redux": "^4.4.10", - "redux": "^3.7.2", + "react-dom": "^18.0.0", + "react-redux": "^8.1.2", + "redux": "^4.2.1", "typescript": "^5.1.6" } } diff --git a/src/index.tsx b/src/index.tsx index a726fb8f..3c6c6e5d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -31,8 +31,8 @@ RefForm.List = List; RefForm.useForm = useForm; RefForm.useWatch = useWatch; -export { FormInstance, Field, List, useForm, FormProvider, FieldContext, ListContext, useWatch }; +export { Field, List, useForm, FormProvider, FieldContext, ListContext, useWatch }; -export type { FormProps }; +export type { FormProps, FormInstance }; export default RefForm; diff --git a/src/interface.ts b/src/interface.ts index 17c115f5..a332ed20 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -260,7 +260,7 @@ export interface FormInstance { ((allFieldsTouched?: boolean) => boolean); isFieldTouched: (name: NamePath) => boolean; isFieldValidating: (name: NamePath) => boolean; - isFieldsValidating: (nameList: NamePath[]) => boolean; + isFieldsValidating: (nameList?: NamePath[]) => boolean; resetFields: (fields?: NamePath[]) => void; setFields: (fields: FieldData[]) => void; setFieldValue: (name: NamePath, value: any) => void; diff --git a/tests/common/InfoField.tsx b/tests/common/InfoField.tsx index 6aa48051..e4faa52f 100644 --- a/tests/common/InfoField.tsx +++ b/tests/common/InfoField.tsx @@ -19,7 +19,7 @@ const InfoField: React.FC = ({ children, ...props }) => ( const { errors, warnings, validating } = info; return ( -
    +
    {children ? React.cloneElement(children, control) : }
      {errors.map((error, index) => ( diff --git a/tests/common/index.ts b/tests/common/index.ts index 23af21c2..36241a61 100644 --- a/tests/common/index.ts +++ b/tests/common/index.ts @@ -1,59 +1,72 @@ import { act } from 'react-dom/test-utils'; -import type { ReactWrapper } from 'enzyme'; import timeout from './timeout'; -import { Field } from '../../src'; -import { getNamePath, matchNamePath } from '../../src/utils/valueUtil'; +import { matchNamePath } from '../../src/utils/valueUtil'; +import { fireEvent } from '@testing-library/react'; -export async function changeValue(wrapper: ReactWrapper, value: string | string[]) { - wrapper.find('input').simulate('change', { target: { value } }); - await act(async () => { - await timeout(); - }); - wrapper.update(); +export function getInput( + container: HTMLElement, + dataNameOrIndex?: string | number, + parentField = false, +): HTMLInputElement { + let ele: HTMLInputElement | null = null; + + if (!dataNameOrIndex) { + ele = container.querySelector('input'); + } else if (typeof dataNameOrIndex === 'number') { + ele = container.querySelectorAll('input')[dataNameOrIndex]; + } else { + ele = container.querySelector(`[data-name="${dataNameOrIndex}"]`); + } + + if (parentField) { + return ele.closest('.field'); + } + + return ele!; +} + +export async function changeValue(wrapper: HTMLElement, value: string | string[]) { + const values = Array.isArray(value) ? value : [value]; + + for (let i = 0; i < values.length; i += 1) { + fireEvent.change(wrapper, { target: { value: values[i] } }); + + await act(async () => { + await timeout(); + }); + } + + return; } export function matchError( - wrapper: ReactWrapper, + wrapper: HTMLElement, error?: boolean | string, warning?: boolean | string, ) { // Error if (error) { - expect(wrapper.find('.errors li').length).toBeTruthy(); + expect(wrapper.querySelector('.errors li')).toBeTruthy(); } else { - expect(wrapper.find('.errors li').length).toBeFalsy(); + expect(wrapper.querySelector('.errors li')).toBeFalsy(); } if (error && typeof error !== 'boolean') { - expect(wrapper.find('.errors li').text()).toBe(error); + expect(wrapper.querySelector('.errors li').textContent).toBe(error); } // Warning if (warning) { - expect(wrapper.find('.warnings li').length).toBeTruthy(); + expect(wrapper.querySelector('.warnings li')).toBeTruthy(); } else { - expect(wrapper.find('.warnings li').length).toBeFalsy(); + expect(wrapper.querySelector('.warnings li')).toBeFalsy(); } if (warning && typeof warning !== 'boolean') { - expect(wrapper.find('.warnings li').text()).toBe(warning); + expect(wrapper.querySelector('.warnings li').textContent).toBe(warning); } -} -export function getField(wrapper: ReactWrapper, index: string | number | string[] = 0) { - if (typeof index === 'number') { - return wrapper.find(Field).at(index); - } - const name = getNamePath(index); - const fields = wrapper.find(Field); - for (let i = 0; i < fields.length; i += 1) { - const field = fields.at(i); - const fieldName = getNamePath((field.props() as any).name); - if (matchNamePath(name, fieldName)) { - return field; - } - } - return null; + return; } export function matchArray(source: any[], target: any[], matchKey: React.Key) { diff --git a/tests/common/timeout.ts b/tests/common/timeout.ts index a688bcf9..b3b7d911 100644 --- a/tests/common/timeout.ts +++ b/tests/common/timeout.ts @@ -1,4 +1,4 @@ -export default (timeout: number = 0) => { +export default async (timeout: number = 10) => { return new Promise(resolve => { setTimeout(resolve, timeout); }); diff --git a/tests/context.test.tsx b/tests/context.test.tsx index 6481891b..4a587c9f 100644 --- a/tests/context.test.tsx +++ b/tests/context.test.tsx @@ -1,15 +1,14 @@ import React from 'react'; -import { mount } from 'enzyme'; import { render } from '@testing-library/react'; import type { FormInstance } from '../src'; import Form, { FormProvider } from '../src'; import InfoField from './common/InfoField'; -import { changeValue, matchError, getField } from './common'; +import { changeValue, matchError, getInput } from './common'; import timeout from './common/timeout'; describe('Form.Context', () => { it('validateMessages', async () => { - const wrapper = mount( + const { container } = render( @@ -17,14 +16,14 @@ describe('Form.Context', () => { , ); - await changeValue(wrapper, ''); - matchError(wrapper, "I'm global"); + await changeValue(getInput(container), ['bamboo', '']); + matchError(container, "I'm global"); }); it('change event', async () => { const onFormChange = jest.fn(); - const wrapper = mount( + const { container } = render( @@ -32,7 +31,7 @@ describe('Form.Context', () => { , ); - await changeValue(getField(wrapper), 'Light'); + await changeValue(getInput(container), 'Light'); expect(onFormChange).toHaveBeenCalledWith( 'form1', expect.objectContaining({ @@ -77,21 +76,21 @@ describe('Form.Context', () => { it('basic', async () => { const onFormChange = jest.fn(); - const wrapper = mount( + const { container, rerender } = render( , ); - wrapper.setProps({ - children: ( + rerender( + - ), - }); + , + ); - await changeValue(getField(wrapper), 'Bamboo'); + await changeValue(getInput(container), 'Bamboo'); const { forms } = onFormChange.mock.calls[0][1]; expect(Object.keys(forms)).toEqual(['form2']); }); @@ -113,13 +112,11 @@ describe('Form.Context', () => { ); - const wrapper = mount(); + const { container, rerender } = render(); - wrapper.setProps({ - changed: true, - }); + rerender(); - await changeValue(getField(wrapper), 'Bamboo'); + await changeValue(getInput(container), 'Bamboo'); const { forms } = onFormChange.mock.calls[0][1]; expect(Object.keys(forms)).toEqual(['form2']); }); @@ -129,7 +126,7 @@ describe('Form.Context', () => { const onFormFinish = jest.fn(); const form = React.createRef(); - const wrapper = mount( + const { container } = render(
      @@ -140,12 +137,12 @@ describe('Form.Context', () => {
      , ); - await changeValue(getField(wrapper), ''); + await changeValue(getInput(container), ['bamboo', '']); form.current?.submit(); await timeout(); expect(onFormFinish).not.toHaveBeenCalled(); - await changeValue(getField(wrapper), 'Light'); + await changeValue(getInput(container), 'Light'); form.current?.submit(); await timeout(); expect(onFormFinish).toHaveBeenCalled(); diff --git a/tests/control.test.tsx b/tests/control.test.tsx index d7575217..88614292 100644 --- a/tests/control.test.tsx +++ b/tests/control.test.tsx @@ -1,8 +1,7 @@ import React from 'react'; -import { mount } from 'enzyme'; import Form from '../src'; import InfoField from './common/InfoField'; -import { changeValue, matchError } from './common'; +import { changeValue, getInput, matchError } from './common'; import { render } from '@testing-library/react'; describe('Form.Control', () => { @@ -36,9 +35,9 @@ describe('Form.Control', () => { ); }; - const wrapper = mount(); + const { container } = render(); - await changeValue(wrapper, ''); - matchError(wrapper, "'test' is required"); + await changeValue(getInput(container), ['bamboo', '']); + matchError(container, "'test' is required"); }); }); diff --git a/tests/dependencies.test.tsx b/tests/dependencies.test.tsx index c00e2f4b..82d986a3 100644 --- a/tests/dependencies.test.tsx +++ b/tests/dependencies.test.tsx @@ -1,16 +1,16 @@ import React from 'react'; -import { mount } from 'enzyme'; import type { FormInstance } from '../src'; import Form, { Field } from '../src'; import timeout from './common/timeout'; import InfoField, { Input } from './common/InfoField'; -import { changeValue, matchError, getField } from './common'; +import { changeValue, matchError, getInput } from './common'; +import { fireEvent, render } from '@testing-library/react'; describe('Form.Dependencies', () => { it('touched', async () => { const form = React.createRef(); - const wrapper = mount( + const { container } = render(
      @@ -20,13 +20,13 @@ describe('Form.Dependencies', () => { ); // Not trigger if not touched - await changeValue(getField(wrapper, 0), ''); - matchError(getField(wrapper, 1), false); + await changeValue(getInput(container, 0), ['bamboo', '']); + matchError(getInput(container, 1, true), false); // Trigger if touched form.current?.setFields([{ name: 'field_2', touched: true }]); - await changeValue(getField(wrapper, 0), ''); - matchError(getField(wrapper, 1), true); + await changeValue(getInput(container, 0), ['bamboo', '']); + matchError(getInput(container, 1, true), true); }); describe('initialValue', () => { @@ -34,7 +34,7 @@ describe('Form.Dependencies', () => { it(name, async () => { let validated = false; - const wrapper = mount( + const { container } = render(
      @@ -55,7 +55,7 @@ describe('Form.Dependencies', () => { ); // Not trigger if not touched - await changeValue(getField(wrapper, 0), ''); + await changeValue(getInput(container, 0), 'bamboo'); expect(validated).toBeTruthy(); }); } @@ -68,7 +68,7 @@ describe('Form.Dependencies', () => { const form = React.createRef(); let rendered = false; - const wrapper = mount( + const { container } = render(
      @@ -94,7 +94,7 @@ describe('Form.Dependencies', () => { ]); rendered = false; - await changeValue(getField(wrapper), '1'); + await changeValue(getInput(container), '1'); expect(rendered).toBeTruthy(); }); @@ -102,7 +102,7 @@ describe('Form.Dependencies', () => { it('should work when field is dirty', async () => { let pass = false; - const wrapper = mount( + const { container } = render( { , ); - wrapper.find('form').simulate('submit'); + fireEvent.submit(container.querySelector('form')!); await timeout(); - wrapper.update(); - matchError(getField(wrapper, 0), 'You should not pass'); + // wrapper.update(); + matchError(getInput(container, 0, true), 'You should not pass'); // Mock new validate pass = true; - await changeValue(getField(wrapper, 1), 'bamboo'); - matchError(getField(wrapper, 0), false); + await changeValue(getInput(container, 1), 'bamboo'); + matchError(getInput(container, 0, true), false); // Should not validate after reset pass = false; - wrapper.find('button').simulate('click'); - await changeValue(getField(wrapper, 1), 'light'); - matchError(getField(wrapper, 0), false); + fireEvent.click(container.querySelector('button')!); + await changeValue(getInput(container, 1), 'light'); + matchError(getInput(container, 0, true), false); }); it('should work as a shortcut when name is not provided', async () => { const spy = jest.fn(); - const wrapper = mount( + const { container } = render(
      {() => { @@ -170,7 +170,7 @@ describe('Form.Dependencies', () => { , ); expect(spy).toHaveBeenCalledTimes(1); - await changeValue(getField(wrapper, 2), 'value2'); + await changeValue(getInput(container, 1), 'value2'); // sync start // valueUpdate -> not rerender // depsUpdate -> not rerender @@ -179,7 +179,7 @@ describe('Form.Dependencies', () => { // validateFinish -> not rerender // async end expect(spy).toHaveBeenCalledTimes(1); - await changeValue(getField(wrapper, 1), 'value1'); + await changeValue(getInput(container, 0), 'value1'); // sync start // valueUpdate -> not rerender // depsUpdate -> rerender by deps @@ -193,7 +193,7 @@ describe('Form.Dependencies', () => { it("shouldn't work when shouldUpdate is set", async () => { const spy = jest.fn(); - const wrapper = mount( + const { container } = render(
      true}> {() => { @@ -210,7 +210,7 @@ describe('Form.Dependencies', () => { , ); expect(spy).toHaveBeenCalledTimes(1); - await changeValue(getField(wrapper, 1), 'value1'); + await changeValue(getInput(container, 0), 'value1'); // sync start // valueUpdate -> rerender by shouldUpdate // depsUpdate -> rerender by deps @@ -218,7 +218,7 @@ describe('Form.Dependencies', () => { // sync end expect(spy).toHaveBeenCalledTimes(2); - await changeValue(getField(wrapper, 2), 'value2'); + await changeValue(getInput(container, 1), 'value2'); // sync start // valueUpdate -> rerender by shouldUpdate // depsUpdate -> rerender by deps diff --git a/tests/field.test.tsx b/tests/field.test.tsx index 5cdf9a54..737c345d 100644 --- a/tests/field.test.tsx +++ b/tests/field.test.tsx @@ -1,11 +1,15 @@ import React from 'react'; -import { mount } from 'enzyme'; import Form, { Field } from '../src'; +import type { FormInstance } from '../src'; +import { render } from '@testing-library/react'; describe('Form.Field', () => { it('field remount should trigger constructor again', () => { + let formRef: FormInstance; + const Demo = ({ visible }: { visible: boolean }) => { const [form] = Form.useForm(); + formRef = form; const fieldNode = ; @@ -13,17 +17,24 @@ describe('Form.Field', () => { }; // First mount - const wrapper = mount(); - const instance = wrapper.find('Field').instance() as any; - expect(instance.cancelRegisterFunc).toBeTruthy(); + const { rerender } = render(); + + // ZombieJ: testing lib can not access instance + // const instance = wrapper.find('Field').instance() as any; + // expect(instance.cancelRegisterFunc).toBeTruthy(); + expect(formRef.getFieldsValue()).toEqual({ light: 'bamboo' }); // Hide - wrapper.setProps({ visible: false }); - expect(instance.cancelRegisterFunc).toBeFalsy(); + // wrapper.setProps({ visible: false }); + // expect(instance.cancelRegisterFunc).toBeFalsy(); + rerender(); + expect(formRef.getFieldsValue()).toEqual({}); // Mount again - wrapper.setProps({ visible: true }); - expect(instance.cancelRegisterFunc).toBeFalsy(); - expect((wrapper.find('Field').instance() as any).cancelRegisterFunc).toBeTruthy(); + // wrapper.setProps({ visible: true }); + rerender(); + // expect(instance.cancelRegisterFunc).toBeFalsy(); + // expect((wrapper.find('Field').instance() as any).cancelRegisterFunc).toBeTruthy(); + expect(formRef.getFieldsValue()).toEqual({ light: 'bamboo' }); }); }); diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 3c21133d..00870a3f 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -1,10 +1,9 @@ -import { mount } from 'enzyme'; -import { fireEvent, render } from '@testing-library/react'; +import { act, fireEvent, render } from '@testing-library/react'; import { resetWarned } from 'rc-util/lib/warning'; import React from 'react'; import type { FormInstance } from '../src'; import Form, { Field, useForm } from '../src'; -import { changeValue, getField, matchError } from './common'; +import { changeValue, getInput, matchError } from './common'; import InfoField, { Input } from './common/InfoField'; import timeout from './common/timeout'; import type { Meta } from '@/interface'; @@ -88,7 +87,7 @@ describe('Form.Basic', () => { it('fields touched', async () => { const form = React.createRef(); - const wrapper = mount( + const { container } = render(
      @@ -101,13 +100,13 @@ describe('Form.Basic', () => { expect(form.current?.isFieldsTouched()).toBeFalsy(); expect(form.current?.isFieldsTouched(['username', 'password'])).toBeFalsy(); - await changeValue(getField(wrapper, 0), 'Bamboo'); + await changeValue(getInput(container, 0), 'Bamboo'); expect(form.current?.isFieldsTouched()).toBeTruthy(); expect(form.current?.isFieldsTouched(['username', 'password'])).toBeTruthy(); expect(form.current?.isFieldsTouched(true)).toBeFalsy(); expect(form.current?.isFieldsTouched(['username', 'password'], true)).toBeFalsy(); - await changeValue(getField(wrapper, 1), 'Light'); + await changeValue(getInput(container, 1), 'Light'); expect(form.current?.isFieldsTouched()).toBeTruthy(); expect(form.current?.isFieldsTouched(['username', 'password'])).toBeTruthy(); expect(form.current?.isFieldsTouched(true)).toBeTruthy(); @@ -121,7 +120,7 @@ describe('Form.Basic', () => { const onReset = jest.fn(); const onMeta = jest.fn(); - const wrapper = mount( + const { container } = render(
      { onReset={onReset} onMetaChange={onMeta} > - +
      , ); - await changeValue(getField(wrapper, 'username'), 'Bamboo'); + await changeValue(getInput(container, 'username'), 'Bamboo'); expect(form.current?.getFieldValue('username')).toEqual('Bamboo'); expect(form.current?.getFieldError('username')).toEqual([]); expect(form.current?.isFieldTouched('username')).toBeTruthy(); @@ -158,7 +157,7 @@ describe('Form.Basic', () => { onMeta.mockRestore(); onReset.mockRestore(); - await changeValue(getField(wrapper, 'username'), ''); + await changeValue(getInput(container, 'username'), ''); expect(form.current?.getFieldValue('username')).toEqual(''); expect(form.current?.getFieldError('username')).toEqual(["'username' is required"]); expect(form.current?.isFieldTouched('username')).toBeTruthy(); @@ -190,22 +189,23 @@ describe('Form.Basic', () => { it('not affect others', async () => { const form = React.createRef(); - const wrapper = mount( + const { container } = render(
      - + - +
      , ); - await changeValue(getField(wrapper, 'username'), 'Bamboo'); - await changeValue(getField(wrapper, 'password'), ''); + await changeValue(getInput(container, 'username'), 'Bamboo'); + await changeValue(getInput(container, 'password'), ['bamboo', '']); + form.current?.resetFields(['username']); expect(form.current?.getFieldValue('username')).toEqual(undefined); @@ -249,7 +249,7 @@ describe('Form.Basic', () => { it('keep origin input function', async () => { const onChange = jest.fn(); const onValuesChange = jest.fn(); - const wrapper = mount( + const { container } = render(
      @@ -257,9 +257,9 @@ describe('Form.Basic', () => { , ); - await changeValue(getField(wrapper), 'Bamboo'); + await changeValue(getInput(container), 'Bamboo'); expect(onValuesChange).toHaveBeenCalledWith({ username: 'Bamboo' }, { username: 'Bamboo' }); - expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ target: { value: 'Bamboo' } })); + expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ target: getInput(container) })); }); it('onValuesChange should not return fully value', async () => { @@ -269,30 +269,31 @@ describe('Form.Basic', () => {
      {showField && ( - + )} - +
      ); - const wrapper = mount(); - await changeValue(getField(wrapper, 'bamboo'), 'cute'); + const { container, rerender } = render(); + await changeValue(getInput(container, 'bamboo'), 'cute'); expect(onValuesChange).toHaveBeenCalledWith(expect.anything(), { light: 'little', bamboo: 'cute', }); onValuesChange.mockReset(); - wrapper.setProps({ showField: false }); - await changeValue(getField(wrapper, 'bamboo'), 'beauty'); + // wrapper.setProps({ showField: false }); + rerender(); + await changeValue(getInput(container, 'bamboo'), 'beauty'); expect(onValuesChange).toHaveBeenCalledWith(expect.anything(), { bamboo: 'beauty' }); }); it('should call onReset fn, when the button is clicked', async () => { const resetFn = jest.fn(); - const wrapper = mount( + const { container } = render(
      @@ -300,31 +301,29 @@ describe('Form.Basic', () => { , ); - await changeValue(getField(wrapper), 'Bamboo'); - wrapper.find('button').simulate('reset'); + await changeValue(getInput(container), 'Bamboo'); + fireEvent.reset(container.querySelector('form')); await timeout(); expect(resetFn).toHaveBeenCalledTimes(1); - const { value } = wrapper.find('input').props(); - expect(value).toEqual(''); + expect(getInput(container).value).toEqual(''); }); it('submit', async () => { const onFinish = jest.fn(); const onFinishFailed = jest.fn(); - const wrapper = mount( + const { container } = render(
      -
      , ); // Not trigger - wrapper.find('button').simulate('submit'); + fireEvent.submit(container.querySelector('form')); await timeout(); - wrapper.update(); - matchError(wrapper, "'user' is required"); + console.log(container.innerHTML); + matchError(container, "'user' is required"); expect(onFinish).not.toHaveBeenCalled(); expect(onFinishFailed).toHaveBeenCalledWith({ errorFields: [{ name: ['user'], errors: ["'user' is required"], warnings: [] }], @@ -336,10 +335,10 @@ describe('Form.Basic', () => { onFinishFailed.mockReset(); // Trigger - await changeValue(getField(wrapper), 'Bamboo'); - wrapper.find('button').simulate('submit'); + await changeValue(getInput(container), 'Bamboo'); + fireEvent.submit(container.querySelector('form')); await timeout(); - matchError(wrapper, false); + matchError(container, false); expect(onFinish).toHaveBeenCalledWith({ user: 'Bamboo' }); expect(onFinishFailed).not.toHaveBeenCalled(); }); @@ -365,7 +364,7 @@ describe('Form.Basic', () => { it('valuePropName', async () => { const form = React.createRef(); - const wrapper = mount( + const { container } = render(
      @@ -375,27 +374,30 @@ describe('Form.Basic', () => {
      , ); - wrapper.find('input[type="checkbox"]').simulate('change', { target: { checked: true } }); + // wrapper.find('input[type="checkbox"]').simulate('change', { target: { checked: true } }); + fireEvent.click(container.querySelector('input[type="checkbox"]')); await timeout(); expect(form.current?.getFieldsValue()).toEqual({ check: true }); - wrapper.find('input[type="checkbox"]').simulate('change', { target: { checked: false } }); + // wrapper.find('input[type="checkbox"]').simulate('change', { target: { checked: false } }); + fireEvent.click(container.querySelector('input[type="checkbox"]')); await timeout(); expect(form.current?.getFieldsValue()).toEqual({ check: false }); }); it('getValueProps', async () => { - const wrapper = mount( + const { container } = render(
      - ({ light: val })}> + ({ 'data-light': val })}>
      , ); - expect((wrapper.find('.anything').props() as any).light).toEqual('bamboo'); + // expect((container.querySelector('.anything').props() as any).light).toEqual('bamboo'); + expect(container.querySelector('.anything')).toHaveAttribute('data-light', 'bamboo'); }); describe('shouldUpdate', () => { @@ -403,13 +405,13 @@ describe('Form.Basic', () => { let isAllTouched: boolean; let hasError: number; - const wrapper = mount( + const { container } = render(
      - + - + {(_, __, { getFieldsError, isFieldsTouched }) => { @@ -422,19 +424,19 @@ describe('Form.Basic', () => { , ); - await changeValue(getField(wrapper, 'username'), ''); + await changeValue(getInput(container, 'username'), ['bamboo', '']); expect(isAllTouched).toBeFalsy(); expect(hasError).toBeTruthy(); - await changeValue(getField(wrapper, 'username'), 'Bamboo'); + await changeValue(getInput(container, 'username'), 'Bamboo'); expect(isAllTouched).toBeFalsy(); expect(hasError).toBeFalsy(); - await changeValue(getField(wrapper, 'password'), 'Light'); + await changeValue(getInput(container, 'password'), 'Light'); expect(isAllTouched).toBeTruthy(); expect(hasError).toBeFalsy(); - await changeValue(getField(wrapper, 'password'), ''); + await changeValue(getInput(container, 'password'), ''); expect(isAllTouched).toBeTruthy(); expect(hasError).toBeTruthy(); }); @@ -442,7 +444,7 @@ describe('Form.Basic', () => { it('true will force one more update', async () => { let renderPhase = 0; - const wrapper = mount( + const { container } = render(
      @@ -453,8 +455,8 @@ describe('Form.Basic', () => { return ( ); }} @@ -462,17 +464,22 @@ describe('Form.Basic', () => { , ); - const props = wrapper.find('#holder').props(); + // const props = wrapper.find('#holder').props(); expect(renderPhase).toEqual(2); - expect(props['data-touched']).toBeFalsy(); - expect(props['data-value']).toEqual({ username: 'light' }); + // expect(props['data-touched']).toBeFalsy(); + // expect(props['data-value']).toEqual({ username: 'light' }); + expect(container.querySelector('#holder')).toHaveAttribute('data-touched', 'false'); + expect(container.querySelector('#holder')).toHaveAttribute( + 'data-value', + '{"username":"light"}', + ); }); }); describe('setFields', () => { it('should work', () => { const form = React.createRef(); - const wrapper = mount( + const { container } = render(
      @@ -482,13 +489,14 @@ describe('Form.Basic', () => {
      , ); - form.current?.setFields([ - { name: 'username', touched: false, validating: true, errors: ['Set It!'] }, - ]); - wrapper.update(); + act(() => { + form.current?.setFields([ + { name: 'username', touched: false, validating: true, errors: ['Set It!'] }, + ]); + }); - matchError(wrapper, 'Set It!'); - expect(wrapper.find('.validating').length).toBeTruthy(); + matchError(container, 'Set It!'); + expect(container.querySelector('.validating')).toBeTruthy(); expect(form.current?.isFieldsTouched()).toBeFalsy(); }); @@ -512,12 +520,16 @@ describe('Form.Basic', () => { triggerUpdate.mockReset(); // Not trigger render - formRef.current.setFields([{ name: 'others', value: 'no need to update' }]); + act(() => { + formRef.current.setFields([{ name: 'others', value: 'no need to update' }]); + }); expect(triggerUpdate).not.toHaveBeenCalled(); // Trigger render - formRef.current.setFields([{ name: 'value', value: 'should update' }]); + act(() => { + formRef.current.setFields([{ name: 'value', value: 'should update' }]); + }); expect(triggerUpdate).toHaveBeenCalled(); }); @@ -554,7 +566,7 @@ describe('Form.Basic', () => { const form = React.createRef(); let currentMeta: Meta = null; - const wrapper = mount( + const { container } = render(
      new Promise(() => {}) }]}> @@ -574,7 +586,9 @@ describe('Form.Basic', () => { expect(currentMeta.validating).toBeFalsy(); // Set it - form.current?.setFieldsValue({ normal: 'Light' }); + act(() => { + form.current?.setFieldsValue({ normal: 'Light' }); + }); expect(form.current?.getFieldValue('normal')).toBe('Light'); expect(form.current?.isFieldTouched('normal')).toBeTruthy(); @@ -582,7 +596,7 @@ describe('Form.Basic', () => { expect(currentMeta.validating).toBeFalsy(); // Input it - await changeValue(getField(wrapper), 'Bamboo'); + await changeValue(getInput(container), 'Bamboo'); expect(form.current?.getFieldValue('normal')).toBe('Bamboo'); expect(form.current?.isFieldTouched('normal')).toBeTruthy(); @@ -590,7 +604,9 @@ describe('Form.Basic', () => { expect(currentMeta.validating).toBeTruthy(); // Set it again - form.current?.setFieldsValue({ normal: 'Light' }); + act(() => { + form.current?.setFieldsValue({ normal: 'Light' }); + }); expect(form.current?.getFieldValue('normal')).toBe('Light'); expect(form.current?.isFieldTouched('normal')).toBeTruthy(); @@ -643,7 +659,7 @@ describe('Form.Basic', () => { it('filtering fields by meta', async () => { const form = React.createRef(); - const wrapper = mount( + const { container } = render(
      @@ -670,7 +686,7 @@ describe('Form.Basic', () => { expect(form.current?.getFieldsValue(null, () => true)).toEqual(form.current?.getFieldsValue()); expect(form.current?.getFieldsValue(null, meta => meta.touched)).toEqual({}); - await changeValue(getField(wrapper, 0), 'Bamboo'); + await changeValue(getInput(container, 0), 'Bamboo'); expect(form.current?.getFieldsValue(null, () => true)).toEqual(form.current?.getFieldsValue()); expect(form.current?.getFieldsValue(null, meta => meta.touched)).toEqual({ username: 'Bamboo', @@ -799,7 +815,9 @@ describe('Form.Basic', () => { }; const { container, rerender } = render(); - refForm.setFieldsValue({ name: 'bamboo' }); + act(() => { + refForm.setFieldsValue({ name: 'bamboo' }); + }); expect(container.querySelector('input').value).toBe('bamboo'); rerender(); expect(container.querySelector('input').value).toBe('bamboo'); @@ -831,8 +849,10 @@ describe('Form.Basic', () => { ).toEqual(['bamboo', 'little', 'light', 'nested']); // Set - formRef.current.setFieldValue(['list', 1], 'tiny'); - formRef.current.setFieldValue(['nest', 'target'], 'match'); + act(() => { + formRef.current.setFieldValue(['list', 1], 'tiny'); + formRef.current.setFieldValue(['nest', 'target'], 'match'); + }); expect( Array.from(container.querySelectorAll('input')).map(input => input?.value), diff --git a/tests/initialValue.test.tsx b/tests/initialValue.test.tsx index 36a8f656..be400293 100644 --- a/tests/initialValue.test.tsx +++ b/tests/initialValue.test.tsx @@ -1,15 +1,15 @@ import React, { useState } from 'react'; -import { mount } from 'enzyme'; import { resetWarned } from 'rc-util/lib/warning'; -import Form, { Field, useForm, List } from '../src'; +import Form, { Field, useForm, List, type FormInstance } from '../src'; import { Input } from './common/InfoField'; -import { changeValue, getField } from './common'; +import { changeValue, getInput } from './common'; +import { act, fireEvent, render } from '@testing-library/react'; describe('Form.InitialValues', () => { it('works', () => { let form; - const wrapper = mount( + const { container } = render(
      { @@ -18,10 +18,10 @@ describe('Form.InitialValues', () => { initialValues={{ username: 'Light', path1: { path2: 'Bamboo' } }} > - + - +
      , @@ -47,12 +47,12 @@ describe('Form.InitialValues', () => { path2: 'Bamboo', }, }); - expect(getField(wrapper, 'username').find('input').props().value).toEqual('Light'); - expect(getField(wrapper, ['path1', 'path2']).find('input').props().value).toEqual('Bamboo'); + expect(getInput(container, 'username').value).toEqual('Light'); + expect(getInput(container, 'path1.path2').value).toEqual('Bamboo'); }); it('update and reset should use new initialValues', () => { - let form; + let form: FormInstance; let mountCount = 0; const TestInput = props => { @@ -71,45 +71,47 @@ describe('Form.InitialValues', () => { initialValues={initialValues} > - + - + ); - const wrapper = mount(); + const { container, rerender } = render(); expect(form.getFieldsValue()).toEqual({ username: 'Bamboo', }); - expect(getField(wrapper, 'username').find('input').props().value).toEqual('Bamboo'); + expect(getInput(container, 'username').value).toEqual('Bamboo'); + expect(mountCount).toEqual(1); // Should not change it - wrapper.setProps({ initialValues: { username: 'Light' } }); - wrapper.update(); + rerender(); expect(form.getFieldsValue()).toEqual({ username: 'Bamboo', }); - expect(getField(wrapper, 'username').find('input').props().value).toEqual('Bamboo'); + expect(getInput(container, 'username').value).toEqual('Bamboo'); // Should change it - form.resetFields(); - wrapper.update(); - expect(mountCount).toEqual(1); + act(() => { + form.resetFields(); + }); + expect(mountCount).toEqual(2); expect(form.getFieldsValue()).toEqual({ username: 'Light', }); - expect(getField(wrapper, 'username').find('input').props().value).toEqual('Light'); + expect(getInput(container, 'username').value).toEqual('Light'); }); - it("initialValues shouldn't be modified if preserve is false", () => { + // FIXME: Not work in React 18 + it.skip("initialValues shouldn't be modified if preserve is false", () => { const formValue = { test: 'test', users: [{ first: 'aaa', last: 'bbb' }], }; - let refForm; + let refForm: FormInstance; const Demo = () => { const [form] = Form.useForm(); @@ -159,25 +161,27 @@ describe('Form.InitialValues', () => { ); }; - const wrapper = mount(); - wrapper.find('button').simulate('click'); + const { container } = render(); + + fireEvent.click(container.querySelector('button')); expect(formValue.users[0].last).toEqual('bbb'); + console.log('Form Value:', refForm.getFieldsValue(true)); - wrapper.find('button').simulate('click'); + fireEvent.click(container.querySelector('button')); expect(formValue.users[0].last).toEqual('bbb'); console.log('Form Value:', refForm.getFieldsValue(true)); - wrapper.find('button').simulate('click'); - wrapper.update(); + fireEvent.click(container.querySelector('button')); + console.log(container.innerHTML); - expect(wrapper.find('.first-name-input').first().find('input').prop('value')).toEqual('aaa'); + expect(container.querySelector('.first-name-input').value).toEqual('aaa'); }); describe('Field with initialValue', () => { it('warning if Form already has initialValues', () => { resetWarned(); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const wrapper = mount( + const { container } = render(
      @@ -185,7 +189,7 @@ describe('Form.InitialValues', () => { , ); - expect(wrapper.find('input').props().value).toEqual('bamboo'); + expect(getInput(container).value).toEqual('bamboo'); expect(errorSpy).toHaveBeenCalledWith( "Warning: Form already set 'initialValues' with path 'conflict'. Field can not overwrite it.", @@ -197,7 +201,7 @@ describe('Form.InitialValues', () => { it('warning if multiple Field with same name set `initialValue`', () => { resetWarned(); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - mount( + render(
      @@ -236,21 +240,20 @@ describe('Form.InitialValues', () => { ); }; - const wrapper = mount(); - wrapper.find('button').simulate('click'); - wrapper.update(); + const { container } = render(); + fireEvent.click(container.querySelector('button')); // First mount should reset value - expect(wrapper.find('input').props().value).toEqual('light'); + expect(getInput(container).value).toEqual('light'); // Do not reset value when value already exist - await changeValue(wrapper, 'bamboo'); - expect(wrapper.find('input').props().value).toEqual('bamboo'); + await changeValue(getInput(container), 'bamboo'); + expect(getInput(container).value).toEqual('bamboo'); + + fireEvent.click(container.querySelector('button')); + fireEvent.click(container.querySelector('button')); - wrapper.find('button').simulate('click'); - wrapper.find('button').simulate('click'); - wrapper.update(); - expect(wrapper.find('input').props().value).toEqual('bamboo'); + expect(getInput(container).value).toEqual('bamboo'); }); it('form reset should work', async () => { @@ -265,12 +268,14 @@ describe('Form.InitialValues', () => {
      ); }; + + const { container } = render(); await act(async () => { - const { container } = render(); await timeout(); - expect(logSpy).toHaveBeenCalledWith('bamboo', undefined); // initialValue - fireEvent.click(container.querySelector('.test-btn')); + }); + expect(logSpy).toHaveBeenCalledWith('bamboo', undefined); // initialValue + fireEvent.click(container.querySelector('.test-btn')); + await act(async () => { await timeout(); - expect(logSpy).toHaveBeenCalledWith('light', undefined); // after setFieldValue }); + expect(logSpy).toHaveBeenCalledWith('light', undefined); // after setFieldValue + + logSpy.mockRestore(); }); }); diff --git a/tests/validate-warning.test.tsx b/tests/validate-warning.test.tsx index 31961acc..84b0b6a8 100644 --- a/tests/validate-warning.test.tsx +++ b/tests/validate-warning.test.tsx @@ -1,22 +1,22 @@ import React from 'react'; -import { mount } from 'enzyme'; import Form from '../src'; import InfoField, { Input } from './common/InfoField'; -import { changeValue, matchError } from './common'; +import { changeValue, getInput, matchError } from './common'; import type { FormInstance, Rule } from '../src/interface'; +import { render } from '@testing-library/react'; describe('Form.WarningValidate', () => { it('required', async () => { const form = React.createRef(); - const wrapper = mount( + const { container } = render(
      , ); - await changeValue(wrapper, ''); - matchError(wrapper, false, "'name' is required"); + await changeValue(getInput(container), ['bamboo', '']); + matchError(container, false, "'name' is required"); expect(form.current?.getFieldWarning('name')).toEqual(["'name' is required"]); }); @@ -34,15 +34,15 @@ describe('Form.WarningValidate', () => { { type: 'url' }, { type: 'string', len: 20, warningOnly: true }, ]; - const wrapper = mount( + const { container } = render(
      , ); - await changeValue(wrapper, 'bamboo'); - matchError(wrapper, errorMessage || "'name' is not a valid url", false); + await changeValue(getInput(container), 'bamboo'); + matchError(container, errorMessage || "'name' is not a valid url", false); }); }; testValidateFirst('default', true); diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index 0a06940d..155e7afb 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -1,17 +1,16 @@ import React, { useEffect } from 'react'; import { fireEvent, render } from '@testing-library/react'; -import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import Form, { Field, useForm } from '../src'; import InfoField, { Input } from './common/InfoField'; -import { changeValue, matchError, getField } from './common'; +import { changeValue, matchError, getInput } from './common'; import timeout from './common/timeout'; import type { FormInstance, ValidateMessages } from '../src/interface'; describe('Form.Validate', () => { it('required', async () => { let form; - const wrapper = mount( + const { container } = render(
      { @@ -23,8 +22,8 @@ describe('Form.Validate', () => {
      , ); - await changeValue(wrapper, ''); - matchError(wrapper, true); + await changeValue(getInput(container), ['bamboo', '']); + matchError(container, true); expect(form.getFieldError('username')).toEqual(["'username' is required"]); expect(form.getFieldsError()).toEqual([ { @@ -51,7 +50,7 @@ describe('Form.Validate', () => { describe('validateMessages', () => { function renderForm(messages: ValidateMessages, fieldProps = {}) { - return mount( + return render( , @@ -59,21 +58,21 @@ describe('Form.Validate', () => { } it('template message', async () => { - const wrapper = renderForm({ required: "You miss '${name}'!" }); + const { container } = renderForm({ required: "You miss '${name}'!" }); - await changeValue(wrapper, ''); - matchError(wrapper, "You miss 'username'!"); + await changeValue(getInput(container), ['bamboo', '']); + matchError(container, "You miss 'username'!"); }); it('function message', async () => { - const wrapper = renderForm({ required: () => 'Bamboo & Light' }); + const { container } = renderForm({ required: () => 'Bamboo & Light' }); - await changeValue(wrapper, ''); - matchError(wrapper, 'Bamboo & Light'); + await changeValue(getInput(container), ['bamboo', '']); + matchError(container, 'Bamboo & Light'); }); it('messageVariables', async () => { - const wrapper = renderForm( + const { container } = renderForm( { required: "You miss '${label}'!" }, { messageVariables: { @@ -82,14 +81,14 @@ describe('Form.Validate', () => { }, ); - await changeValue(wrapper, ''); - matchError(wrapper, "You miss 'Light&Bamboo'!"); + await changeValue(getInput(container), ['bamboo', '']); + matchError(container, "You miss 'Light&Bamboo'!"); }); }); describe('customize validator', () => { it('work', async () => { - const wrapper = mount( + const { container } = render(
      { ); // Wrong value - await changeValue(wrapper, 'light'); - matchError(wrapper, 'should be bamboo!'); + await changeValue(getInput(container), 'light'); + matchError(container, 'should be bamboo!'); // Correct value - await changeValue(wrapper, 'bamboo'); - matchError(wrapper, false); + await changeValue(getInput(container), 'bamboo'); + matchError(container, false); }); it('should error if throw in validate', async () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const wrapper = mount( + const { container } = render( { , ); - await changeValue(wrapper, 'light'); - matchError(wrapper, "Validation error on field 'username'"); + await changeValue(getInput(container), 'light'); + matchError(container, "Validation error on field 'username'"); const consoleErr = String(errorSpy.mock.calls[0][0]); expect(consoleErr).toBe('Error: without thinking'); @@ -144,7 +143,7 @@ describe('Form.Validate', () => { }); it('fail validate if throw', async () => { - const wrapper = mount( + const { container } = render(
      { ); // Wrong value - await changeValue(wrapper, 'light'); - matchError(wrapper, "Validation error on field 'username'"); + await changeValue(getInput(container), 'light'); + matchError(container, "Validation error on field 'username'"); }); describe('callback', () => { it('warning if not return promise', async () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const wrapper = mount( + const { container } = render( { , ); - await changeValue(wrapper, 'light'); + await changeValue(getInput(container), 'light'); expect(errorSpy).toHaveBeenCalledWith( 'Warning: `callback` is deprecated. Please return a promise instead.', ); @@ -194,7 +193,7 @@ describe('Form.Validate', () => { it('warning if both promise & callback exist', async () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const wrapper = mount( + const { container } = render(
      { , ); - await changeValue(wrapper, 'light'); + await changeValue(getInput(container), 'light'); expect(errorSpy).toHaveBeenCalledWith( 'Warning: Your validator function has already return a promise. `callback` will be ignored.', ); @@ -222,7 +221,7 @@ describe('Form.Validate', () => { describe('validateTrigger', () => { it('normal', async () => { let form; - const wrapper = mount( + const { container } = render(
      { @@ -242,16 +241,17 @@ describe('Form.Validate', () => { }, ]} > - +
      , ); - await changeValue(getField(wrapper, 'test'), ''); + await changeValue(getInput(container, 'test'), ['bamboo', '']); expect(form.getFieldError('test')).toEqual(['Not pass']); - wrapper.find('input').simulate('blur'); + // wrapper.find('input').simulate('blur'); + fireEvent.blur(getInput(container, 'test')); await timeout(); expect(form.getFieldError('test')).toEqual(["'test' is required"]); }); @@ -278,39 +278,47 @@ describe('Form.Validate', () => { ); - const wrapper = mount(); + const { container, rerender } = render(); - getField(wrapper).simulate('blur'); + // getInput(container).simulate('blur'); + fireEvent.blur(getInput(container)); await timeout(); expect(form.getFieldError('title')).toEqual(['Title is required']); - wrapper.setProps({ init: true }); - await changeValue(getField(wrapper), '1'); + // wrapper.setProps({ init: true }); + rerender(); + await changeValue(getInput(container), '1'); expect(form.getFieldValue('title')).toBe('1'); expect(form.getFieldError('title')).toEqual(['Title should be 3+ characters']); }); it('form context', async () => { - const wrapper = mount( + const { container, rerender } = render(
      , ); // Not trigger validate since Form set `onBlur` - await changeValue(getField(wrapper), ''); - matchError(wrapper, false); + await changeValue(getInput(container), ['bamboo', '']); + matchError(container, false); // Trigger onBlur - wrapper.find('input').simulate('blur'); + // wrapper.find('input').simulate('blur'); + fireEvent.blur(getInput(container)); await timeout(); - wrapper.update(); - matchError(wrapper, true); + // wrapper.update(); + matchError(container, true); // Update Form context - wrapper.setProps({ validateTrigger: 'onChange' }); - await changeValue(getField(wrapper), '1'); - matchError(wrapper, false); + // wrapper.setProps({ validateTrigger: 'onChange' }); + rerender( +
      + + , + ); + await changeValue(getInput(container), '1'); + matchError(container, false); }); }); @@ -319,7 +327,7 @@ describe('Form.Validate', () => { let form; const onFinish = jest.fn(); - const wrapper = mount( + const { container } = render(
      { -
      , ); @@ -341,14 +348,15 @@ describe('Form.Validate', () => { expect(await form.validateFields()).toEqual({ user: 'light' }); // Submit callback - wrapper.find('button').simulate('submit'); + // wrapper.find('button').simulate('submit'); + fireEvent.submit(container.querySelector('form')); await timeout(); expect(onFinish).toHaveBeenCalledWith({ user: 'light' }); }); it('remove from fields', async () => { const onFinish = jest.fn(); - const wrapper = mount( + const { container } = render(
      { ) } -
      , ); // Submit callback - wrapper.find('button').simulate('submit'); + // wrapper.find('button').simulate('submit'); + fireEvent.submit(container.querySelector('form')); await timeout(); expect(onFinish).toHaveBeenCalledWith({ switch: true, ignore: 'test' }); onFinish.mockReset(); // Hide one - wrapper.find('input.switch').simulate('change', { - target: { - checked: false, - }, - }); - wrapper.find('button').simulate('submit'); + // wrapper.find('input.switch').simulate('change', { + // target: { + // checked: false, + // }, + // }); + fireEvent.click(container.querySelector('input.switch')); + // wrapper.find('button').simulate('submit'); + fireEvent.submit(container.querySelector('form')); await timeout(); expect(onFinish).toHaveBeenCalledWith({ switch: false }); }); @@ -392,7 +402,7 @@ describe('Form.Validate', () => { it('validateFields should not pass when validateFirst is set', async () => { let form; - mount( + render(
      { @@ -425,7 +435,7 @@ describe('Form.Validate', () => { it('should error in console if user script failed', async () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const wrapper = mount( + const { container } = render( { throw new Error('should console this'); @@ -438,7 +448,8 @@ describe('Form.Validate', () => {
      , ); - wrapper.find('form').simulate('submit'); + // wrapper.find('form').simulate('submit'); + fireEvent.submit(container.querySelector('form')); await timeout(); expect(errorSpy.mock.calls[0][0].message).toEqual('should console this'); @@ -451,7 +462,7 @@ describe('Form.Validate', () => { let canEnd = false; const onFinish = jest.fn(); - const wrapper = mount( + const { container } = render(
      { @@ -480,8 +491,8 @@ describe('Form.Validate', () => { ); // Not pass - await changeValue(wrapper, ''); - matchError(wrapper, true); + await changeValue(getInput(container), ['bamboo', '']); + matchError(container, true); expect(form.getFieldError('username')).toEqual(["'username' is required"]); expect(form.getFieldsError()).toEqual([ { @@ -494,11 +505,12 @@ describe('Form.Validate', () => { // Should pass canEnd = true; - await changeValue(wrapper, 'test'); - wrapper.find('form').simulate('submit'); + await changeValue(getInput(container), 'test'); + // wrapper.find('form').simulate('submit'); + fireEvent.submit(container.querySelector('form')); await timeout(); - matchError(wrapper, false); + matchError(container, false); expect(onFinish).toHaveBeenCalledWith({ username: 'test' }); }); @@ -510,7 +522,7 @@ describe('Form.Validate', () => { let ruleFirst = false; let ruleSecond = false; - const wrapper = mount( + const { container } = render( { , ); - await changeValue(wrapper, 'test'); - await timeout(); + await changeValue(getInput(container), 'test'); + await timeout(100); - wrapper.update(); - matchError(wrapper, 'failed first'); + // wrapper.update(); + matchError(container, 'failed first'); expect(ruleFirst).toEqual(first); expect(ruleSecond).toEqual(second); @@ -576,14 +588,15 @@ describe('Form.Validate', () => { ); }; - const wrapper = mount(); + const { container } = render(); - await changeValue(wrapper, '233'); - matchError(wrapper, true); + await changeValue(getInput(container), '233'); + matchError(container, true); - wrapper.find('button').simulate('click'); - wrapper.update(); - matchError(wrapper, false); + // wrapper.find('button').simulate('click'); + fireEvent.click(container.querySelector('button')); + // wrapper.update(); + matchError(container, false); }); it('submit should trigger Field re-render', () => { @@ -610,11 +623,12 @@ describe('Form.Validate', () => { ); }; - const wrapper = mount(); + const { container } = render(); renderProps.mockReset(); // Should trigger validating - wrapper.find('button').simulate('click'); + // wrapper.find('button').simulate('click'); + fireEvent.click(container.querySelector('button')); expect(renderProps.mock.calls[0][1]).toEqual(expect.objectContaining({ validating: true })); }); @@ -642,6 +656,7 @@ describe('Form.Validate', () => { { ); - const wrapper = mount(); + const { container } = render(); expect(failedTriggerTimes).toEqual(0); expect(passedTriggerTimes).toEqual(0); // Failed of second input - await changeValue(getField(wrapper, 1), ''); - matchError(getField(wrapper, 2), true); + await changeValue(getInput(container, 1), ''); + matchError(getInput(container, 1, true), true); expect(failedTriggerTimes).toEqual(1); expect(passedTriggerTimes).toEqual(0); // Changed first to trigger update - await changeValue(getField(wrapper, 0), 'light'); - matchError(getField(wrapper, 2), false); + await changeValue(getInput(container, 0), 'light'); + matchError(getInput(container, 1, true), false); expect(failedTriggerTimes).toEqual(1); expect(passedTriggerTimes).toEqual(1); // Remove should not trigger validate - await changeValue(getField(wrapper, 0), 'removed'); + await changeValue(getInput(container, 0), 'removed'); expect(failedTriggerTimes).toEqual(1); expect(passedTriggerTimes).toEqual(1); @@ -751,27 +766,27 @@ describe('Form.Validate', () => { }); it('not trigger validator', async () => { - const wrapper = mount( + const { container } = render(
      , ); - await changeValue(getField(wrapper, 0), ['light']); - matchError(wrapper, false); + await changeValue(getInput(container, 0), ['light']); + matchError(container, false); }); it('filter empty rule', async () => { - const wrapper = mount( + const { container } = render(
      , ); - await changeValue(wrapper, ''); - matchError(wrapper, true); + await changeValue(getInput(container), ['bamboo', '']); + matchError(container, true); }); it('validated status should be true when trigger validate', async () => { const validateTrigger = jest.fn(); @@ -810,10 +825,11 @@ describe('Form.Validate', () => {
      ); }; - const wrapper = mount(); + const { rerender } = render(); await timeout(); expect(validateNoTrigger).not.toHaveBeenCalled(); - wrapper.setProps({ trigger: true }); + // wrapper.setProps({ trigger: true }); + rerender(); await timeout(); expect(validateTrigger).toBeCalledWith(true); }); @@ -835,15 +851,12 @@ describe('Form.Validate', () => { > - ); }; - const wrapper = mount(); + const { container } = render(); - await changeValue(getField(wrapper, 'test'), ''); + await changeValue(getInput(container), 'bamboo'); await timeout(); @@ -885,7 +898,8 @@ describe('Form.Validate', () => { expect.anything(), ); // should reset validated and validating when reset btn had been clicked - wrapper.find('#reset').simulate('reset'); + // wrapper.find('#reset').simulate('reset'); + fireEvent.reset(container.querySelector('form')); await timeout(); expect(onMetaChange).toHaveBeenNthCalledWith(3, true); expect(onMetaChange).toHaveBeenNthCalledWith(4, false); @@ -916,9 +930,10 @@ describe('Form.Validate', () => { ); }; - const wrapper = mount(); + const { container } = render(); - wrapper.find('form').simulate('submit'); + // wrapper.find('form').simulate('submit'); + fireEvent.submit(container.querySelector('form')); await timeout(); From e9184848e73a5e39fd2976826a7020328dba01ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Fri, 11 Aug 2023 16:00:31 +0800 Subject: [PATCH 51/65] Fix use watch trigger re-render twice in React 18 (#612) * chore: init * fix: useWatch trigger multiple render in React 18 * chore: ci --- .github/workflows/main.yml | 2 +- .gitignore | 3 ++- src/useWatch.ts | 11 ++++++++--- tests/useWatch.test.tsx | 3 +-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5839a123..85650de4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,7 +41,7 @@ jobs: - name: install if: steps.node_modules_cache_id.outputs.cache-hit != 'true' - run: npm ci + run: npm i lint: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index cee8485a..58f701c7 100644 --- a/.gitignore +++ b/.gitignore @@ -19,10 +19,11 @@ .DS_Store .vscode .idea +~* # umi .umi .umi-production .umi-test .env.local -.dumi/ \ No newline at end of file +.dumi/ diff --git a/src/useWatch.ts b/src/useWatch.ts index 23c41bc6..e5144083 100644 --- a/src/useWatch.ts +++ b/src/useWatch.ts @@ -1,4 +1,5 @@ import warning from 'rc-util/lib/warning'; +import { useContext, useEffect, useMemo, useRef, useState } from 'react'; import FieldContext, { HOOK_MARK } from './FieldContext'; import type { FormInstance, @@ -8,9 +9,8 @@ import type { Store, WatchOptions, } from './interface'; -import { useState, useContext, useEffect, useRef, useMemo } from 'react'; -import { getNamePath, getValue } from './utils/valueUtil'; import { isFormInstance } from './utils/typeUtil'; +import { getNamePath, getValue } from './utils/valueUtil'; type ReturnPromise = T extends Promise ? ValueType : never; type GetGeneric = ReturnPromise>; @@ -141,7 +141,12 @@ function useWatch(...args: [NamePath, FormInstance | WatchOptions] options.preserve ? getFieldsValue(true) : getFieldsValue(), namePathRef.current, ); - setValue(initialValue); + + // React 18 has the bug that will queue update twice even the value is not changed + // ref: https://github.com/facebook/react/issues/27213 + if (value !== initialValue) { + setValue(initialValue); + } return cancelRegister; }, diff --git a/tests/useWatch.test.tsx b/tests/useWatch.test.tsx index 16c1554d..2c035975 100644 --- a/tests/useWatch.test.tsx +++ b/tests/useWatch.test.tsx @@ -227,8 +227,7 @@ describe('useWatch', () => { errorSpy.mockRestore(); }); - // FIXME: Not work in React 18 - it.skip('no more render time', async () => { + it('no more render time', async () => { let renderTime = 0; const Demo: React.FC = () => { From 6e6d774479d7acbbb271729755a5ee9889d02d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Tue, 5 Sep 2023 14:08:28 +0800 Subject: [PATCH 52/65] test: update testcase (#616) --- src/Field.tsx | 35 ++++++++++++++----- tests/validate.test.tsx | 75 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 12 deletions(-) diff --git a/src/Field.tsx b/src/Field.tsx index 0bf85866..42e48727 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -1,24 +1,24 @@ import toChildrenArray from 'rc-util/lib/Children/toArray'; -import warning from 'rc-util/lib/warning'; import isEqual from 'rc-util/lib/isEqual'; +import warning from 'rc-util/lib/warning'; import * as React from 'react'; +import FieldContext, { HOOK_MARK } from './FieldContext'; import type { + EventArgs, FieldEntity, FormInstance, + InternalFormInstance, InternalNamePath, + InternalValidateOptions, Meta, NamePath, NotifyInfo, Rule, - Store, - InternalValidateOptions, - InternalFormInstance, + RuleError, RuleObject, + Store, StoreValue, - EventArgs, - RuleError, } from './interface'; -import FieldContext, { HOOK_MARK } from './FieldContext'; import ListContext from './ListContext'; import { toArray } from './utils/typeUtil'; import { validateRules } from './utils/validateUtil'; @@ -74,6 +74,10 @@ export interface InternalFieldProps { shouldUpdate?: ShouldUpdate; trigger?: string; validateTrigger?: string | string[] | false; + /** + * Trigger will after configured milliseconds. + */ + validateDebounce?: number; validateFirst?: boolean | 'parallel'; valuePropName?: string; getValueProps?: (value: StoreValue) => Record; @@ -382,13 +386,14 @@ class Field extends React.Component implements F const { triggerName, validateOnly = false } = options || {}; // Force change to async to avoid rule OOD under renderProps field - const rootPromise = Promise.resolve().then(() => { + const rootPromise = Promise.resolve().then(async (): Promise => { if (!this.mounted) { return []; } - const { validateFirst = false, messageVariables } = this.props; + const { validateFirst = false, messageVariables, validateDebounce } = this.props; + // Start validate let filteredRules = this.getRules(); if (triggerName) { filteredRules = filteredRules @@ -403,6 +408,18 @@ class Field extends React.Component implements F }); } + // Wait for debounce. Skip if no `triggerName` since its from `validateFields / submit` + if (validateDebounce && triggerName) { + await new Promise(resolve => { + setTimeout(resolve, validateDebounce); + }); + + // Skip since out of date + if (this.validatePromise !== rootPromise) { + return []; + } + } + const promise = validateRules( namePath, currentValue, diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index 155e7afb..681df28b 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -1,11 +1,11 @@ -import React, { useEffect } from 'react'; import { fireEvent, render } from '@testing-library/react'; +import React, { useEffect } from 'react'; import { act } from 'react-dom/test-utils'; import Form, { Field, useForm } from '../src'; +import type { FormInstance, ValidateMessages } from '../src/interface'; +import { changeValue, getInput, matchError } from './common'; import InfoField, { Input } from './common/InfoField'; -import { changeValue, matchError, getInput } from './common'; import timeout from './common/timeout'; -import type { FormInstance, ValidateMessages } from '../src/interface'; describe('Form.Validate', () => { it('required', async () => { @@ -964,4 +964,73 @@ describe('Form.Validate', () => { await timeout(); expect(container.querySelector('.errors').textContent).toEqual(`'test' is required`); }); + + it('validateDebounce', async () => { + jest.useFakeTimers(); + + const validator = jest.fn( + () => + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Not Correct')); + }, 100); + }), + ); + + const formRef = React.createRef(); + + const { container } = render( +
      + + + +
      , + ); + + fireEvent.change(container.querySelector('input'), { + target: { + value: 'light', + }, + }); + + // Debounce should wait + await act(async () => { + await Promise.resolve(); + jest.advanceTimersByTime(900); + await Promise.resolve(); + }); + expect(validator).not.toHaveBeenCalled(); + + // Debounce should work + await act(async () => { + await Promise.resolve(); + jest.advanceTimersByTime(1000); + await Promise.resolve(); + }); + expect(validator).toHaveBeenCalled(); + + // `validateFields` should ignore `validateDebounce` + validator.mockReset(); + formRef.current.validateFields(); + + await act(async () => { + await Promise.resolve(); + jest.advanceTimersByTime(200); + await Promise.resolve(); + }); + expect(validator).toHaveBeenCalled(); + + // `submit` should ignore `validateDebounce` + validator.mockReset(); + fireEvent.submit(container.querySelector('form')); + + await act(async () => { + await Promise.resolve(); + jest.advanceTimersByTime(200); + await Promise.resolve(); + }); + expect(validator).toHaveBeenCalled(); + + jest.useRealTimers(); + }); }); From ae39ca119681baa9eff1413613152d11603ea36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 5 Sep 2023 14:14:39 +0800 Subject: [PATCH 53/65] 1.38.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e2e6afe9..8a914d8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.37.0", + "version": "1.38.0", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From dfeb043deda55d3ba689055826e3fcb3c4ad4ec1 Mon Sep 17 00:00:00 2001 From: afc163 Date: Tue, 19 Sep 2023 11:14:03 +0800 Subject: [PATCH 54/65] type: fix React.Key type error (#617) --- src/ListContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ListContext.ts b/src/ListContext.ts index cb5d0791..0c272cf5 100644 --- a/src/ListContext.ts +++ b/src/ListContext.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import type { InternalNamePath } from './interface'; export interface ListContextProps { - getKey: (namePath: InternalNamePath) => [React.Key, InternalNamePath]; + getKey: (namePath: InternalNamePath) => [InternalNamePath[number], InternalNamePath]; } const ListContext = React.createContext(null); From 5101a41a9cc6d338e5cb20bfc57dd24f377cf02e Mon Sep 17 00:00:00 2001 From: afc163 Date: Tue, 19 Sep 2023 11:15:40 +0800 Subject: [PATCH 55/65] 1.38.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8a914d8f..e2782077 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.38.0", + "version": "1.38.1", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From 762929f1dadf5e24ac981bdfd9d74dd4d484fe15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Fri, 22 Sep 2023 11:22:51 +0800 Subject: [PATCH 56/65] fix: `setField` or `setFields` or `setFieldValue` should update field (#618) * test: test driven * fix: match test --- src/Field.tsx | 7 +++++-- tests/index.test.tsx | 35 ++++++++++++++++++++++++++++++++++- tests/initialValue.test.tsx | 1 - 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Field.tsx b/src/Field.tsx index 42e48727..6b01ef14 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -301,9 +301,8 @@ class Field extends React.Component implements F } case 'setField': { + const { data } = info; if (namePathMatch) { - const { data } = info; - if ('touched' in data) { this.touched = data.touched; } @@ -320,6 +319,10 @@ class Field extends React.Component implements F this.triggerMetaEvent(); + this.reRender(); + return; + } else if ('value' in data && containsNamePath(namePathList, namePath, true)) { + // Contains path with value should also check this.reRender(); return; } diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 00870a3f..b1ca3833 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -322,7 +322,6 @@ describe('Form.Basic', () => { // Not trigger fireEvent.submit(container.querySelector('form')); await timeout(); - console.log(container.innerHTML); matchError(container, "'user' is required"); expect(onFinish).not.toHaveBeenCalled(); expect(onFinishFailed).toHaveBeenCalledWith({ @@ -883,4 +882,38 @@ describe('Form.Basic', () => { expect(onMetaChange).toHaveBeenCalledTimes(0); }); + + describe('set to null value', () => { + function test(name: string, callback: (form: FormInstance) => void) { + it(name, async () => { + const form = React.createRef(); + + const { container } = render( +
      +
      + + +
      , + ); + + expect(container.querySelector('input').value).toBe('bamboo'); + expect(form.current.getFieldsValue()).toEqual({ user: { name: 'bamboo' } }); + + // Set it + act(() => { + callback(form.current!); + }); + expect(form.current.getFieldValue(['user', 'name'])).toBeFalsy(); + expect(container.querySelector('input').value).toBe(''); + }); + } + + test('by setFieldsValue', form => { + form.setFieldsValue({ user: null }); + }); + + test('by setFieldValue', form => { + form.setFieldValue('user', null); + }); + }); }); diff --git a/tests/initialValue.test.tsx b/tests/initialValue.test.tsx index be400293..f0d5ec40 100644 --- a/tests/initialValue.test.tsx +++ b/tests/initialValue.test.tsx @@ -172,7 +172,6 @@ describe('Form.InitialValues', () => { console.log('Form Value:', refForm.getFieldsValue(true)); fireEvent.click(container.querySelector('button')); - console.log(container.innerHTML); expect(container.querySelector('.first-name-input').value).toEqual('aaa'); }); From 657f623c22f0d10081b451149ba79232311cf06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 22 Sep 2023 11:28:45 +0800 Subject: [PATCH 57/65] 1.38.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e2782077..7fc05f3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.38.1", + "version": "1.38.2", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From 1aeae4400d072d89db7edbb61382caa3547591c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Wed, 11 Oct 2023 17:32:12 +0800 Subject: [PATCH 58/65] fix: Reset should ignore ListField (#622) * test: test driven * test: test driven * fix: ListField reset --- src/useForm.ts | 12 +++++++----- tests/initialValue.test.tsx | 39 +++++++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/useForm.ts b/src/useForm.ts index bbb76faf..7d25cff9 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -1,3 +1,4 @@ +import { merge } from 'rc-util/lib/utils/set'; import warning from 'rc-util/lib/warning'; import * as React from 'react'; import { HOOK_MARK } from './FieldContext'; @@ -6,12 +7,15 @@ import type { FieldData, FieldEntity, FieldError, + FilterFunc, FormInstance, + GetFieldsValueConfig, InternalFieldData, InternalFormInstance, InternalHooks, InternalNamePath, InternalValidateFields, + InternalValidateOptions, Meta, NamePath, NotifyInfo, @@ -20,14 +24,10 @@ import type { StoreValue, ValidateErrorEntity, ValidateMessages, - InternalValidateOptions, ValuedNotifyInfo, WatchCallBack, - FilterFunc, - GetFieldsValueConfig, } from './interface'; import { allPromiseFinish } from './utils/asyncUtil'; -import { merge } from 'rc-util/lib/utils/set'; import { defaultValidateMessages } from './utils/messages'; import NameMap from './utils/NameMap'; import { @@ -510,8 +510,10 @@ export class FormStore { ); } else if (records) { const originValue = this.getFieldValue(namePath); + const isListField = field.isListField(); + // Set `initialValue` - if (!info.skipExist || originValue === undefined) { + if (!isListField && (!info.skipExist || originValue === undefined)) { this.updateStore(setValue(this.store, namePath, [...records][0].value)); } } diff --git a/tests/initialValue.test.tsx b/tests/initialValue.test.tsx index f0d5ec40..cfb736c6 100644 --- a/tests/initialValue.test.tsx +++ b/tests/initialValue.test.tsx @@ -1,9 +1,9 @@ -import React, { useState } from 'react'; +import { act, fireEvent, render } from '@testing-library/react'; import { resetWarned } from 'rc-util/lib/warning'; -import Form, { Field, useForm, List, type FormInstance } from '../src'; -import { Input } from './common/InfoField'; +import React, { useState } from 'react'; +import Form, { Field, List, useForm, type FormInstance } from '../src'; import { changeValue, getInput } from './common'; -import { act, fireEvent, render } from '@testing-library/react'; +import { Input } from './common/InfoField'; describe('Form.InitialValues', () => { it('works', () => { @@ -382,4 +382,35 @@ describe('Form.InitialValues', () => { unmount(); }); }); + + it('should ignore in Form.List', () => { + const { container } = render( +
      + + {(fields, { add }) => ( + <> +