Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to IS_REACT_ACT_ENVIRONMENT instead of act when needed when using react 18 #1131

Merged
merged 5 commits into from
Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 33 additions & 1 deletion src/__tests__/waitFor.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { Text, TouchableOpacity, View, Pressable } from 'react-native';
import { fireEvent, render, waitFor } from '..';

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

test('waits for async event with fireEvent', async () => {
const Comp = ({ onPress }: { onPress: () => void }) => {
AugustinLF marked this conversation as resolved.
Show resolved Hide resolved
const [state, setState] = React.useState(false);

React.useEffect(() => {
if (state) {
onPress();
}
}, [state, onPress]);

return (
<Pressable
onPress={async () => {
await Promise.resolve();
setState(true);
AugustinLF marked this conversation as resolved.
Show resolved Hide resolved
}}
>
<Text>Trigger</Text>
</Pressable>
);
};

const spy = jest.fn();
const { getByText } = render(<Comp onPress={spy} />);

fireEvent.press(getByText('Trigger'));

await waitFor(() => {
expect(spy).toHaveBeenCalled();
});
});

test.each([false, true])(
'waits for element until it stops throwing using fake timers (legacyFakeTimers = %s)',
async (legacyFakeTimers) => {
Expand Down
98 changes: 96 additions & 2 deletions src/act.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,101 @@
import { act } from 'react-test-renderer';
import { act as reactTestRendererAct } from 'react-test-renderer';
AugustinLF marked this conversation as resolved.
Show resolved Hide resolved
import { checkReactVersionAtLeast } from './checkReactVersionAtLeast';

const actMock = (callback: () => void) => {
callback();
};

export default act || actMock;
type GlobalWithReactActEnvironment = {
AugustinLF marked this conversation as resolved.
Show resolved Hide resolved
IS_REACT_ACT_ENVIRONMENT?: boolean;
} & typeof globalThis;
AugustinLF marked this conversation as resolved.
Show resolved Hide resolved
function getGlobalThis(): GlobalWithReactActEnvironment {
// eslint-disable-next-line no-restricted-globals
AugustinLF marked this conversation as resolved.
Show resolved Hide resolved
if (typeof self !== 'undefined') {
// eslint-disable-next-line no-restricted-globals
return self as GlobalWithReactActEnvironment;
}
if (typeof window !== 'undefined') {
return window;
}
if (typeof global !== 'undefined') {
return global;
}

throw new Error('unable to locate global object');
}

function setIsReactActEnvironment(isReactActEnvironment: boolean | undefined) {
getGlobalThis().IS_REACT_ACT_ENVIRONMENT = isReactActEnvironment;
}

function getIsReactActEnvironment() {
return getGlobalThis().IS_REACT_ACT_ENVIRONMENT;
}

type Act = typeof reactTestRendererAct;
AugustinLF marked this conversation as resolved.
Show resolved Hide resolved
function withGlobalActEnvironment(actImplementation: Act) {
return (callback: Parameters<Act>[0]) => {
const previousActEnvironment = getIsReactActEnvironment();
setIsReactActEnvironment(true);

// this code is riddled with eslint disabling comments because this doesn't use real promises but eslint thinks we do
try {
// The return value of `act` is always a thenable.
let callbackNeedsToBeAwaited = false;
const actResult = actImplementation(() => {
const result = callback();
if (
result !== null &&
typeof result === 'object' &&
// eslint-disable-next-line promise/prefer-await-to-then
typeof (result as any).then === 'function'
AugustinLF marked this conversation as resolved.
Show resolved Hide resolved
) {
callbackNeedsToBeAwaited = true;
}
return result;
});
if (callbackNeedsToBeAwaited) {
const thenable = actResult;
return {
then: (
resolve: (value: never) => never,
reject: (value: never) => never
AugustinLF marked this conversation as resolved.
Show resolved Hide resolved
) => {
// eslint-disable-next-line
thenable.then(
// eslint-disable-next-line promise/always-return
(returnValue) => {
setIsReactActEnvironment(previousActEnvironment);
resolve(returnValue);
},
(error) => {
setIsReactActEnvironment(previousActEnvironment);
reject(error);
}
);
},
};
} else {
setIsReactActEnvironment(previousActEnvironment);
return actResult;
}
} catch (error) {
// Can't be a `finally {}` block since we don't know if we have to immediately restore IS_REACT_ACT_ENVIRONMENT
// or if we have to await the callback first.
setIsReactActEnvironment(previousActEnvironment);
throw error;
}
};
}

const act = reactTestRendererAct
AugustinLF marked this conversation as resolved.
Show resolved Hide resolved
? checkReactVersionAtLeast(18, 0)
? withGlobalActEnvironment(reactTestRendererAct)
: reactTestRendererAct
: actMock;

export default act;
export {
setIsReactActEnvironment as setReactActEnvironment,
getIsReactActEnvironment,
};
11 changes: 11 additions & 0 deletions src/checkReactVersionAtLeast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as React from 'react';

export function checkReactVersionAtLeast(
AugustinLF marked this conversation as resolved.
Show resolved Hide resolved
major: number,
minor: number
): boolean {
if (React.version === undefined) return false;
const [actualMajor, actualMinor] = React.version.split('.').map(Number);

return actualMajor > major || (actualMajor === major && actualMinor >= minor);
}
18 changes: 18 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { cleanup } from './pure';
import { flushMicroTasks } from './flushMicroTasks';
import { getIsReactActEnvironment, setReactActEnvironment } from './act';

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

if (
typeof beforeAll === 'function' &&
typeof afterAll === 'function' &&
!process.env.RNTL_SKIP_AUTO_CLEANUP
AugustinLF marked this conversation as resolved.
Show resolved Hide resolved
) {
// This matches the behavior of React < 18.
let previousIsReactActEnvironment = getIsReactActEnvironment();
beforeAll(() => {
previousIsReactActEnvironment = getIsReactActEnvironment();
setReactActEnvironment(true);
});

afterAll(() => {
setReactActEnvironment(previousIsReactActEnvironment);
});
}

export * from './pure';
22 changes: 13 additions & 9 deletions src/waitFor.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
/* globals jest */
import * as React from 'react';
import act from './act';
import act, { setReactActEnvironment, getIsReactActEnvironment } from './act';
import { ErrorWithStack, copyStackTrace } from './helpers/errors';
import {
setTimeout,
clearTimeout,
setImmediate,
jestFakeTimersAreEnabled,
} from './helpers/timers';
import { checkReactVersionAtLeast } from './checkReactVersionAtLeast';

const DEFAULT_TIMEOUT = 1000;
const DEFAULT_INTERVAL = 50;

function checkReactVersionAtLeast(major: number, minor: number): boolean {
if (React.version === undefined) return false;
const [actualMajor, actualMinor] = React.version.split('.').map(Number);

return actualMajor > major || (actualMajor === major && actualMinor >= minor);
}

export type WaitForOptions = {
timeout?: number;
interval?: number;
Expand Down Expand Up @@ -198,6 +191,17 @@ export default async function waitFor<T>(
return waitForInternal(expectation, optionsWithStackTrace);
}

if (checkReactVersionAtLeast(18, 0)) {
AugustinLF marked this conversation as resolved.
Show resolved Hide resolved
const previousActEnvironment = getIsReactActEnvironment();
setReactActEnvironment(false);

try {
return await waitForInternal(expectation, optionsWithStackTrace);
} finally {
setReactActEnvironment(previousActEnvironment);
}
}

let result: T;

await act(async () => {
Expand Down