diff --git a/README.md b/README.md
index d09b10f23..a179002e8 100644
--- a/README.md
+++ b/README.md
@@ -119,7 +119,8 @@ The [public API](https://callstack.github.io/react-native-testing-library/docs/a
- [`render`](https://callstack.github.io/react-native-testing-library/docs/api#render) – deeply renders given React element and returns helpers to query the output components.
- [`fireEvent`](https://callstack.github.io/react-native-testing-library/docs/api#fireevent) - invokes named event handler on the element.
-- [`waitFor`](https://callstack.github.io/react-native-testing-library/docs/api#waitfor) - waits for non-deterministic periods of time until your element appears or times out.
+- [`waitFor`](https://callstack.github.io/react-native-testing-library/docs/api#waitfor) - waits for non-deterministic periods of time until queried element is added or times out.
+- [`waitForElementToBeRemoved`](https://callstack.github.io/react-native-testing-library/docs/api#waitforelementtoberemoved) - waits for non-deterministic periods of time until queried element is removed or times out.
- [`within`](https://callstack.github.io/react-native-testing-library/docs/api#within) - creates a queries object scoped for given element.
## Migration Guides
diff --git a/src/__tests__/waitForElementToBeRemoved.test.js b/src/__tests__/waitForElementToBeRemoved.test.js
new file mode 100644
index 000000000..d2fc12091
--- /dev/null
+++ b/src/__tests__/waitForElementToBeRemoved.test.js
@@ -0,0 +1,141 @@
+// @flow
+import React, { useState } from 'react';
+import { View, Text, TouchableOpacity } from 'react-native';
+import { render, fireEvent, waitForElementToBeRemoved } from '..';
+
+const TestSetup = ({ shouldUseDelay = true }) => {
+ const [isAdded, setIsAdded] = useState(true);
+
+ const removeElement = async () => {
+ if (shouldUseDelay) {
+ setTimeout(() => setIsAdded(false), 300);
+ } else {
+ setIsAdded(false);
+ }
+ };
+
+ return (
+
+ {isAdded && Observed Element}
+
+
+ Remove Element
+
+
+ );
+};
+
+test('waits when using getBy query', async () => {
+ const screen = render();
+
+ fireEvent.press(screen.getByText('Remove Element'));
+ const element = screen.getByText('Observed Element');
+ expect(element).toBeTruthy();
+
+ const result = await waitForElementToBeRemoved(() =>
+ screen.getByText('Observed Element')
+ );
+ expect(screen.queryByText('Observed Element')).toBeNull();
+ expect(result).toEqual(element);
+});
+
+test('waits when using getAllBy query', async () => {
+ const screen = render();
+
+ fireEvent.press(screen.getByText('Remove Element'));
+ const elements = screen.getAllByText('Observed Element');
+ expect(elements).toBeTruthy();
+
+ const result = await waitForElementToBeRemoved(() =>
+ screen.getAllByText('Observed Element')
+ );
+ expect(screen.queryByText('Observed Element')).toBeNull();
+ expect(result).toEqual(elements);
+});
+
+test('waits when using queryBy query', async () => {
+ const screen = render();
+
+ fireEvent.press(screen.getByText('Remove Element'));
+ const element = screen.getByText('Observed Element');
+ expect(element).toBeTruthy();
+
+ const result = await waitForElementToBeRemoved(() =>
+ screen.queryByText('Observed Element')
+ );
+ expect(screen.queryByText('Observed Element')).toBeNull();
+ expect(result).toEqual(element);
+});
+
+test('waits when using queryAllBy query', async () => {
+ const screen = render();
+
+ fireEvent.press(screen.getByText('Remove Element'));
+ const elements = screen.getAllByText('Observed Element');
+ expect(elements).toBeTruthy();
+
+ const result = await waitForElementToBeRemoved(() =>
+ screen.queryAllByText('Observed Element')
+ );
+ expect(screen.queryByText('Observed Element')).toBeNull();
+ expect(result).toEqual(elements);
+});
+
+test('checks if elements exist at start', async () => {
+ const screen = render();
+
+ fireEvent.press(screen.getByText('Remove Element'));
+ expect(screen.queryByText('Observed Element')).toBeNull();
+
+ await expect(
+ waitForElementToBeRemoved(() => screen.queryByText('Observed Element'))
+ ).rejects.toThrow(
+ 'The element(s) given to waitForElementToBeRemoved are already removed. waitForElementToBeRemoved requires that the element(s) exist(s) before waiting for removal.'
+ );
+});
+
+test('waits until timeout', async () => {
+ const screen = render();
+
+ fireEvent.press(screen.getByText('Remove Element'));
+ expect(screen.getByText('Observed Element')).toBeTruthy();
+
+ await expect(
+ waitForElementToBeRemoved(() => screen.getByText('Observed Element'), {
+ timeout: 100,
+ })
+ ).rejects.toThrow('Timed out in waitForElementToBeRemoved.');
+
+ // Async action ends after 300ms and we only waited 100ms, so we need to wait for the remaining
+ // async actions to finish
+ await waitForElementToBeRemoved(() => screen.getByText('Observed Element'));
+});
+
+test('waits with custom interval', async () => {
+ const mockFn = jest.fn(() => );
+
+ try {
+ await waitForElementToBeRemoved(() => mockFn(), {
+ timeout: 400,
+ interval: 200,
+ });
+ } catch (e) {
+ // Suppress expected error
+ }
+
+ expect(mockFn).toHaveBeenCalledTimes(4);
+});
+
+test('works with fake timers', async () => {
+ jest.useFakeTimers();
+
+ const mockFn = jest.fn(() => );
+
+ waitForElementToBeRemoved(() => mockFn(), {
+ timeout: 400,
+ interval: 200,
+ });
+
+ jest.advanceTimersByTime(400);
+ expect(mockFn).toHaveBeenCalledTimes(4);
+});
diff --git a/src/pure.js b/src/pure.js
index 7765847a6..0c1a21aae 100644
--- a/src/pure.js
+++ b/src/pure.js
@@ -6,6 +6,7 @@ import flushMicrotasksQueue from './flushMicroTasks';
import render from './render';
import shallow from './shallow';
import waitFor, { waitForElement } from './waitFor';
+import waitForElementToBeRemoved from './waitForElementToBeRemoved';
import within from './within';
export { act };
@@ -15,4 +16,5 @@ export { flushMicrotasksQueue };
export { render };
export { shallow };
export { waitFor, waitForElement };
+export { waitForElementToBeRemoved };
export { within };
diff --git a/src/waitForElementToBeRemoved.js b/src/waitForElementToBeRemoved.js
new file mode 100644
index 000000000..3da9a111d
--- /dev/null
+++ b/src/waitForElementToBeRemoved.js
@@ -0,0 +1,41 @@
+// @flow
+import waitFor, { type WaitForOptions } from './waitFor';
+import { ErrorWithStack } from './helpers/errors';
+
+const isRemoved = (result) =>
+ !result || (Array.isArray(result) && !result.length);
+
+export default async function waitForElementToBeRemoved(
+ expectation: () => T,
+ options?: WaitForOptions
+): Promise {
+ // Created here so we get a nice stacktrace
+ const timeoutError = new ErrorWithStack(
+ 'Timed out in waitForElementToBeRemoved.',
+ waitForElementToBeRemoved
+ );
+
+ // Elements have to be present initally and then removed.
+ const initialElements = expectation();
+ if (isRemoved(initialElements)) {
+ throw new ErrorWithStack(
+ 'The element(s) given to waitForElementToBeRemoved are already removed. waitForElementToBeRemoved requires that the element(s) exist(s) before waiting for removal.',
+ waitForElementToBeRemoved
+ );
+ }
+
+ return waitFor(() => {
+ let result;
+ try {
+ result = expectation();
+ } catch (error) {
+ return initialElements;
+ }
+
+ if (!isRemoved(result)) {
+ throw timeoutError;
+ }
+
+ return initialElements;
+ }, options);
+}
diff --git a/typings/__tests__/index.test.tsx b/typings/__tests__/index.test.tsx
index f6f7cb31b..7cf125812 100644
--- a/typings/__tests__/index.test.tsx
+++ b/typings/__tests__/index.test.tsx
@@ -6,6 +6,7 @@ import {
fireEvent,
flushMicrotasksQueue,
waitFor,
+ waitForElementToBeRemoved,
act,
within,
} from '../..';
@@ -184,26 +185,77 @@ fireEvent.changeText(element, 'string');
fireEvent.scroll(element, 'eventData');
// waitFor API
+const timeout = { timeout: 10 };
+const timeoutInterval = { timeout: 100, interval: 10 };
+
const waitGetBy: Promise[] = [
waitFor(() => tree.getByA11yLabel('label')),
- waitFor(() => tree.getByA11yLabel('label'), {
- timeout: 10,
- }),
- waitFor(() => tree.getByA11yLabel('label'), {
- timeout: 100,
- interval: 10,
- }),
+ waitFor(() => tree.getByA11yLabel('label'), timeout),
+ waitFor(
+ () => tree.getByA11yLabel('label'),
+ timeoutInterval
+ ),
];
const waitGetAllBy: Promise[] = [
waitFor(() => tree.getAllByA11yLabel('label')),
- waitFor(() => tree.getAllByA11yLabel('label'), {
- timeout: 10,
- }),
- waitFor(() => tree.getAllByA11yLabel('label'), {
- timeout: 100,
- interval: 10,
- }),
+ waitFor(() => tree.getAllByA11yLabel('label'), timeout),
+ waitFor(
+ () => tree.getAllByA11yLabel('label'),
+ timeoutInterval
+ ),
+];
+
+// waitForElementToBeRemoved API
+const waitForElementToBeRemovedGetBy: Promise[] = [
+ waitForElementToBeRemoved(() => tree.getByText('text')),
+ waitForElementToBeRemoved(
+ () => tree.getByText('text'),
+ timeout
+ ),
+ waitForElementToBeRemoved(
+ () => tree.getByText('text'),
+ timeoutInterval
+ ),
+];
+const waitForElementToBeRemovedGetAllBy: Promise[] = [
+ waitForElementToBeRemoved(() =>
+ tree.getAllByText('text')
+ ),
+ waitForElementToBeRemoved(
+ () => tree.getAllByText('text'),
+ timeout
+ ),
+ waitForElementToBeRemoved(
+ () => tree.getAllByText('text'),
+ timeoutInterval
+ ),
+];
+const waitForElementToBeRemovedQueryBy: Promise[] = [
+ waitForElementToBeRemoved(() =>
+ tree.queryByText('text')
+ ),
+ waitForElementToBeRemoved(
+ () => tree.queryByText('text'),
+ timeout
+ ),
+ waitForElementToBeRemoved(
+ () => tree.queryByText('text'),
+ timeoutInterval
+ ),
+];
+const waitForElementToBeRemovedQueryAllBy: Promise[] = [
+ waitForElementToBeRemoved(() =>
+ tree.queryAllByText('text')
+ ),
+ waitForElementToBeRemoved(
+ () => tree.queryAllByText('text'),
+ timeout
+ ),
+ waitForElementToBeRemoved(
+ () => tree.queryAllByText('text'),
+ timeoutInterval
+ ),
];
const waitForFlush: Promise = flushMicrotasksQueue();
diff --git a/typings/index.d.ts b/typings/index.d.ts
index b998fb93a..fdc14e968 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -280,6 +280,14 @@ export type FireEventAPI = FireEventFunction & {
scroll: (element: ReactTestInstance, ...data: Array) => any;
};
+export declare const render: (
+ component: React.ReactElement,
+ options?: RenderOptions
+) => RenderAPI;
+
+export declare const cleanup: () => void;
+export declare const fireEvent: FireEventAPI;
+
type WaitForOptions = {
timeout?: number;
interval?: number;
@@ -290,14 +298,15 @@ export type WaitForFunction = (
options?: WaitForOptions
) => Promise;
-export declare const render: (
- component: React.ReactElement,
- options?: RenderOptions
-) => RenderAPI;
-
-export declare const cleanup: () => void;
-export declare const fireEvent: FireEventAPI;
export declare const waitFor: WaitForFunction;
+
+export type WaitForElementToBeRemovedFunction = (
+ expectation: () => T,
+ options?: WaitForOptions
+) => Promise;
+
+export declare const waitForElementToBeRemoved: WaitForElementToBeRemovedFunction;
+
export declare const act: (callback: () => void) => Thenable;
export declare const within: (instance: ReactTestInstance) => Queries;
diff --git a/website/docs/API.md b/website/docs/API.md
index a5ad19ed1..f43d230e9 100644
--- a/website/docs/API.md
+++ b/website/docs/API.md
@@ -336,20 +336,55 @@ function waitFor(
Waits for non-deterministic periods of time until your element appears or times out. `waitFor` periodically calls `expectation` every `interval` milliseconds to determine whether the element appeared or not.
+```jsx
+import { render, waitFor } from 'react-testing-library';
+
+test('waiting for an Banana to be ready', async () => {
+ const { getByText } = render();
+
+ await waitFor(() => getByText('Banana ready'));
+});
+```
+
:::info
In order to properly use `waitFor` you need at least React >=16.9.0 (featuring async `act`) or React Native >=0.60 (which comes with React >=16.9.0).
:::
+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.
+
+## `waitForElementToBeRemoved`
+
+- [`Example code`](https://github.com/callstack/react-native-testing-library/blob/master/src/__tests__/waitForElementToBeRemoved.test.js)
+
+Defined as:
+
```jsx
-import { render, waitFor } from 'react-testing-library';
+function waitForElementToBeRemoved(
+ expectation: () => T,
+ { timeout: number = 4500, interval: number = 50 }
+): Promise {}
+```
-test('waiting for an Banana to be ready', async () => {
+Waits for non-deterministic periods of time until queried element is removed or times out. `waitForElementToBeRemoved` periodically calls `expectation` every `interval` milliseconds to determine whether the element has been removed or not.
+
+```jsx
+import { render, waitForElementToBeRemoved } from 'react-testing-library';
+
+test('waiting for an Banana to be removed', async () => {
const { getByText } = render();
- await waitFor(() => getByText('Banana ready'));
+ await waitForElementToBeRemoved(() => getByText('Banana ready'));
});
```
+This method expects that the element is initally present in the render tree and then is removed from it. If the element is not present when you call this method it throws an error.
+
+You can use any of `getBy`, `getAllBy`, `queryBy` and `queryAllBy` queries for `expectation` parameter.
+
+:::info
+In order to properly use `waitForElementToBeRemoved` you need at least React >=16.9.0 (featuring async `act`) or React Native >=0.60 (which comes with React >=16.9.0).
+:::
+
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.
## `within`