Skip to content

Commit bf63ab2

Browse files
committed
Switch to IS_REACT_ACT_ENVIRONMENT instead of act when needed when used with react 18
1 parent 10e69c5 commit bf63ab2

File tree

5 files changed

+171
-12
lines changed

5 files changed

+171
-12
lines changed

src/__tests__/waitFor.test.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { Text, TouchableOpacity, View } from 'react-native';
2+
import { Text, TouchableOpacity, View, Pressable } from 'react-native';
33
import { fireEvent, render, waitFor } from '..';
44

55
class Banana extends React.Component<any> {
@@ -78,6 +78,38 @@ test('waits for element with custom interval', async () => {
7878
expect(mockFn).toHaveBeenCalledTimes(2);
7979
});
8080

81+
test('waits for async event with fireEvent', async () => {
82+
const Comp = ({ onPress }: { onPress: () => void }) => {
83+
const [state, setState] = React.useState(false);
84+
85+
React.useEffect(() => {
86+
if (state) {
87+
onPress();
88+
}
89+
}, [state, onPress]);
90+
91+
return (
92+
<Pressable
93+
onPress={async () => {
94+
await Promise.resolve();
95+
setState(true);
96+
}}
97+
>
98+
<Text>Trigger</Text>
99+
</Pressable>
100+
);
101+
};
102+
103+
const spy = jest.fn();
104+
const { getByText } = render(<Comp onPress={spy} />);
105+
106+
fireEvent.press(getByText('Trigger'));
107+
108+
await waitFor(() => {
109+
expect(spy).toHaveBeenCalled();
110+
});
111+
});
112+
81113
test.each([false, true])(
82114
'waits for element until it stops throwing using fake timers (legacyFakeTimers = %s)',
83115
async (legacyFakeTimers) => {

src/act.ts

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,101 @@
1-
import { act } from 'react-test-renderer';
1+
import { act as reactTestRendererAct } from 'react-test-renderer';
2+
import { checkReactVersionAtLeast } from './checkReactVersionAtLeast';
23

34
const actMock = (callback: () => void) => {
45
callback();
56
};
67

7-
export default act || actMock;
8+
type GlobalWithReactActEnvironment = {
9+
IS_REACT_ACT_ENVIRONMENT?: boolean;
10+
} & typeof globalThis;
11+
function getGlobalThis(): GlobalWithReactActEnvironment {
12+
// eslint-disable-next-line no-restricted-globals
13+
if (typeof self !== 'undefined') {
14+
// eslint-disable-next-line no-restricted-globals
15+
return self as GlobalWithReactActEnvironment;
16+
}
17+
if (typeof window !== 'undefined') {
18+
return window;
19+
}
20+
if (typeof global !== 'undefined') {
21+
return global;
22+
}
23+
24+
throw new Error('unable to locate global object');
25+
}
26+
27+
function setIsReactActEnvironment(isReactActEnvironment: boolean | undefined) {
28+
getGlobalThis().IS_REACT_ACT_ENVIRONMENT = isReactActEnvironment;
29+
}
30+
31+
function getIsReactActEnvironment() {
32+
return getGlobalThis().IS_REACT_ACT_ENVIRONMENT;
33+
}
34+
35+
type Act = typeof reactTestRendererAct;
36+
function withGlobalActEnvironment(actImplementation: Act) {
37+
return (callback: Parameters<Act>[0]) => {
38+
const previousActEnvironment = getIsReactActEnvironment();
39+
setIsReactActEnvironment(true);
40+
41+
// this code is riddled with eslint disabling comments because this doesn't use real promises but eslint thinks we do
42+
try {
43+
// The return value of `act` is always a thenable.
44+
let callbackNeedsToBeAwaited = false;
45+
const actResult = actImplementation(() => {
46+
const result = callback();
47+
if (
48+
result !== null &&
49+
typeof result === 'object' &&
50+
// eslint-disable-next-line promise/prefer-await-to-then
51+
typeof (result as any).then === 'function'
52+
) {
53+
callbackNeedsToBeAwaited = true;
54+
}
55+
return result;
56+
});
57+
if (callbackNeedsToBeAwaited) {
58+
const thenable = actResult;
59+
return {
60+
then: (
61+
resolve: (value: never) => never,
62+
reject: (value: never) => never
63+
) => {
64+
// eslint-disable-next-line
65+
thenable.then(
66+
// eslint-disable-next-line promise/always-return
67+
(returnValue) => {
68+
setIsReactActEnvironment(previousActEnvironment);
69+
resolve(returnValue);
70+
},
71+
(error) => {
72+
setIsReactActEnvironment(previousActEnvironment);
73+
reject(error);
74+
}
75+
);
76+
},
77+
};
78+
} else {
79+
setIsReactActEnvironment(previousActEnvironment);
80+
return actResult;
81+
}
82+
} catch (error) {
83+
// Can't be a `finally {}` block since we don't know if we have to immediately restore IS_REACT_ACT_ENVIRONMENT
84+
// or if we have to await the callback first.
85+
setIsReactActEnvironment(previousActEnvironment);
86+
throw error;
87+
}
88+
};
89+
}
90+
91+
const act = reactTestRendererAct
92+
? checkReactVersionAtLeast(18, 0)
93+
? withGlobalActEnvironment(reactTestRendererAct)
94+
: reactTestRendererAct
95+
: actMock;
96+
97+
export default act;
98+
export {
99+
setIsReactActEnvironment as setReactActEnvironment,
100+
getIsReactActEnvironment,
101+
};

src/checkReactVersionAtLeast.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as React from 'react';
2+
3+
export function checkReactVersionAtLeast(
4+
major: number,
5+
minor: number
6+
): boolean {
7+
if (React.version === undefined) return false;
8+
const [actualMajor, actualMinor] = React.version.split('.').map(Number);
9+
10+
return actualMajor > major || (actualMajor === major && actualMinor >= minor);
11+
}

src/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { cleanup } from './pure';
22
import { flushMicroTasks } from './flushMicroTasks';
3+
import { getIsReactActEnvironment, setReactActEnvironment } from './act';
34

45
// If we're running in a test runner that supports afterEach
56
// then we'll automatically run cleanup afterEach test
@@ -14,4 +15,21 @@ if (typeof afterEach === 'function' && !process.env.RNTL_SKIP_AUTO_CLEANUP) {
1415
});
1516
}
1617

18+
if (
19+
typeof beforeAll === 'function' &&
20+
typeof afterAll === 'function' &&
21+
!process.env.RNTL_SKIP_AUTO_CLEANUP
22+
) {
23+
// This matches the behavior of React < 18.
24+
let previousIsReactActEnvironment = getIsReactActEnvironment();
25+
beforeAll(() => {
26+
previousIsReactActEnvironment = getIsReactActEnvironment();
27+
setReactActEnvironment(true);
28+
});
29+
30+
afterAll(() => {
31+
setReactActEnvironment(previousIsReactActEnvironment);
32+
});
33+
}
34+
1735
export * from './pure';

src/waitFor.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,17 @@
11
/* globals jest */
2-
import * as React from 'react';
3-
import act from './act';
2+
import act, { setReactActEnvironment, getIsReactActEnvironment } from './act';
43
import { ErrorWithStack, copyStackTrace } from './helpers/errors';
54
import {
65
setTimeout,
76
clearTimeout,
87
setImmediate,
98
jestFakeTimersAreEnabled,
109
} from './helpers/timers';
10+
import { checkReactVersionAtLeast } from './checkReactVersionAtLeast';
1111

1212
const DEFAULT_TIMEOUT = 1000;
1313
const DEFAULT_INTERVAL = 50;
1414

15-
function checkReactVersionAtLeast(major: number, minor: number): boolean {
16-
if (React.version === undefined) return false;
17-
const [actualMajor, actualMinor] = React.version.split('.').map(Number);
18-
19-
return actualMajor > major || (actualMajor === major && actualMinor >= minor);
20-
}
21-
2215
export type WaitForOptions = {
2316
timeout?: number;
2417
interval?: number;
@@ -198,6 +191,17 @@ export default async function waitFor<T>(
198191
return waitForInternal(expectation, optionsWithStackTrace);
199192
}
200193

194+
if (checkReactVersionAtLeast(18, 0)) {
195+
const previousActEnvironment = getIsReactActEnvironment();
196+
setReactActEnvironment(false);
197+
198+
try {
199+
return await waitForInternal(expectation, optionsWithStackTrace);
200+
} finally {
201+
setReactActEnvironment(previousActEnvironment);
202+
}
203+
}
204+
201205
let result: T;
202206

203207
await act(async () => {

0 commit comments

Comments
 (0)