From 9a72b49e36e9c19e2eb5b88bdde966e057429e68 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, 3 Mar 2026 14:46:19 +0800 Subject: [PATCH 1/7] test: test driven --- tests/dependencies.test.tsx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/dependencies.test.tsx b/tests/dependencies.test.tsx index deeb0fc74..b8cbbc0d0 100644 --- a/tests/dependencies.test.tsx +++ b/tests/dependencies.test.tsx @@ -268,4 +268,30 @@ describe('Form.Dependencies', () => { expect(container.querySelectorAll('input')).toHaveLength(1); expect(counter).toEqual(1); }); + + it('error should be cleared when dependency field changes and rule becomes false', async () => { + const Demo = () => { + const [form] = Form.useForm(); + const type = Form.useWatch('type', form); + + return ( +
+ + + + + + +
+ ); + }; + + const { container } = render(); + await changeValue(getInput(container, 1), ['bamboo', '']); + matchError(container, true); + + // Change type to make rule true + await changeValue(getInput(container), '1'); + matchError(container, false); + }); }); From 026e60939eee4f345e03f4332d1450191c5f32b9 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, 3 Mar 2026 14:49:31 +0800 Subject: [PATCH 2/7] test: test driven --- tests/dependencies.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dependencies.test.tsx b/tests/dependencies.test.tsx index b8cbbc0d0..3223848a9 100644 --- a/tests/dependencies.test.tsx +++ b/tests/dependencies.test.tsx @@ -279,7 +279,7 @@ describe('Form.Dependencies', () => { - + From aabcf72d7c16b5a98c693959ff5831775ef18cc0 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, 3 Mar 2026 16:40:05 +0800 Subject: [PATCH 3/7] feat: await next frame before validation to support useWatch in rules Co-Authored-By: Claude Opus 4.6 --- src/Field.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Field.tsx b/src/Field.tsx index 69e680ce8..c5ea46e7e 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -2,6 +2,7 @@ import toChildrenArray from '@rc-component/util/lib/Children/toArray'; import isEqual from '@rc-component/util/lib/isEqual'; import warning from '@rc-component/util/lib/warning'; import * as React from 'react'; +import raf from '@rc-component/util/lib/raf'; import FieldContext, { HOOK_MARK } from './FieldContext'; import type { EventArgs, @@ -99,8 +100,10 @@ export interface InternalFieldProps { fieldContext?: InternalFormInstance; } -export interface FieldProps - extends Omit, 'name' | 'fieldContext'> { +export interface FieldProps extends Omit< + InternalFieldProps, + 'name' | 'fieldContext' +> { name?: NamePath; } @@ -399,6 +402,10 @@ class Field extends React.Component implements F const { validateFirst = false, messageVariables, validateDebounce } = this.props; + // Should wait for the frame render, + // since developer may `useWatch` value in the rules. + await new Promise(resolve => raf(() => resolve())); + // Start validate let filteredRules = this.getRules(); if (triggerName) { From ffed1b2b1d6ca053a474ef307c9e170ba25e61b3 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, 3 Mar 2026 17:23:09 +0800 Subject: [PATCH 4/7] refactor: replace raf with custom delayFrame for better test control - Replace @rc-component/util/lib/raf with custom delayFrame utility - Export macroTask for test usage - Add delayUtil.ts with mockable delayFrame implementation - Update tests to use fake timers and waitFakeTime for better control - Add jest.mock for delayUtil in test files Co-Authored-By: Claude Opus 4.6 --- src/Field.tsx | 4 ++-- src/hooks/useNotifyWatch.ts | 3 ++- src/utils/delayUtil.ts | 9 +++++++++ tests/__mocks__/src/utils/delayUtil.ts | 4 ++++ tests/common/timeout.ts | 8 ++++++++ tests/context.test.tsx | 2 ++ tests/dependencies.test.tsx | 9 ++++++++- tests/index.test.tsx | 2 ++ tests/list.test.tsx | 2 ++ tests/validate-warning.test.tsx | 2 ++ tests/validate.test.tsx | 2 ++ 11 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 src/utils/delayUtil.ts create mode 100644 tests/__mocks__/src/utils/delayUtil.ts diff --git a/src/Field.tsx b/src/Field.tsx index c5ea46e7e..be5a13c2a 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -2,7 +2,6 @@ import toChildrenArray from '@rc-component/util/lib/Children/toArray'; import isEqual from '@rc-component/util/lib/isEqual'; import warning from '@rc-component/util/lib/warning'; import * as React from 'react'; -import raf from '@rc-component/util/lib/raf'; import FieldContext, { HOOK_MARK } from './FieldContext'; import type { EventArgs, @@ -29,6 +28,7 @@ import { getNamePath, getValue, } from './utils/valueUtil'; +import delayFrame from './utils/delayUtil'; const EMPTY_ERRORS: any[] = []; const EMPTY_WARNINGS: any[] = []; @@ -404,7 +404,7 @@ class Field extends React.Component implements F // Should wait for the frame render, // since developer may `useWatch` value in the rules. - await new Promise(resolve => raf(() => resolve())); + await delayFrame(); // Start validate let filteredRules = this.getRules(); diff --git a/src/hooks/useNotifyWatch.ts b/src/hooks/useNotifyWatch.ts index 5beea99e1..5ddf963b7 100644 --- a/src/hooks/useNotifyWatch.ts +++ b/src/hooks/useNotifyWatch.ts @@ -4,8 +4,9 @@ import type { FormStore } from './useForm'; /** * Call action with delay in macro task. + * export for test usage. */ -const macroTask = (fn: VoidFunction) => { +export const macroTask = (fn: VoidFunction) => { const channel = new MessageChannel(); channel.port1.onmessage = fn; channel.port2.postMessage(null); diff --git a/src/utils/delayUtil.ts b/src/utils/delayUtil.ts new file mode 100644 index 000000000..b852ac35b --- /dev/null +++ b/src/utils/delayUtil.ts @@ -0,0 +1,9 @@ +import raf from '@rc-component/util/lib/raf'; + +export default async function delayFrame() { + return new Promise(resolve => { + raf(() => { + resolve(); + }); + }); +} diff --git a/tests/__mocks__/src/utils/delayUtil.ts b/tests/__mocks__/src/utils/delayUtil.ts new file mode 100644 index 000000000..f6fcabd07 --- /dev/null +++ b/tests/__mocks__/src/utils/delayUtil.ts @@ -0,0 +1,4 @@ +export default async function delayFrame() { + console.log('@@@'); + return Promise.resolve(); +} diff --git a/tests/common/timeout.ts b/tests/common/timeout.ts index 1b2e52cb0..d2c8eb46e 100644 --- a/tests/common/timeout.ts +++ b/tests/common/timeout.ts @@ -1,3 +1,4 @@ +import { macroTask } from '../../src/hooks/useNotifyWatch'; import { act } from '@testing-library/react'; export default async (timeout: number = 10) => { @@ -7,6 +8,13 @@ export default async (timeout: number = 10) => { }; export async function waitFakeTime(timeout: number = 10) { + await act(async () => { + await new Promise(resolve => { + macroTask(resolve); + jest.advanceTimersByTime(11); + }); + }); + await act(async () => { await Promise.resolve(); jest.advanceTimersByTime(timeout); diff --git a/tests/context.test.tsx b/tests/context.test.tsx index 4a587c9f3..2a68ab138 100644 --- a/tests/context.test.tsx +++ b/tests/context.test.tsx @@ -6,6 +6,8 @@ import InfoField from './common/InfoField'; import { changeValue, matchError, getInput } from './common'; import timeout from './common/timeout'; +jest.mock('../src/utils/delayUtil'); + describe('Form.Context', () => { it('validateMessages', async () => { const { container } = render( diff --git a/tests/dependencies.test.tsx b/tests/dependencies.test.tsx index 3223848a9..76804d9c3 100644 --- a/tests/dependencies.test.tsx +++ b/tests/dependencies.test.tsx @@ -1,12 +1,16 @@ import React from 'react'; import type { FormInstance } from '../src'; import Form, { Field } from '../src'; -import timeout, { waitFakeTime } from './common/timeout'; +import { waitFakeTime } from './common/timeout'; import InfoField, { Input } from './common/InfoField'; import { changeValue, matchError, getInput } from './common'; import { fireEvent, render } from '@testing-library/react'; describe('Form.Dependencies', () => { + afterEach(() => { + jest.useRealTimers(); + }); + it('touched', async () => { const form = React.createRef(); @@ -32,6 +36,8 @@ describe('Form.Dependencies', () => { describe('initialValue', () => { function test(name: string, formProps = {}, fieldProps = {}) { it(name, async () => { + jest.useFakeTimers(); + let validated = false; const { container } = render( @@ -56,6 +62,7 @@ describe('Form.Dependencies', () => { // Not trigger if not touched await changeValue(getInput(container, 0), 'bamboo'); + await waitFakeTime(); expect(validated).toBeTruthy(); }); } diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 35cf0e232..8cafc91fb 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -6,6 +6,8 @@ import Form, { Field, useForm } from '../src'; import { changeValue, getInput, matchError } from './common'; import InfoField, { Input } from './common/InfoField'; import timeout, { waitFakeTime } from './common/timeout'; + +jest.mock('../src/utils/delayUtil'); import type { FormRef, Meta } from '@/interface'; describe('Form.Basic', () => { diff --git a/tests/list.test.tsx b/tests/list.test.tsx index 9aac91b39..a491b83ae 100644 --- a/tests/list.test.tsx +++ b/tests/list.test.tsx @@ -10,6 +10,8 @@ import InfoField, { Input } from './common/InfoField'; import { changeValue, getInput } from './common'; import timeout from './common/timeout'; +jest.mock('../src/utils/delayUtil'); + describe('Form.List', () => { const form = React.createRef(); diff --git a/tests/validate-warning.test.tsx b/tests/validate-warning.test.tsx index 84b0b6a8b..4a142c404 100644 --- a/tests/validate-warning.test.tsx +++ b/tests/validate-warning.test.tsx @@ -5,6 +5,8 @@ import { changeValue, getInput, matchError } from './common'; import type { FormInstance, Rule } from '../src/interface'; import { render } from '@testing-library/react'; +jest.mock('../src/utils/delayUtil'); + describe('Form.WarningValidate', () => { it('required', async () => { const form = React.createRef(); diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index 7fb1cdd99..a76e2dd8f 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -6,6 +6,8 @@ import { changeValue, getInput, matchError } from './common'; import InfoField, { Input } from './common/InfoField'; import timeout, { waitFakeTime } from './common/timeout'; +jest.mock('../src/utils/delayUtil'); + describe('Form.Validate', () => { it('required', async () => { const form = React.createRef(); From 6f5d2c4fbefb6a450a608e8a49609f1859a72f96 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, 3 Mar 2026 17:43:56 +0800 Subject: [PATCH 5/7] chore: remove debug console.log statements --- tests/__mocks__/src/utils/delayUtil.ts | 2 +- tests/dependencies.test.tsx | 2 +- tests/initialValue.test.tsx | 4 ++-- tests/useWatch.test.tsx | 2 ++ 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/__mocks__/src/utils/delayUtil.ts b/tests/__mocks__/src/utils/delayUtil.ts index f6fcabd07..9e813130c 100644 --- a/tests/__mocks__/src/utils/delayUtil.ts +++ b/tests/__mocks__/src/utils/delayUtil.ts @@ -1,4 +1,4 @@ export default async function delayFrame() { - console.log('@@@'); + return Promise.resolve(); return Promise.resolve(); } diff --git a/tests/dependencies.test.tsx b/tests/dependencies.test.tsx index 76804d9c3..9023bb282 100644 --- a/tests/dependencies.test.tsx +++ b/tests/dependencies.test.tsx @@ -260,7 +260,7 @@ describe('Form.Dependencies', () => { false}> {() => { - console.log('render!'); + counter += 1; return null; }} diff --git a/tests/initialValue.test.tsx b/tests/initialValue.test.tsx index bc5f0a105..6c06a9623 100644 --- a/tests/initialValue.test.tsx +++ b/tests/initialValue.test.tsx @@ -165,11 +165,11 @@ describe('Form.InitialValues', () => { fireEvent.click(container.querySelector('button')); expect(formValue.users[0].last).toEqual('bbb'); - console.log('Form Value:', refForm.getFieldsValue(true)); + fireEvent.click(container.querySelector('button')); expect(formValue.users[0].last).toEqual('bbb'); - console.log('Form Value:', refForm.getFieldsValue(true)); + fireEvent.click(container.querySelector('button')); diff --git a/tests/useWatch.test.tsx b/tests/useWatch.test.tsx index ac6080d3a..b3d997ff7 100644 --- a/tests/useWatch.test.tsx +++ b/tests/useWatch.test.tsx @@ -471,6 +471,8 @@ describe('useWatch', () => { React.useEffect(() => { console.log(nameValuePreserve, nameValue); }, [nameValuePreserve, nameValue]); + + return (
From 1a342508ec4344a6358a1b123ad44584b9c110d5 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, 3 Mar 2026 17:50:21 +0800 Subject: [PATCH 6/7] refactor: remove macroTask export and use setTimeout in tests --- src/hooks/useNotifyWatch.ts | 3 +-- tests/common/timeout.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/hooks/useNotifyWatch.ts b/src/hooks/useNotifyWatch.ts index 5ddf963b7..5beea99e1 100644 --- a/src/hooks/useNotifyWatch.ts +++ b/src/hooks/useNotifyWatch.ts @@ -4,9 +4,8 @@ import type { FormStore } from './useForm'; /** * Call action with delay in macro task. - * export for test usage. */ -export const macroTask = (fn: VoidFunction) => { +const macroTask = (fn: VoidFunction) => { const channel = new MessageChannel(); channel.port1.onmessage = fn; channel.port2.postMessage(null); diff --git a/tests/common/timeout.ts b/tests/common/timeout.ts index d2c8eb46e..ad44dcd49 100644 --- a/tests/common/timeout.ts +++ b/tests/common/timeout.ts @@ -1,4 +1,3 @@ -import { macroTask } from '../../src/hooks/useNotifyWatch'; import { act } from '@testing-library/react'; export default async (timeout: number = 10) => { @@ -10,7 +9,14 @@ export default async (timeout: number = 10) => { export async function waitFakeTime(timeout: number = 10) { await act(async () => { await new Promise(resolve => { - macroTask(resolve); + setTimeout(resolve, 11); + jest.advanceTimersByTime(11); + }); + }); + + await act(async () => { + await new Promise(resolve => { + setTimeout(resolve, 11); jest.advanceTimersByTime(11); }); }); From 8c0135ded2bd757555074c4b2df98ad880473467 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, 3 Mar 2026 17:56:57 +0800 Subject: [PATCH 7/7] fix: remove duplicate line in mock file --- tests/__mocks__/src/utils/delayUtil.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/__mocks__/src/utils/delayUtil.ts b/tests/__mocks__/src/utils/delayUtil.ts index 9e813130c..b694c6bc7 100644 --- a/tests/__mocks__/src/utils/delayUtil.ts +++ b/tests/__mocks__/src/utils/delayUtil.ts @@ -1,4 +1,3 @@ export default async function delayFrame() { return Promise.resolve(); - return Promise.resolve(); }