Skip to content

Commit c371dd3

Browse files
pierrezimmermannbammdjastrzebski
authored andcommitted
feat: use asyncWrapper for userEvent to prevent act warnings
1 parent 8686fc8 commit c371dd3

File tree

5 files changed

+71
-72
lines changed

5 files changed

+71
-72
lines changed
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import * as React from "react";
2-
import { render, screen, fireEvent } from "@testing-library/react";
3-
import userEvent from "@testing-library/user-event";
1+
import * as React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
44

5-
test("userEvent.click()", async () => {
5+
test('userEvent.click()', async () => {
66
const handleClick = jest.fn();
77

88
render(
@@ -11,12 +11,12 @@ test("userEvent.click()", async () => {
1111
</button>
1212
);
1313

14-
const button = screen.getByText("Click");
14+
const button = screen.getByText('Click');
1515
await userEvent.click(button);
1616
expect(handleClick).toHaveBeenCalledTimes(1);
1717
});
1818

19-
test("fireEvent.click()", () => {
19+
test('fireEvent.click()', () => {
2020
const handleClick = jest.fn();
2121

2222
render(
@@ -25,7 +25,7 @@ test("fireEvent.click()", () => {
2525
</button>
2626
);
2727

28-
const button = screen.getByText("Click");
28+
const button = screen.getByText('Click');
2929
fireEvent.click(button);
3030
expect(handleClick).toHaveBeenCalledTimes(1);
3131
});

src/user-event/__tests__/act.test.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from 'react';
2+
import { Pressable, Text } from 'react-native';
3+
import render from '../../render';
4+
import { userEvent } from '..';
5+
import { screen } from '../../screen';
6+
7+
test('user event disables act environmennt', async () => {
8+
const consoleErrorSpy = jest.spyOn(console, 'error');
9+
jest.useFakeTimers();
10+
const TestComponent = () => {
11+
const [isVisible, setIsVisible] = React.useState(false);
12+
13+
React.useEffect(() => {
14+
setTimeout(() => {
15+
setIsVisible(true);
16+
}, 100);
17+
}, []);
18+
19+
return (
20+
<>
21+
<Pressable testID="pressable" />
22+
{isVisible && <Text />}
23+
</>
24+
);
25+
};
26+
27+
render(<TestComponent />);
28+
29+
await userEvent.press(screen.getByTestId('pressable'));
30+
31+
expect(consoleErrorSpy).not.toHaveBeenCalled();
32+
});

src/user-event/setup/setup.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { PressOptions, press, longPress } from '../press';
44
import { TypeOptions, type } from '../type';
55
import { clear } from '../clear';
66
import { ScrollToOptions, scrollTo } from '../scroll';
7+
import { wait } from '../utils';
8+
import { asyncWrapper } from '../utils/asyncWrapper';
79

810
export interface UserEventSetupOptions {
911
/**
@@ -143,13 +145,33 @@ function createInstance(config: UserEventConfig): UserEventInstance {
143145

144146
// We need to bind these functions, as they access the config through 'this.config'.
145147
const api = {
146-
press: press.bind(instance),
147-
longPress: longPress.bind(instance),
148-
type: type.bind(instance),
149-
clear: clear.bind(instance),
150-
scrollTo: scrollTo.bind(instance),
148+
press: wrapAndBindImpl(instance, press),
149+
longPress: wrapAndBindImpl(instance, longPress),
150+
type: wrapAndBindImpl(instance, type),
151+
clear: wrapAndBindImpl(instance, clear),
152+
scrollTo: wrapAndBindImpl(instance, scrollTo),
151153
};
152154

153155
Object.assign(instance, api);
154156
return instance;
155157
}
158+
159+
// This implementation is sourced from testing-library/user-event
160+
// https://github.com/testing-library/user-event/blob/7a305dee9ab833d6f338d567fc2e862b4838b76a/src/setup/setup.ts#L121
161+
function wrapAndBindImpl<Impl extends (...args: any) => Promise<any>>(
162+
instance: UserEventInstance,
163+
impl: Impl
164+
): Impl {
165+
const method = ((...args: any[]) => {
166+
return asyncWrapper(() =>
167+
// eslint-disable-next-line promise/prefer-await-to-then
168+
impl.apply(instance, args).then(async (ret) => {
169+
await wait(instance.config);
170+
return ret;
171+
})
172+
);
173+
}) as Impl;
174+
175+
Object.defineProperty(method, 'name', { get: () => impl.name });
176+
return method;
177+
}

src/user-event/utils/__tests__/wait.test.tsx

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import React, { useEffect, useState } from 'react';
2-
import { Text } from 'react-native';
31
import { wait } from '../wait';
4-
import render from '../../../render';
5-
import { screen } from '../../../screen';
62

73
beforeEach(() => {
84
jest.useRealTimers();
@@ -63,31 +59,4 @@ describe('wait()', () => {
6359
expect(advanceTimers).not.toHaveBeenCalled();
6460
}
6561
);
66-
67-
it('is wrapped by act', async () => {
68-
const consoleErrorSpy = jest.spyOn(console, 'error');
69-
jest.useFakeTimers();
70-
const TestComponent = () => {
71-
const [isVisible, setIsVisible] = useState(false);
72-
73-
useEffect(() => {
74-
setTimeout(() => {
75-
setIsVisible(true);
76-
}, 100);
77-
}, []);
78-
79-
if (isVisible) {
80-
return <Text>Visible</Text>;
81-
}
82-
83-
return null;
84-
};
85-
86-
render(<TestComponent />);
87-
88-
await wait({ delay: 100, advanceTimers: jest.advanceTimersByTime });
89-
90-
expect(screen.getByText('Visible')).toBeTruthy();
91-
expect(consoleErrorSpy).not.toHaveBeenCalled();
92-
});
9362
});

src/waitFor.ts

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
/* globals jest */
2-
import act, { setReactActEnvironment, getIsReactActEnvironment } from './act';
32
import { getConfig } from './config';
4-
import { flushMicroTasks, flushMicroTasksLegacy } from './flush-micro-tasks';
3+
import { flushMicroTasks } from './flush-micro-tasks';
54
import { ErrorWithStack, copyStackTrace } from './helpers/errors';
65
import {
76
setTimeout,
87
clearTimeout,
98
jestFakeTimersAreEnabled,
109
} from './helpers/timers';
11-
import { checkReactVersionAtLeast } from './react-versions';
10+
import { asyncWrapper } from './user-event/utils/asyncWrapper';
1211

1312
const DEFAULT_INTERVAL = 50;
1413

@@ -199,30 +198,7 @@ export default async function waitFor<T>(
199198
const stackTraceError = new ErrorWithStack('STACK_TRACE_ERROR', waitFor);
200199
const optionsWithStackTrace = { stackTraceError, ...options };
201200

202-
if (checkReactVersionAtLeast(18, 0)) {
203-
const previousActEnvironment = getIsReactActEnvironment();
204-
setReactActEnvironment(false);
205-
206-
try {
207-
const result = await waitForInternal(expectation, optionsWithStackTrace);
208-
// Flush the microtask queue before restoring the `act` environment
209-
await flushMicroTasksLegacy();
210-
return result;
211-
} finally {
212-
setReactActEnvironment(previousActEnvironment);
213-
}
214-
}
215-
216-
if (!checkReactVersionAtLeast(16, 9)) {
217-
return waitForInternal(expectation, optionsWithStackTrace);
218-
}
219-
220-
let result: T;
221-
222-
await act(async () => {
223-
result = await waitForInternal(expectation, optionsWithStackTrace);
224-
});
225-
226-
// Either we have result or `waitFor` threw error
227-
return result!;
201+
return asyncWrapper(() =>
202+
waitForInternal(expectation, optionsWithStackTrace)
203+
);
228204
}

0 commit comments

Comments
 (0)