Skip to content
Merged
10 changes: 10 additions & 0 deletions jest/preset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const reactNativePreset = require('react-native/jest-preset');
Copy link

@sbalay sbalay Mar 2, 2021

Choose a reason for hiding this comment

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

Is this being exposed as a jest preset of @testing-library/react-native or is it just used locally for the tests in this codebase? If exposed, it should be documented somewhere

Copy link
Member

Choose a reason for hiding this comment

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

Yup, will do in a followup


module.exports = {
...reactNativePreset,
// this is needed to make modern fake timers work
// because the react-native preset overrides global.Promise
setupFiles: [require.resolve('./save-promise.js')]
.concat(reactNativePreset.setupFiles)
.concat([require.resolve('./restore-promise.js')]),
};
1 change: 1 addition & 0 deletions jest/restore-promise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global.Promise = global.RNTL_ORIGINAL_PROMISE;
1 change: 1 addition & 0 deletions jest/save-promise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global.RNTL_ORIGINAL_PROMISE = Promise;
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,12 @@
"build": "rm -rf build; babel src --out-dir build --ignore 'src/__tests__/*'"
},
"jest": {
"preset": "react-native",
"preset": "../jest/preset.js",
"moduleFileExtensions": [
"js",
"json"
],
"rootDir": "./src"
"rootDir": "./src",
"testPathIgnorePatterns": ["timerUtils"]
}
}
14 changes: 14 additions & 0 deletions src/__tests__/timerUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// @flow

import { setTimeout } from '../helpers/timers';

const TimerMode = {
Legacy: 'legacy',
Modern: 'modern', // broken for now
};

async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export { TimerMode, sleep };
29 changes: 29 additions & 0 deletions src/__tests__/timers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// @flow
import waitFor from '../waitFor';
import { TimerMode } from './timerUtils';

describe.each([TimerMode.Legacy, TimerMode.Modern])(
'%s fake timers tests',
(fakeTimerType) => {
beforeEach(() => {
jest.useFakeTimers(fakeTimerType);
});

test('it successfully runs tests', () => {
expect(true).toBeTruthy();
});

test('it successfully uses waitFor', async () => {
await waitFor(() => {
expect(true).toBeTruthy();
});
});

test('it successfully uses waitFor with real timers', async () => {
jest.useRealTimers();
await waitFor(() => {
expect(true).toBeTruthy();
});
});
}
);
81 changes: 53 additions & 28 deletions src/__tests__/waitFor.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// @flow
import * as React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { render, fireEvent, waitFor } from '..';
import { Text, TouchableOpacity, View } from 'react-native';
import { fireEvent, render, waitFor } from '..';
import { TimerMode } from './timerUtils';

class Banana extends React.Component<any> {
changeFresh = () => {
Expand Down Expand Up @@ -76,39 +77,63 @@ test('waits for element with custom interval', async () => {
// suppress
}

expect(mockFn).toHaveBeenCalledTimes(3);
expect(mockFn).toHaveBeenCalledTimes(2);
Copy link
Member

Choose a reason for hiding this comment

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

this is a change in behavior

});

test('works with legacy fake timers', async () => {
jest.useFakeTimers('legacy');
test.each([TimerMode.Legacy, TimerMode.Modern])(
'waits for element until it stops throwing using %s fake timers',
async (fakeTimerType) => {
jest.useFakeTimers(fakeTimerType);
const { getByText, queryByText } = render(<BananaContainer />);

const mockFn = jest.fn(() => {
throw Error('test');
});
fireEvent.press(getByText('Change freshness!'));
expect(queryByText('Fresh')).toBeNull();

try {
waitFor(() => mockFn(), { timeout: 400, interval: 200 });
} catch (e) {
// suppress
jest.advanceTimersByTime(300);
const freshBananaText = await waitFor(() => getByText('Fresh'));

expect(freshBananaText.props.children).toBe('Fresh');
}
jest.advanceTimersByTime(400);
);

expect(mockFn).toHaveBeenCalledTimes(3);
});
test.each([TimerMode.Legacy, TimerMode.Modern])(
'waits for assertion until timeout is met with %s fake timers',
async (fakeTimerType) => {
jest.useFakeTimers(fakeTimerType);

test('works with fake timers', async () => {
jest.useFakeTimers('modern');
const mockFn = jest.fn(() => {
throw Error('test');
});

const mockFn = jest.fn(() => {
throw Error('test');
});
try {
await waitFor(() => mockFn(), { timeout: 400, interval: 200 });
} catch (error) {
// suppress
}

try {
waitFor(() => mockFn(), { timeout: 400, interval: 200 });
} catch (e) {
// suppress
expect(mockFn).toHaveBeenCalledTimes(3);
}
jest.advanceTimersByTime(400);

expect(mockFn).toHaveBeenCalledTimes(3);
});
);

test.each([TimerMode.Legacy, TimerMode.Legacy])(
'awaiting something that succeeds before timeout works with %s fake timers',
async (fakeTimerType) => {
jest.useFakeTimers(fakeTimerType);

let calls = 0;
const mockFn = jest.fn(() => {
calls += 1;
if (calls < 3) {
throw Error('test');
}
});

try {
await waitFor(() => mockFn(), { timeout: 400, interval: 200 });
} catch (error) {
// suppress
}

expect(mockFn).toHaveBeenCalledTimes(3);
}
);
44 changes: 19 additions & 25 deletions src/__tests__/waitForElementToBeRemoved.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import React, { useState } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { render, fireEvent, waitForElementToBeRemoved } from '..';
import { TimerMode } from './timerUtils';

const TestSetup = ({ shouldUseDelay = true }) => {
const [isAdded, setIsAdded] = useState(true);
Expand Down Expand Up @@ -120,7 +121,7 @@ test('waits with custom interval', async () => {

try {
await waitForElementToBeRemoved(() => mockFn(), {
timeout: 400,
timeout: 600,
interval: 200,
});
} catch (e) {
Expand All @@ -130,30 +131,23 @@ test('waits with custom interval', async () => {
expect(mockFn).toHaveBeenCalledTimes(4);
});

test('works with legacy fake timers', async () => {
jest.useFakeTimers('legacy');
test.each([TimerMode.Legacy, TimerMode.Modern])(
'works with %s fake timers',
async (fakeTimerType) => {
jest.useFakeTimers(fakeTimerType);

const mockFn = jest.fn(() => <View />);

waitForElementToBeRemoved(() => mockFn(), {
timeout: 400,
interval: 200,
});

jest.advanceTimersByTime(400);
expect(mockFn).toHaveBeenCalledTimes(4);
});

test('works with fake timers', async () => {
jest.useFakeTimers('modern');
const mockFn = jest.fn(() => <View />);

const mockFn = jest.fn(() => <View />);

waitForElementToBeRemoved(() => mockFn(), {
timeout: 400,
interval: 200,
});
try {
await waitForElementToBeRemoved(() => mockFn(), {
timeout: 400,
interval: 200,
});
} catch (e) {
// Suppress expected error
}

jest.advanceTimersByTime(400);
expect(mockFn).toHaveBeenCalledTimes(4);
});
// waitForElementToBeRemoved runs an initial call of the expectation
expect(mockFn).toHaveBeenCalledTimes(4);
}
);
1 change: 1 addition & 0 deletions src/flushMicroTasks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @flow
import { printDeprecationWarning } from './helpers/errors';
import { setImmediate } from './helpers/timers';

type Thenable<T> = { then: (() => T) => mixed };

Expand Down
88 changes: 88 additions & 0 deletions src/helpers/timers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Most content of this file sourced directly from https://github.com/testing-library/dom-testing-library/blob/master/src/helpers.js
// @flow
/* globals jest */

const globalObj = typeof window === 'undefined' ? global : window;

// Currently this fn only supports jest timers, but it could support other test runners in the future.
function runWithRealTimers<T>(callback: () => T): T {
const fakeTimersType = getJestFakeTimersType();
if (fakeTimersType) {
jest.useRealTimers();
}

const callbackReturnValue = callback();

if (fakeTimersType) {
jest.useFakeTimers(fakeTimersType);
}

return callbackReturnValue;
}

function getJestFakeTimersType() {
// istanbul ignore if
if (
typeof jest === 'undefined' ||
typeof globalObj.setTimeout === 'undefined'
) {
return null;
}

if (
typeof globalObj.setTimeout._isMockFunction !== 'undefined' &&
globalObj.setTimeout._isMockFunction
) {
return 'legacy';
}

if (
typeof globalObj.setTimeout.clock !== 'undefined' &&
// $FlowIgnore[prop-missing]
typeof jest.getRealSystemTime !== 'undefined'
) {
try {
// jest.getRealSystemTime is only supported for Jest's `modern` fake timers and otherwise throws
// $FlowExpectedError
jest.getRealSystemTime();
return 'modern';
} catch {
// not using Jest's modern fake timers
}
}
return null;
}

const jestFakeTimersAreEnabled = (): boolean =>
Boolean(getJestFakeTimersType());

// we only run our tests in node, and setImmediate is supported in node.
function setImmediatePolyfill(fn) {
return globalObj.setTimeout(fn, 0);
}

type BindTimeFunctions = {
clearTimeoutFn: typeof clearTimeout,
setImmediateFn: typeof setImmediate,
setTimeoutFn: typeof setTimeout,
};

function bindTimeFunctions(): BindTimeFunctions {
return {
clearTimeoutFn: globalObj.clearTimeout,
setImmediateFn: globalObj.setImmediate || setImmediatePolyfill,
setTimeoutFn: globalObj.setTimeout,
};
}

const { clearTimeoutFn, setImmediateFn, setTimeoutFn } = (runWithRealTimers(
bindTimeFunctions
): BindTimeFunctions);

export {
runWithRealTimers,
jestFakeTimersAreEnabled,
clearTimeoutFn as clearTimeout,
setImmediateFn as setImmediate,
setTimeoutFn as setTimeout,
};
Loading