diff --git a/README.md b/README.md index 03b7a7d6d..c25ee6b53 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,32 @@ const { getByTestId } = render( fireEvent.scroll(getByTestId('scroll-view'), eventData); ``` +## `waitForElement` + +Defined as: + +```jsx +function waitForExpect( + expectation: () => T, + timeout: number = 4500, + interval: number = 50 +): Promise {} +``` + +Wait for non-deterministic periods of time until your element appears or times out. `waitForExpect` periodically calls `expectation` every `interval` milliseconds to determine whether the element appeared or not. + +```jsx +import { render, waitForElement } from 'react-testing-library'; + +test('waiting for an Banana to be ready', async () => { + const { getByText } = render(); + + await waitForElement(() => getByText('Banana ready')); +}); +``` + +If you're using Jest's [Timer Mocks](https://jestjs.io/docs/en/timer-mocks#docsNav), remember not to use `async/await` syntax as it will stall your tests. + ## `debug` Log prettified shallowly rendered component or test instance (just like snapshot) to stdout. diff --git a/src/__tests__/fireEvent.test.js b/src/__tests__/fireEvent.test.js index 28a52d6f8..edb73dfc8 100644 --- a/src/__tests__/fireEvent.test.js +++ b/src/__tests__/fireEvent.test.js @@ -7,8 +7,7 @@ import { ScrollView, TextInput, } from '../__mocks__/reactNativeMock'; -import fireEvent from '../fireEvent'; -import { render } from '..'; +import { render, fireEvent } from '..'; const OnPressComponent = ({ onPress }) => ( diff --git a/src/__tests__/waitForElement.test.js b/src/__tests__/waitForElement.test.js new file mode 100644 index 000000000..387b33df3 --- /dev/null +++ b/src/__tests__/waitForElement.test.js @@ -0,0 +1,92 @@ +// @flow +/* eslint-disable react/no-multi-comp */ +import React from 'react'; +import { View, Text, TouchableOpacity } from '../__mocks__/reactNativeMock'; +import { render, fireEvent, waitForElement } from '..'; + +class Banana extends React.Component<*, *> { + changeFresh = () => { + this.props.onChangeFresh(); + }; + + render() { + return ( + + {this.props.fresh && Fresh} + + Change freshness! + + + ); + } +} + +class BananaContainer extends React.Component<*, *> { + state = { fresh: false }; + + onChangeFresh = async () => { + await new Promise(resolve => setTimeout(resolve, 300)); + this.setState({ fresh: true }); + }; + + render() { + return ( + + ); + } +} + +test('waits for element until it stops throwing', async () => { + const { getByTestId, getByName, queryByTestId } = render(); + + fireEvent.press(getByName('TouchableOpacity')); + + expect(queryByTestId('fresh')).toBeNull(); + + const freshBananaText = await waitForElement(() => getByTestId('fresh')); + + expect(freshBananaText.props.children).toBe('Fresh'); +}); + +test('waits for element until timeout is met', async () => { + const { getByTestId, getByName } = render(); + + fireEvent.press(getByName('TouchableOpacity')); + + await expect( + waitForElement(() => getByTestId('fresh'), 100) + ).rejects.toThrow(); +}); + +test('waits for element with custom interval', async () => { + const mockFn = jest.fn(() => { + throw Error('test'); + }); + + try { + await waitForElement(() => mockFn(), 400, 200); + } catch (e) { + // suppress + } + + expect(mockFn).toBeCalledTimes(3); +}); + +test('works with fake timers', async () => { + jest.useFakeTimers(); + + const mockFn = jest.fn(() => { + throw Error('test'); + }); + + try { + waitForElement(() => mockFn(), 400, 200); + } catch (e) { + // suppress + } + jest.runTimersToTime(400); + + expect(mockFn).toBeCalledTimes(3); + + jest.useRealTimers(); +}); diff --git a/src/index.js b/src/index.js index a2181079a..f8e6548fe 100644 --- a/src/index.js +++ b/src/index.js @@ -4,9 +4,11 @@ import shallow from './shallow'; import flushMicrotasksQueue from './flushMicrotasksQueue'; import debug from './debug'; import fireEvent from './fireEvent'; +import waitForElement from './waitForElement'; export { render }; export { shallow }; export { flushMicrotasksQueue }; export { debug }; export { fireEvent }; +export { waitForElement }; diff --git a/src/waitForElement.js b/src/waitForElement.js new file mode 100644 index 000000000..af08e2b15 --- /dev/null +++ b/src/waitForElement.js @@ -0,0 +1,26 @@ +// @flow +export default function waitForExpect( + expectation: () => T, + timeout: number = 4500, + interval: number = 50 +) { + const startTime = Date.now(); + return new Promise((resolve, reject) => { + const rejectOrRerun = error => { + if (Date.now() - startTime >= timeout) { + reject(error); + return; + } + setTimeout(runExpectation, interval); + }; + function runExpectation() { + try { + const result = expectation(); + resolve(result); + } catch (error) { + rejectOrRerun(error); + } + } + setTimeout(runExpectation, 0); + }); +}