Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
getNamePath,
getValue,
} from './utils/valueUtil';
import delayFrame from './utils/delayUtil';

const EMPTY_ERRORS: any[] = [];
const EMPTY_WARNINGS: any[] = [];
Expand Down Expand Up @@ -99,8 +100,10 @@ export interface InternalFieldProps<Values = any> {
fieldContext?: InternalFormInstance;
}

export interface FieldProps<Values = any>
extends Omit<InternalFieldProps<Values>, 'name' | 'fieldContext'> {
export interface FieldProps<Values = any> extends Omit<
InternalFieldProps<Values>,
'name' | 'fieldContext'
> {
name?: NamePath<Values>;
}

Expand Down Expand Up @@ -399,6 +402,10 @@ class Field extends React.Component<InternalFieldProps, FieldState> implements F

const { validateFirst = false, messageVariables, validateDebounce } = this.props;

// Should wait for the frame render,
// since developer may `useWatch` value in the rules.
await delayFrame();

// Start validate
let filteredRules = this.getRules();
if (triggerName) {
Expand Down
9 changes: 9 additions & 0 deletions src/utils/delayUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import raf from '@rc-component/util/lib/raf';

export default async function delayFrame() {
return new Promise<void>(resolve => {
raf(() => {
resolve();
});
});
}
3 changes: 3 additions & 0 deletions tests/__mocks__/src/utils/delayUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default async function delayFrame() {
return Promise.resolve();
}
14 changes: 14 additions & 0 deletions tests/common/timeout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ export default async (timeout: number = 10) => {
};

export async function waitFakeTime(timeout: number = 10) {
await act(async () => {
await new Promise<void>(resolve => {
setTimeout(resolve, 11);
jest.advanceTimersByTime(11);
});
});

await act(async () => {
await new Promise<void>(resolve => {
setTimeout(resolve, 11);
jest.advanceTimersByTime(11);
});
});
Comment on lines +10 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

waitFakeTime 在共享 helper 中无条件多推进 22ms,容易影响无关测试时序。

Line 10-22 的两次固定 11ms 推进会作用于所有调用方,不仅仅是 useWatch 场景;这可能提前触发其它定时逻辑,导致测试“误通过”或时序不稳定。建议把额外帧推进改为可配置参数,仅在需要的用例里显式开启。

可参考的最小改法
 export async function waitFakeTime(timeout: number = 10) {
-  await act(async () => {
-    await new Promise<void>(resolve => {
-      setTimeout(resolve, 11);
-      jest.advanceTimersByTime(11);
-    });
-  });
-
-  await act(async () => {
-    await new Promise<void>(resolve => {
-      setTimeout(resolve, 11);
-      jest.advanceTimersByTime(11);
-    });
-  });
+export async function waitFakeTime(timeout: number = 10, preFrames: number = 0) {
+  for (let i = 0; i < preFrames; i += 1) {
+    await act(async () => {
+      await new Promise<void>(resolve => {
+        setTimeout(resolve, 11);
+        jest.advanceTimersByTime(11);
+      });
+    });
+  }

   await act(async () => {
     await Promise.resolve();
     jest.advanceTimersByTime(timeout);
     await Promise.resolve();
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/common/timeout.ts` around lines 10 - 22, The shared helper waitFakeTime
in tests/common/timeout.ts unconditionally advances fake timers twice by 11ms
(total 22ms), which can affect unrelated test timing; change waitFakeTime to
accept an optional parameter (e.g., extraAdvanceMs or advanceExtraFrames
boolean) defaulting to 0/false and only call the additional
act+jest.advanceTimersByTime when that parameter is truthy, and update any
callers that need the extra frame to pass the flag so other tests keep their
original timing; reference the waitFakeTime helper and the two occurrences of
act + jest.advanceTimersByTime blocks in this file when making the change.


await act(async () => {
await Promise.resolve();
jest.advanceTimersByTime(timeout);
Expand Down
2 changes: 2 additions & 0 deletions tests/context.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
37 changes: 35 additions & 2 deletions tests/dependencies.test.tsx
Original file line number Diff line number Diff line change
@@ -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<FormInstance>();

Expand All @@ -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(
Expand All @@ -56,6 +62,7 @@ describe('Form.Dependencies', () => {

// Not trigger if not touched
await changeValue(getInput(container, 0), 'bamboo');
await waitFakeTime();
expect(validated).toBeTruthy();
});
}
Expand Down Expand Up @@ -253,7 +260,7 @@ describe('Form.Dependencies', () => {

<Field shouldUpdate={() => false}>
{() => {
console.log('render!');

counter += 1;
return null;
}}
Expand All @@ -268,4 +275,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 (
<Form form={form}>
<InfoField name="type">
<Input />
</InfoField>
<InfoField name="name" rules={[{ required: type !== '1' }]} dependencies={['type']}>
<Input />
</InfoField>
</Form>
);
};

const { container } = render(<Demo />);
await changeValue(getInput(container, 1), ['bamboo', '']);
matchError(container, true);

// Change type to make rule true
await changeValue(getInput(container), '1');
matchError(container, false);
});
});
2 changes: 2 additions & 0 deletions tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
4 changes: 2 additions & 2 deletions tests/initialValue.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand Down
2 changes: 2 additions & 0 deletions tests/list.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

为啥要 mock,delayFrame 里面的 raf 不能在 test 中用吗,raf 有什么功能

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raf 见评论描述,测试里 mock 是因为 raf 需要 adv timer。而测试里有很多测试都是从 rc-form 里过来的,所以用的是真实的 timeout 导致测试会卡死。所以对部分老测试使用 mock 来保持时序,其他比较新的测试写法 fakeTimer 的不动也自己过了。


describe('Form.List', () => {
const form = React.createRef<FormInstance>();

Expand Down
2 changes: 2 additions & 0 deletions tests/useWatch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,8 @@ describe('useWatch', () => {
React.useEffect(() => {
console.log(nameValuePreserve, nameValue);
}, [nameValuePreserve, nameValue]);


return (
<div>
<Form form={form} initialValues={{ name: 'bamboo' }} />
Expand Down
2 changes: 2 additions & 0 deletions tests/validate-warning.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<FormInstance>();
Expand Down
2 changes: 2 additions & 0 deletions tests/validate.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<FormInstance>();
Expand Down