diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index 4a957f4c..a52e6fb2 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -4,27 +4,53 @@ import Input from './components/Input'; export default function App() { const [form] = Form.useForm(); - const [keyName, setKeyName] = React.useState(true); + const names = Form.useWatch('names', form); - // const val = Form.useWatch(keyName ? 'name' : 'age', form); - const val = Form.useWatch(values => values[keyName ? 'name' : 'age'], form); + console.log('[Antd V6] names:', names); return ( -
- - - - - - - - {val} -
+
+

Antd V6 - useWatch + Form.List

+ +
+ + {(fields, { add, remove }) => { + return ( + <> + {fields.map(({key, ...field}, index) => ( +
+ + + + +
+ ))} + +
+ + +
+ + ); + }} +
+
+
); } diff --git a/package.json b/package.json index 6837d60d..bc50f56c 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "gh-pages": "^6.1.0", "jest": "^29.0.0", "prettier": "^3.1.0", - "rc-test": "^7.0.15", + "rc-test": "^7.1.3", "react": "^18.0.0", "react-dnd": "^8.0.3", "react-dnd-html5-backend": "^8.0.3", diff --git a/src/BatchUpdate.tsx b/src/BatchUpdate.tsx deleted file mode 100644 index 9482dd04..00000000 --- a/src/BatchUpdate.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; - -export type BatchTask = (key: string, callback: VoidFunction) => void; - -export interface BatchUpdateRef { - batch: BatchTask; -} - -const BatchUpdate = React.forwardRef((_, ref) => { - const [batchInfo, setBatchInfo] = React.useState>({}); - - React.useLayoutEffect(() => { - const keys = Object.keys(batchInfo); - if (keys.length) { - keys.forEach(key => { - batchInfo[key]?.(); - }); - setBatchInfo({}); - } - }, [batchInfo]); - - React.useImperativeHandle(ref, () => ({ - batch: (key, callback) => { - setBatchInfo(ori => ({ - ...ori, - [key]: callback, - })); - }, - })); - - return null; -}); - -export default BatchUpdate; diff --git a/src/FieldContext.ts b/src/FieldContext.ts index 1f927949..d2b6f4d5 100644 --- a/src/FieldContext.ts +++ b/src/FieldContext.ts @@ -42,7 +42,6 @@ const Context = React.createContext({ setValidateMessages: warningFunc, setPreserve: warningFunc, getInitialValue: warningFunc, - setBatchUpdate: warningFunc, }; }, }); diff --git a/src/Form.tsx b/src/Form.tsx index 184cc2d5..84a4f62d 100644 --- a/src/Form.tsx +++ b/src/Form.tsx @@ -8,14 +8,12 @@ import type { InternalFormInstance, FormRef, } from './interface'; -import useForm from './useForm'; +import useForm from './hooks/useForm'; import FieldContext, { HOOK_MARK } from './FieldContext'; import type { FormContextProps } from './FormContext'; import FormContext from './FormContext'; import { isSimilar } from './utils/valueUtil'; import ListContext from './ListContext'; -import type { BatchTask, BatchUpdateRef } from './BatchUpdate'; -import BatchUpdate from './BatchUpdate'; type BaseFormProps = Omit, 'onSubmit' | 'children'>; @@ -72,7 +70,6 @@ const Form: React.ForwardRefRenderFunction = ( setValidateMessages, setPreserve, destroyForm, - setBatchUpdate, } = (formInstance as InternalFormInstance).getInternalHooks(HOOK_MARK); // Pass ref with form instance @@ -121,41 +118,6 @@ const Form: React.ForwardRefRenderFunction = ( mountRef.current = true; } - // ======================== Batch Update ======================== - // zombieJ: - // To avoid Form self re-render, - // We create a sub component `BatchUpdate` to handle batch update logic. - // When the call with do not change immediate, we will batch the update - // and flush it in `useLayoutEffect` for next tick. - - // Set batch update ref - const batchUpdateRef = React.useRef(null); - const batchUpdateTasksRef = React.useRef<[key: string, fn: VoidFunction][]>([]); - - const tryFlushBatch = () => { - if (batchUpdateRef.current) { - batchUpdateTasksRef.current.forEach(([key, fn]) => { - batchUpdateRef.current.batch(key, fn); - }); - batchUpdateTasksRef.current = []; - } - }; - - // Ref update - const setBatchUpdateRef = React.useCallback((batchUpdate: BatchUpdateRef | null) => { - batchUpdateRef.current = batchUpdate; - tryFlushBatch(); - }, []); - - // Task list - - const batchUpdate: BatchTask = (key, callback) => { - batchUpdateTasksRef.current.push([key, callback]); - tryFlushBatch(); - }; - - setBatchUpdate(batchUpdate); - // ========================== Unmount =========================== React.useEffect( () => () => destroyForm(clearOnDestroy), @@ -197,7 +159,6 @@ const Form: React.ForwardRefRenderFunction = ( const wrapperNode = ( {childrenNode} - ); diff --git a/src/useForm.ts b/src/hooks/useForm.ts similarity index 95% rename from src/useForm.ts rename to src/hooks/useForm.ts index e2cd4b70..3353d8e6 100644 --- a/src/useForm.ts +++ b/src/hooks/useForm.ts @@ -2,7 +2,7 @@ import { merge } from '@rc-component/util/lib/utils/set'; import { mergeWith } from '@rc-component/util'; import warning from '@rc-component/util/lib/warning'; import * as React from 'react'; -import { HOOK_MARK } from './FieldContext'; +import { HOOK_MARK } from '../FieldContext'; import type { Callbacks, FieldData, @@ -26,11 +26,10 @@ import type { ValidateErrorEntity, ValidateMessages, ValuedNotifyInfo, - WatchCallBack, -} from './interface'; -import { allPromiseFinish } from './utils/asyncUtil'; -import { defaultValidateMessages } from './utils/messages'; -import NameMap from './utils/NameMap'; +} from '../interface'; +import { allPromiseFinish } from '../utils/asyncUtil'; +import { defaultValidateMessages } from '../utils/messages'; +import NameMap from '../utils/NameMap'; import { cloneByNamePathList, containsNamePath, @@ -38,8 +37,8 @@ import { getValue, matchNamePath, setValue, -} from './utils/valueUtil'; -import type { BatchTask } from './BatchUpdate'; +} from '../utils/valueUtil'; +import WatcherCenter from './useNotifyWatch'; type FlexibleFieldEntity = Partial; @@ -78,6 +77,8 @@ export class FormStore { private lastValidatePromise: Promise = null; + private watcherCenter = new WatcherCenter(this); + constructor(forceRootUpdate: () => void) { this.forceRootUpdate = forceRootUpdate; } @@ -121,7 +122,6 @@ export class FormStore { setPreserve: this.setPreserve, getInitialValue: this.getInitialValue, registerWatch: this.registerWatch, - setBatchUpdate: this.setBatchUpdate, }; } @@ -195,47 +195,12 @@ export class FormStore { }; // ============================= Watch ============================ - private watchList: WatchCallBack[] = []; - private registerWatch: InternalHooks['registerWatch'] = callback => { - this.watchList.push(callback); - - return () => { - this.watchList = this.watchList.filter(fn => fn !== callback); - }; + return this.watcherCenter.register(callback); }; private notifyWatch = (namePath: InternalNamePath[] = []) => { - // 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, allValues, namePath); - }); - } - }; - - private notifyWatchNamePathList: InternalNamePath[] = []; - private batchNotifyWatch = (namePath: InternalNamePath) => { - this.notifyWatchNamePathList.push(namePath); - this.batch('notifyWatch', () => { - this.notifyWatch(this.notifyWatchNamePathList); - this.notifyWatchNamePathList = []; - }); - }; - - // ============================= Batch ============================ - private batchUpdate: BatchTask; - - private setBatchUpdate = (batchUpdate: BatchTask) => { - this.batchUpdate = batchUpdate; - }; - - // Batch call the task, only last will be called - private batch = (key: string, callback: VoidFunction) => { - this.batchUpdate(key, callback); + this.watcherCenter.notify(namePath); }; // ========================== Dev Warning ========================= @@ -669,7 +634,7 @@ export class FormStore { private registerField = (entity: FieldEntity) => { this.fieldEntities.push(entity); const namePath = entity.getNamePath(); - this.batchNotifyWatch(namePath); + this.notifyWatch([namePath]); // Set initial values if (entity.props.initialValue !== undefined) { @@ -709,7 +674,7 @@ export class FormStore { } } - this.batchNotifyWatch(namePath); + this.notifyWatch([namePath]); }; }; @@ -1078,6 +1043,7 @@ function useForm(form?: FormInstance): [FormInstance(null); const [, forceUpdate] = React.useState({}); + // Create singleton FormStore if (!formRef.current) { if (form) { formRef.current = form; diff --git a/src/hooks/useNotifyWatch.ts b/src/hooks/useNotifyWatch.ts new file mode 100644 index 00000000..5beea99e --- /dev/null +++ b/src/hooks/useNotifyWatch.ts @@ -0,0 +1,62 @@ +import { matchNamePath } from '../utils/valueUtil'; +import type { InternalNamePath, WatchCallBack } from '../interface'; +import type { FormStore } from './useForm'; + +/** + * Call action with delay in macro task. + */ +const macroTask = (fn: VoidFunction) => { + const channel = new MessageChannel(); + channel.port1.onmessage = fn; + channel.port2.postMessage(null); +}; + +export default class WatcherCenter { + namePathList: InternalNamePath[] = []; + taskId: number = 0; + + watcherList = new Set(); + form: FormStore; + + constructor(form: FormStore) { + this.form = form; + } + + public register(callback: WatchCallBack): VoidFunction { + this.watcherList.add(callback); + + return () => { + this.watcherList.delete(callback); + }; + } + + public notify(namePath: InternalNamePath[]) { + // Insert with deduplication + namePath.forEach(path => { + if (this.namePathList.every(exist => !matchNamePath(exist, path))) { + this.namePathList.push(path); + } + }); + + this.doBatch(); + } + + private doBatch() { + this.taskId += 1; + const currentId = this.taskId; + + macroTask(() => { + if (currentId === this.taskId && this.watcherList.size) { + const formInst = this.form.getForm(); + const values = formInst.getFieldsValue(); + const allValues = formInst.getFieldsValue(true); + + this.watcherList.forEach(callback => { + callback(values, allValues, this.namePathList); + }); + + this.namePathList = []; + } + }); + } +} diff --git a/src/useWatch.ts b/src/hooks/useWatch.ts similarity index 96% rename from src/useWatch.ts rename to src/hooks/useWatch.ts index 74b10176..c656ef53 100644 --- a/src/useWatch.ts +++ b/src/hooks/useWatch.ts @@ -1,15 +1,15 @@ import warning from '@rc-component/util/lib/warning'; import { useContext, useEffect, useMemo, useRef, useState } from 'react'; -import FieldContext, { HOOK_MARK } from './FieldContext'; +import FieldContext, { HOOK_MARK } from '../FieldContext'; import type { FormInstance, InternalFormInstance, NamePath, Store, WatchOptions, -} from './interface'; -import { isFormInstance } from './utils/typeUtil'; -import { getNamePath, getValue } from './utils/valueUtil'; +} from '../interface'; +import { isFormInstance } from '../utils/typeUtil'; +import { getNamePath, getValue } from '../utils/valueUtil'; import { useEvent } from '@rc-component/util'; type ReturnPromise = T extends Promise ? ValueType : never; diff --git a/src/index.tsx b/src/index.tsx index bf535b72..4433aa77 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,13 +2,13 @@ import * as React from 'react'; import type { FormRef, FormInstance } from './interface'; import Field from './Field'; import List from './List'; -import useForm from './useForm'; +import useForm from './hooks/useForm'; import type { FormProps } from './Form'; import FieldForm from './Form'; import { FormProvider } from './FormContext'; import FieldContext from './FieldContext'; import ListContext from './ListContext'; -import useWatch from './useWatch'; +import useWatch from './hooks/useWatch'; const InternalForm = React.forwardRef(FieldForm) as ( props: FormProps & { ref?: React.Ref> }, diff --git a/src/interface.ts b/src/interface.ts index b8f30b73..926093b0 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -240,7 +240,6 @@ export interface InternalHooks { setValidateMessages: (validateMessages: ValidateMessages) => void; setPreserve: (preserve?: boolean) => void; getInitialValue: (namePath: InternalNamePath) => StoreValue; - setBatchUpdate: (fn: BatchTask) => void; } /** Only return partial when type is not any */ diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 21c123d4..9f4e6426 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -48,7 +48,7 @@ export function containsNamePath( * 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]` + * @param partialMatch Default false. True will make `[a, b]` match `[a, b, c]` */ export function matchNamePath( namePath: InternalNamePath, diff --git a/tests/common/index.ts b/tests/common/index.ts index 23f03605..7f3b43f6 100644 --- a/tests/common/index.ts +++ b/tests/common/index.ts @@ -24,18 +24,26 @@ export function getInput( return ele!; } +const nativeSetTimeout = window.setTimeout; + export async function changeValue(wrapper: HTMLElement, value: string | string[]) { const values = Array.isArray(value) ? value : [value]; + const isMockTimer = nativeSetTimeout !== window.setTimeout; + for (let i = 0; i < values.length; i += 1) { fireEvent.change(wrapper, { target: { value: values[i] } }); - await act(async () => { - await timeout(); - }); + if (isMockTimer) { + act(() => { + jest.advanceTimersByTime(1000); + }); + } else { + await act(async () => { + await timeout(); + }); + } } - - return; } export function matchError( diff --git a/tests/dependencies.test.tsx b/tests/dependencies.test.tsx index a06facd3..deeb0fc7 100644 --- a/tests/dependencies.test.tsx +++ b/tests/dependencies.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import type { FormInstance } from '../src'; import Form, { Field } from '../src'; -import timeout from './common/timeout'; +import timeout, { waitFakeTime } from './common/timeout'; import InfoField, { Input } from './common/InfoField'; import { changeValue, matchError, getInput } from './common'; import { fireEvent, render } from '@testing-library/react'; @@ -100,6 +100,8 @@ describe('Form.Dependencies', () => { }); it('should work when field is dirty', async () => { + jest.useFakeTimers(); + let pass = false; const { container } = render( @@ -135,8 +137,7 @@ describe('Form.Dependencies', () => { ); fireEvent.submit(container.querySelector('form')!); - await timeout(); - // wrapper.update(); + await waitFakeTime(); matchError(getInput(container, 0, true), 'You should not pass'); // Mock new validate @@ -149,6 +150,8 @@ describe('Form.Dependencies', () => { fireEvent.click(container.querySelector('button')!); await changeValue(getInput(container, 1), 'light'); matchError(getInput(container, 0, true), false); + + jest.useRealTimers(); }); it('should work as a shortcut when name is not provided', async () => { diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 24c37a6d..526ba868 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -3,9 +3,9 @@ import { resetWarned } from '@rc-component/util/lib/warning'; import React from 'react'; import type { FormInstance } from '../src'; import Form, { Field, useForm } from '../src'; -import { changeValue, getInput, matchError } from './common'; +import { changeValue, getInput, matchError, waitFakeTimer } from './common'; import InfoField, { Input } from './common/InfoField'; -import timeout from './common/timeout'; +import timeout, { waitFakeTime } from './common/timeout'; import type { FormRef, Meta } from '@/interface'; describe('Form.Basic', () => { @@ -311,6 +311,8 @@ describe('Form.Basic', () => { expect(getInput(container).value).toEqual(''); }); it('submit', async () => { + jest.useFakeTimers(); + const onFinish = jest.fn(); const onFinishFailed = jest.fn(); @@ -324,7 +326,7 @@ describe('Form.Basic', () => { // Not trigger fireEvent.submit(container.querySelector('form')); - await timeout(); + await waitFakeTime(); matchError(container, "'user' is required"); expect(onFinish).not.toHaveBeenCalled(); expect(onFinishFailed).toHaveBeenCalledWith({ @@ -332,7 +334,7 @@ describe('Form.Basic', () => { errorFields: [{ name: ['user'], errors: ["'user' is required"], warnings: [] }], outOfDate: false, values: { - user: undefined + user: undefined, }, }); @@ -342,10 +344,12 @@ describe('Form.Basic', () => { // Trigger await changeValue(getInput(container), 'Bamboo'); fireEvent.submit(container.querySelector('form')); - await timeout(); + await waitFakeTime(); matchError(container, false); expect(onFinish).toHaveBeenCalledWith({ user: 'Bamboo' }); expect(onFinishFailed).not.toHaveBeenCalled(); + + jest.useRealTimers(); }); it('getInternalHooks should not usable by user', () => { @@ -903,7 +907,7 @@ describe('Form.Basic', () => { // (setFieldValue internally calls setFields with touched: true) expect(formRef.current.isFieldTouched(['list', 1])).toBeTruthy(); expect(formRef.current.isFieldTouched(['nest', 'target'])).toBeTruthy(); - + // Verify other fields remain untouched expect(formRef.current.isFieldTouched(['list', 0])).toBeFalsy(); expect(formRef.current.isFieldTouched(['list', 2])).toBeFalsy(); diff --git a/tests/setupAfterEnv.ts b/tests/setupAfterEnv.ts index 7b0828bf..58503f88 100644 --- a/tests/setupAfterEnv.ts +++ b/tests/setupAfterEnv.ts @@ -1 +1,30 @@ import '@testing-library/jest-dom'; + +window.MessageChannel = class { + port1: any; + port2: any; + + constructor() { + const createPort = () => { + const port = { + onmessage: null, + postMessage: (message: any) => { + setTimeout(() => { + if (port._target && typeof port._target.onmessage === 'function') { + port._target.onmessage({ data: message }); + } + }, 10); + }, + _target: null, + }; + return port; + }; + + const port1 = createPort(); + const port2 = createPort(); + port1._target = port2; + port2._target = port1; + this.port1 = port1; + this.port2 = port2; + } +} as any; diff --git a/tests/useWatch.test.tsx b/tests/useWatch.test.tsx index b26dfcf1..ac6080d3 100644 --- a/tests/useWatch.test.tsx +++ b/tests/useWatch.test.tsx @@ -3,12 +3,20 @@ import { render, fireEvent, act } from '@testing-library/react'; import type { FormInstance } from '../src'; import { List } from '../src'; import Form, { Field } from '../src'; -import timeout from './common/timeout'; +import { waitFakeTime } from './common/timeout'; import { Input } from './common/InfoField'; -import { stringify } from '../src/useWatch'; +import { stringify } from '../src/hooks/useWatch'; import { changeValue } from './common'; describe('useWatch', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + it('field initialValue', async () => { const Demo: React.FC = () => { const [form] = Form.useForm(); @@ -26,9 +34,7 @@ describe('useWatch', () => { }; const { container } = render(); - await act(async () => { - await timeout(); - }); + await waitFakeTime(); expect(container.querySelector('.values')?.textContent).toEqual('bamboo'); }); @@ -49,9 +55,7 @@ describe('useWatch', () => { }; const { container } = render(); - await act(async () => { - await timeout(); - }); + await waitFakeTime(); expect(container.querySelector('.values')?.textContent).toEqual('bamboo'); }); @@ -73,23 +77,24 @@ describe('useWatch', () => { }; const { container } = render(); - await act(async () => { - await timeout(); - }); + await waitFakeTime(); await act(async () => { staticForm.current?.setFields([{ name: 'name', value: 'little' }]); }); + await waitFakeTime(); expect(container.querySelector('.values').textContent)?.toEqual('little'); await act(async () => { staticForm.current?.setFieldsValue({ name: 'light' }); }); + await waitFakeTime(); expect(container.querySelector('.values').textContent)?.toEqual('light'); await act(async () => { staticForm.current?.resetFields(); }); + await waitFakeTime(); expect(container.querySelector('.values').textContent)?.toEqual(''); }); @@ -113,17 +118,16 @@ describe('useWatch', () => { }; const { container, rerender } = render(); - - await act(async () => { - await timeout(); - }); + await waitFakeTime(); expect(container.querySelector('.values')?.textContent).toEqual('bamboo'); rerender(); + await waitFakeTime(); expect(container.querySelector('.values')?.textContent).toEqual(''); rerender(); + await waitFakeTime(); expect(container.querySelector('.values')?.textContent).toEqual('bamboo'); }); @@ -152,15 +156,15 @@ describe('useWatch', () => { }; const { container, rerender } = render(); - await act(async () => { - await timeout(); - }); + await waitFakeTime(); expect(container.querySelector('.values')?.textContent).toEqual('bamboo'); rerender(); + await waitFakeTime(); expect(container.querySelector('.values')?.textContent).toEqual(''); rerender(); + await waitFakeTime(); expect(container.querySelector('.values')?.textContent).toEqual('bamboo'); }); }); @@ -195,16 +199,14 @@ describe('useWatch', () => { }; const { container } = render(); - await act(async () => { - await timeout(); - }); + await waitFakeTime(); + expect(container.querySelector('.values')?.textContent).toEqual( JSON.stringify(['bamboo', 'light']), ); fireEvent.click(container.querySelector('.remove')); - await act(async () => { - await timeout(); - }); + await waitFakeTime(); + expect(container.querySelector('.values')?.textContent).toEqual( JSON.stringify(['light']), ); @@ -350,9 +352,7 @@ describe('useWatch', () => { }; const { container } = render(); - fireEvent.change(container.querySelector('input'), { - target: { value: 'bamboo' }, - }); + changeValue(container.querySelector('input'), 'bamboo'); expect(updateA > updateB).toBeTruthy(); }); @@ -382,10 +382,9 @@ describe('useWatch', () => { ); }; const { container } = render(); - fireEvent.change(container.querySelector('input'), { - target: { value: 'bamboo' }, - }); + changeValue(container.querySelector('input'), 'bamboo'); container.querySelector('button').click(); + expect(container.querySelector('.value')?.textContent).toEqual('bamboo'); }); it('stringify error', () => { @@ -416,9 +415,10 @@ describe('useWatch', () => { 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' }, - }); + // fireEvent.change(container.querySelector('input'), { + // target: { value: 'bamboo' }, + // }); + changeValue(container.querySelector('input'), '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.', @@ -481,14 +481,11 @@ describe('useWatch', () => { }; const { container } = render(); - await act(async () => { - await timeout(); - }); + await waitFakeTime(); expect(logSpy).toHaveBeenCalledWith('bamboo', undefined); // initialValue + fireEvent.click(container.querySelector('.test-btn')); - await act(async () => { - await timeout(); - }); + await waitFakeTime(); expect(logSpy).toHaveBeenCalledWith('light', undefined); // after setFieldValue logSpy.mockRestore(); @@ -510,9 +507,8 @@ describe('useWatch', () => { }; const { container } = render(); - await act(async () => { - await timeout(); - }); + await waitFakeTime(); + expect(container.querySelector('.values')?.textContent).toEqual('bamboo'); const input = container.querySelectorAll('input'); await changeValue(input[0], 'bamboo2'); @@ -536,4 +532,47 @@ describe('useWatch', () => { expect(list[0]).toEqual({}); expect(list[1]).toEqual({ name: 'bamboo' }); }); + + it('list remove should not trigger intermediate undefined value', async () => { + let snapshots: any[] = []; + + const Demo: React.FC = () => { + const [form] = Form.useForm(); + const users = Form.useWatch(['users'], form) || []; + snapshots.push(users); + + return ( +
+ + {(fields, { remove }) => ( +
+ {fields.map(field => ( + +
+ + ))} + +
+ )} + + + ); + }; + + const { container } = render(); + await waitFakeTime(); + snapshots = []; + + fireEvent.click(container.querySelector('button'));; + await waitFakeTime(); + + expect(snapshots).toHaveLength(1); + expect(snapshots[0]).toEqual(['bamboo']); + }); }); diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index 2a9d9227..7fb1cdd9 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -288,6 +288,8 @@ describe('Form.Validate', () => { }); it('form context', async () => { + jest.useFakeTimers(); + const { container, rerender } = render(
@@ -299,14 +301,11 @@ describe('Form.Validate', () => { matchError(container, false); // Trigger onBlur - // wrapper.find('input').simulate('blur'); fireEvent.blur(getInput(container)); - await timeout(); - // wrapper.update(); + await waitFakeTime(); matchError(container, true); // Update Form context - // wrapper.setProps({ validateTrigger: 'onChange' }); rerender( @@ -314,6 +313,8 @@ describe('Form.Validate', () => { ); await changeValue(getInput(container), '1'); matchError(container, false); + + jest.useRealTimers(); }); });