Skip to content

Commit ceb07ef

Browse files
zombieJclaude
andauthored
fix: Rule deps on the useWatch (#776)
* test: test driven * test: test driven * feat: await next frame before validation to support useWatch in rules Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * chore: remove debug console.log statements * refactor: remove macroTask export and use setTimeout in tests * fix: remove duplicate line in mock file --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fa2483d commit ceb07ef

File tree

12 files changed

+84
-6
lines changed

12 files changed

+84
-6
lines changed

src/Field.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
getNamePath,
2929
getValue,
3030
} from './utils/valueUtil';
31+
import delayFrame from './utils/delayUtil';
3132

3233
const EMPTY_ERRORS: any[] = [];
3334
const EMPTY_WARNINGS: any[] = [];
@@ -99,8 +100,10 @@ export interface InternalFieldProps<Values = any> {
99100
fieldContext?: InternalFormInstance;
100101
}
101102

102-
export interface FieldProps<Values = any>
103-
extends Omit<InternalFieldProps<Values>, 'name' | 'fieldContext'> {
103+
export interface FieldProps<Values = any> extends Omit<
104+
InternalFieldProps<Values>,
105+
'name' | 'fieldContext'
106+
> {
104107
name?: NamePath<Values>;
105108
}
106109

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

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

405+
// Should wait for the frame render,
406+
// since developer may `useWatch` value in the rules.
407+
await delayFrame();
408+
402409
// Start validate
403410
let filteredRules = this.getRules();
404411
if (triggerName) {

src/utils/delayUtil.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import raf from '@rc-component/util/lib/raf';
2+
3+
export default async function delayFrame() {
4+
return new Promise<void>(resolve => {
5+
raf(() => {
6+
resolve();
7+
});
8+
});
9+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default async function delayFrame() {
2+
return Promise.resolve();
3+
}

tests/common/timeout.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ export default async (timeout: number = 10) => {
77
};
88

99
export async function waitFakeTime(timeout: number = 10) {
10+
await act(async () => {
11+
await new Promise<void>(resolve => {
12+
setTimeout(resolve, 11);
13+
jest.advanceTimersByTime(11);
14+
});
15+
});
16+
17+
await act(async () => {
18+
await new Promise<void>(resolve => {
19+
setTimeout(resolve, 11);
20+
jest.advanceTimersByTime(11);
21+
});
22+
});
23+
1024
await act(async () => {
1125
await Promise.resolve();
1226
jest.advanceTimersByTime(timeout);

tests/context.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import InfoField from './common/InfoField';
66
import { changeValue, matchError, getInput } from './common';
77
import timeout from './common/timeout';
88

9+
jest.mock('../src/utils/delayUtil');
10+
911
describe('Form.Context', () => {
1012
it('validateMessages', async () => {
1113
const { container } = render(

tests/dependencies.test.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import React from 'react';
22
import type { FormInstance } from '../src';
33
import Form, { Field } from '../src';
4-
import timeout, { waitFakeTime } from './common/timeout';
4+
import { waitFakeTime } from './common/timeout';
55
import InfoField, { Input } from './common/InfoField';
66
import { changeValue, matchError, getInput } from './common';
77
import { fireEvent, render } from '@testing-library/react';
88

99
describe('Form.Dependencies', () => {
10+
afterEach(() => {
11+
jest.useRealTimers();
12+
});
13+
1014
it('touched', async () => {
1115
const form = React.createRef<FormInstance>();
1216

@@ -32,6 +36,8 @@ describe('Form.Dependencies', () => {
3236
describe('initialValue', () => {
3337
function test(name: string, formProps = {}, fieldProps = {}) {
3438
it(name, async () => {
39+
jest.useFakeTimers();
40+
3541
let validated = false;
3642

3743
const { container } = render(
@@ -56,6 +62,7 @@ describe('Form.Dependencies', () => {
5662

5763
// Not trigger if not touched
5864
await changeValue(getInput(container, 0), 'bamboo');
65+
await waitFakeTime();
5966
expect(validated).toBeTruthy();
6067
});
6168
}
@@ -253,7 +260,7 @@ describe('Form.Dependencies', () => {
253260

254261
<Field shouldUpdate={() => false}>
255262
{() => {
256-
console.log('render!');
263+
257264
counter += 1;
258265
return null;
259266
}}
@@ -268,4 +275,30 @@ describe('Form.Dependencies', () => {
268275
expect(container.querySelectorAll('input')).toHaveLength(1);
269276
expect(counter).toEqual(1);
270277
});
278+
279+
it('error should be cleared when dependency field changes and rule becomes false', async () => {
280+
const Demo = () => {
281+
const [form] = Form.useForm();
282+
const type = Form.useWatch('type', form);
283+
284+
return (
285+
<Form form={form}>
286+
<InfoField name="type">
287+
<Input />
288+
</InfoField>
289+
<InfoField name="name" rules={[{ required: type !== '1' }]} dependencies={['type']}>
290+
<Input />
291+
</InfoField>
292+
</Form>
293+
);
294+
};
295+
296+
const { container } = render(<Demo />);
297+
await changeValue(getInput(container, 1), ['bamboo', '']);
298+
matchError(container, true);
299+
300+
// Change type to make rule true
301+
await changeValue(getInput(container), '1');
302+
matchError(container, false);
303+
});
271304
});

tests/index.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import Form, { Field, useForm } from '../src';
66
import { changeValue, getInput, matchError } from './common';
77
import InfoField, { Input } from './common/InfoField';
88
import timeout, { waitFakeTime } from './common/timeout';
9+
10+
jest.mock('../src/utils/delayUtil');
911
import type { FormRef, Meta } from '@/interface';
1012

1113
describe('Form.Basic', () => {

tests/initialValue.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,11 @@ describe('Form.InitialValues', () => {
165165

166166
fireEvent.click(container.querySelector('button'));
167167
expect(formValue.users[0].last).toEqual('bbb');
168-
console.log('Form Value:', refForm.getFieldsValue(true));
168+
169169

170170
fireEvent.click(container.querySelector('button'));
171171
expect(formValue.users[0].last).toEqual('bbb');
172-
console.log('Form Value:', refForm.getFieldsValue(true));
172+
173173

174174
fireEvent.click(container.querySelector('button'));
175175

tests/list.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import InfoField, { Input } from './common/InfoField';
1010
import { changeValue, getInput } from './common';
1111
import timeout from './common/timeout';
1212

13+
jest.mock('../src/utils/delayUtil');
14+
1315
describe('Form.List', () => {
1416
const form = React.createRef<FormInstance>();
1517

tests/useWatch.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,8 @@ describe('useWatch', () => {
471471
React.useEffect(() => {
472472
console.log(nameValuePreserve, nameValue);
473473
}, [nameValuePreserve, nameValue]);
474+
475+
474476
return (
475477
<div>
476478
<Form form={form} initialValues={{ name: 'bamboo' }} />

0 commit comments

Comments
 (0)