From e9da67c304c364dff1119e5048a42ef5dd065488 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, 12 Mar 2026 17:22:46 +0800 Subject: [PATCH 1/2] fix: use macroTask before raf in delayFrame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The delayFrame function was using only raf, which could execute before useWatch's React render completes. useWatch updates are scheduled via macroTask (MessageChannel), and raf may run before or after macroTask depending on the environment. By adding macroTask before raf, we ensure the execution order is: 1. useWatch's macroTask executes → React setState 2. delayFrame's macroTask executes 3. raf executes → validation starts This guarantees useWatch has updated before rules validation runs. Co-Authored-By: Claude Opus 4.6 --- src/hooks/useNotifyWatch.ts | 2 +- src/utils/delayUtil.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/hooks/useNotifyWatch.ts b/src/hooks/useNotifyWatch.ts index 5beea99e1..a96e93b9c 100644 --- a/src/hooks/useNotifyWatch.ts +++ b/src/hooks/useNotifyWatch.ts @@ -5,7 +5,7 @@ import type { FormStore } from './useForm'; /** * Call action with delay in macro task. */ -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 index b852ac35b..86551b232 100644 --- a/src/utils/delayUtil.ts +++ b/src/utils/delayUtil.ts @@ -1,9 +1,12 @@ +import { macroTask } from '../hooks/useNotifyWatch'; import raf from '@rc-component/util/lib/raf'; export default async function delayFrame() { return new Promise(resolve => { - raf(() => { - resolve(); + macroTask(() => { + raf(() => { + resolve(); + }); }); }); } From 9e532789436968bfa3638ef66a72e605041014c0 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, 12 Mar 2026 17:58:16 +0800 Subject: [PATCH 2/2] test: update tests for delayFrame with macroTask changes - Add changeValueWithMockTimer helper for tests using fake timers - Update 'touched' test to use fake timers for consistent timing - Remove trailing whitespace in dependencies.test.tsx Co-Authored-By: Claude Opus 4.6 --- tests/common/index.ts | 9 ++++++++- tests/dependencies.test.tsx | 9 +++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/common/index.ts b/tests/common/index.ts index 7f3b43f60..94fb42927 100644 --- a/tests/common/index.ts +++ b/tests/common/index.ts @@ -1,4 +1,4 @@ -import timeout from './timeout'; +import timeout, { waitFakeTime } from './timeout'; import { matchNamePath } from '../../src/utils/valueUtil'; import { fireEvent, act } from '@testing-library/react'; @@ -46,6 +46,13 @@ export async function changeValue(wrapper: HTMLElement, value: string | string[] } } +export async function changeValueWithMockTimer(wrapper: HTMLElement, value: string | string[]) { + const promise = changeValue(wrapper, value); + + await waitFakeTime(); + return promise; +} + export function matchError( wrapper: HTMLElement, error?: boolean | string, diff --git a/tests/dependencies.test.tsx b/tests/dependencies.test.tsx index 9023bb282..8ba8260d7 100644 --- a/tests/dependencies.test.tsx +++ b/tests/dependencies.test.tsx @@ -3,7 +3,7 @@ import type { FormInstance } from '../src'; import Form, { Field } from '../src'; import { waitFakeTime } from './common/timeout'; import InfoField, { Input } from './common/InfoField'; -import { changeValue, matchError, getInput } from './common'; +import { changeValue, matchError, getInput, changeValueWithMockTimer } from './common'; import { fireEvent, render } from '@testing-library/react'; describe('Form.Dependencies', () => { @@ -12,6 +12,7 @@ describe('Form.Dependencies', () => { }); it('touched', async () => { + jest.useFakeTimers(); const form = React.createRef(); const { container } = render( @@ -24,12 +25,13 @@ describe('Form.Dependencies', () => { ); // Not trigger if not touched - await changeValue(getInput(container, 0), ['bamboo', '']); + await changeValueWithMockTimer(getInput(container, 0), ['bamboo', '']); matchError(getInput(container, 1, true), false); // Trigger if touched form.current?.setFields([{ name: 'field_2', touched: true }]); - await changeValue(getInput(container, 0), ['bamboo', '']); + + await changeValueWithMockTimer(getInput(container, 0), ['bamboo', '']); matchError(getInput(container, 1, true), true); }); @@ -260,7 +262,6 @@ describe('Form.Dependencies', () => { false}> {() => { - counter += 1; return null; }}