Skip to content

Commit 300d16b

Browse files
feature: wait for element to be removed (#376)
* feature: basic implementation * feature: basic implementation * feature: added typescript types * feature: check if element exists initially * resolve promise with initial elements * docs: README & API * use ErrorWithStack helper Co-authored-by: Michał Pierzchała <[email protected]>
1 parent dab54a9 commit 300d16b

File tree

7 files changed

+306
-25
lines changed

7 files changed

+306
-25
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ The [public API](https://callstack.github.io/react-native-testing-library/docs/a
119119

120120
- [`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.
121121
- [`fireEvent`](https://callstack.github.io/react-native-testing-library/docs/api#fireevent) - invokes named event handler on the element.
122-
- [`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.
122+
- [`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.
123+
- [`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.
123124
- [`within`](https://callstack.github.io/react-native-testing-library/docs/api#within) - creates a queries object scoped for given element.
124125

125126
## Migration Guides
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// @flow
2+
import React, { useState } from 'react';
3+
import { View, Text, TouchableOpacity } from 'react-native';
4+
import { render, fireEvent, waitForElementToBeRemoved } from '..';
5+
6+
const TestSetup = ({ shouldUseDelay = true }) => {
7+
const [isAdded, setIsAdded] = useState(true);
8+
9+
const removeElement = async () => {
10+
if (shouldUseDelay) {
11+
setTimeout(() => setIsAdded(false), 300);
12+
} else {
13+
setIsAdded(false);
14+
}
15+
};
16+
17+
return (
18+
<View>
19+
{isAdded && <Text>Observed Element</Text>}
20+
21+
<TouchableOpacity onPress={removeElement}>
22+
<Text>Remove Element</Text>
23+
</TouchableOpacity>
24+
</View>
25+
);
26+
};
27+
28+
test('waits when using getBy query', async () => {
29+
const screen = render(<TestSetup />);
30+
31+
fireEvent.press(screen.getByText('Remove Element'));
32+
const element = screen.getByText('Observed Element');
33+
expect(element).toBeTruthy();
34+
35+
const result = await waitForElementToBeRemoved(() =>
36+
screen.getByText('Observed Element')
37+
);
38+
expect(screen.queryByText('Observed Element')).toBeNull();
39+
expect(result).toEqual(element);
40+
});
41+
42+
test('waits when using getAllBy query', async () => {
43+
const screen = render(<TestSetup />);
44+
45+
fireEvent.press(screen.getByText('Remove Element'));
46+
const elements = screen.getAllByText('Observed Element');
47+
expect(elements).toBeTruthy();
48+
49+
const result = await waitForElementToBeRemoved(() =>
50+
screen.getAllByText('Observed Element')
51+
);
52+
expect(screen.queryByText('Observed Element')).toBeNull();
53+
expect(result).toEqual(elements);
54+
});
55+
56+
test('waits when using queryBy query', async () => {
57+
const screen = render(<TestSetup />);
58+
59+
fireEvent.press(screen.getByText('Remove Element'));
60+
const element = screen.getByText('Observed Element');
61+
expect(element).toBeTruthy();
62+
63+
const result = await waitForElementToBeRemoved(() =>
64+
screen.queryByText('Observed Element')
65+
);
66+
expect(screen.queryByText('Observed Element')).toBeNull();
67+
expect(result).toEqual(element);
68+
});
69+
70+
test('waits when using queryAllBy query', async () => {
71+
const screen = render(<TestSetup />);
72+
73+
fireEvent.press(screen.getByText('Remove Element'));
74+
const elements = screen.getAllByText('Observed Element');
75+
expect(elements).toBeTruthy();
76+
77+
const result = await waitForElementToBeRemoved(() =>
78+
screen.queryAllByText('Observed Element')
79+
);
80+
expect(screen.queryByText('Observed Element')).toBeNull();
81+
expect(result).toEqual(elements);
82+
});
83+
84+
test('checks if elements exist at start', async () => {
85+
const screen = render(<TestSetup shouldUseDelay={false} />);
86+
87+
fireEvent.press(screen.getByText('Remove Element'));
88+
expect(screen.queryByText('Observed Element')).toBeNull();
89+
90+
await expect(
91+
waitForElementToBeRemoved(() => screen.queryByText('Observed Element'))
92+
).rejects.toThrow(
93+
'The element(s) given to waitForElementToBeRemoved are already removed. waitForElementToBeRemoved requires that the element(s) exist(s) before waiting for removal.'
94+
);
95+
});
96+
97+
test('waits until timeout', async () => {
98+
const screen = render(<TestSetup />);
99+
100+
fireEvent.press(screen.getByText('Remove Element'));
101+
expect(screen.getByText('Observed Element')).toBeTruthy();
102+
103+
await expect(
104+
waitForElementToBeRemoved(() => screen.getByText('Observed Element'), {
105+
timeout: 100,
106+
})
107+
).rejects.toThrow('Timed out in waitForElementToBeRemoved.');
108+
109+
// Async action ends after 300ms and we only waited 100ms, so we need to wait for the remaining
110+
// async actions to finish
111+
await waitForElementToBeRemoved(() => screen.getByText('Observed Element'));
112+
});
113+
114+
test('waits with custom interval', async () => {
115+
const mockFn = jest.fn(() => <View />);
116+
117+
try {
118+
await waitForElementToBeRemoved(() => mockFn(), {
119+
timeout: 400,
120+
interval: 200,
121+
});
122+
} catch (e) {
123+
// Suppress expected error
124+
}
125+
126+
expect(mockFn).toHaveBeenCalledTimes(4);
127+
});
128+
129+
test('works with fake timers', async () => {
130+
jest.useFakeTimers();
131+
132+
const mockFn = jest.fn(() => <View />);
133+
134+
waitForElementToBeRemoved(() => mockFn(), {
135+
timeout: 400,
136+
interval: 200,
137+
});
138+
139+
jest.advanceTimersByTime(400);
140+
expect(mockFn).toHaveBeenCalledTimes(4);
141+
});

src/pure.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import flushMicrotasksQueue from './flushMicroTasks';
66
import render from './render';
77
import shallow from './shallow';
88
import waitFor, { waitForElement } from './waitFor';
9+
import waitForElementToBeRemoved from './waitForElementToBeRemoved';
910
import within from './within';
1011

1112
export { act };
@@ -15,4 +16,5 @@ export { flushMicrotasksQueue };
1516
export { render };
1617
export { shallow };
1718
export { waitFor, waitForElement };
19+
export { waitForElementToBeRemoved };
1820
export { within };

src/waitForElementToBeRemoved.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// @flow
2+
import waitFor, { type WaitForOptions } from './waitFor';
3+
import { ErrorWithStack } from './helpers/errors';
4+
5+
const isRemoved = (result) =>
6+
!result || (Array.isArray(result) && !result.length);
7+
8+
export default async function waitForElementToBeRemoved<T>(
9+
expectation: () => T,
10+
options?: WaitForOptions
11+
): Promise<T> {
12+
// Created here so we get a nice stacktrace
13+
const timeoutError = new ErrorWithStack(
14+
'Timed out in waitForElementToBeRemoved.',
15+
waitForElementToBeRemoved
16+
);
17+
18+
// Elements have to be present initally and then removed.
19+
const initialElements = expectation();
20+
if (isRemoved(initialElements)) {
21+
throw new ErrorWithStack(
22+
'The element(s) given to waitForElementToBeRemoved are already removed. waitForElementToBeRemoved requires that the element(s) exist(s) before waiting for removal.',
23+
waitForElementToBeRemoved
24+
);
25+
}
26+
27+
return waitFor(() => {
28+
let result;
29+
try {
30+
result = expectation();
31+
} catch (error) {
32+
return initialElements;
33+
}
34+
35+
if (!isRemoved(result)) {
36+
throw timeoutError;
37+
}
38+
39+
return initialElements;
40+
}, options);
41+
}

typings/__tests__/index.test.tsx

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
fireEvent,
77
flushMicrotasksQueue,
88
waitFor,
9+
waitForElementToBeRemoved,
910
act,
1011
within,
1112
} from '../..';
@@ -184,26 +185,77 @@ fireEvent.changeText(element, 'string');
184185
fireEvent.scroll(element, 'eventData');
185186

186187
// waitFor API
188+
const timeout = { timeout: 10 };
189+
const timeoutInterval = { timeout: 100, interval: 10 };
190+
187191
const waitGetBy: Promise<ReactTestInstance>[] = [
188192
waitFor<ReactTestInstance>(() => tree.getByA11yLabel('label')),
189-
waitFor<ReactTestInstance>(() => tree.getByA11yLabel('label'), {
190-
timeout: 10,
191-
}),
192-
waitFor<ReactTestInstance>(() => tree.getByA11yLabel('label'), {
193-
timeout: 100,
194-
interval: 10,
195-
}),
193+
waitFor<ReactTestInstance>(() => tree.getByA11yLabel('label'), timeout),
194+
waitFor<ReactTestInstance>(
195+
() => tree.getByA11yLabel('label'),
196+
timeoutInterval
197+
),
196198
];
197199

198200
const waitGetAllBy: Promise<ReactTestInstance[]>[] = [
199201
waitFor<ReactTestInstance[]>(() => tree.getAllByA11yLabel('label')),
200-
waitFor<ReactTestInstance[]>(() => tree.getAllByA11yLabel('label'), {
201-
timeout: 10,
202-
}),
203-
waitFor<ReactTestInstance[]>(() => tree.getAllByA11yLabel('label'), {
204-
timeout: 100,
205-
interval: 10,
206-
}),
202+
waitFor<ReactTestInstance[]>(() => tree.getAllByA11yLabel('label'), timeout),
203+
waitFor<ReactTestInstance[]>(
204+
() => tree.getAllByA11yLabel('label'),
205+
timeoutInterval
206+
),
207+
];
208+
209+
// waitForElementToBeRemoved API
210+
const waitForElementToBeRemovedGetBy: Promise<ReactTestInstance>[] = [
211+
waitForElementToBeRemoved<ReactTestInstance>(() => tree.getByText('text')),
212+
waitForElementToBeRemoved<ReactTestInstance>(
213+
() => tree.getByText('text'),
214+
timeout
215+
),
216+
waitForElementToBeRemoved<ReactTestInstance>(
217+
() => tree.getByText('text'),
218+
timeoutInterval
219+
),
220+
];
221+
const waitForElementToBeRemovedGetAllBy: Promise<ReactTestInstance[]>[] = [
222+
waitForElementToBeRemoved<ReactTestInstance[]>(() =>
223+
tree.getAllByText('text')
224+
),
225+
waitForElementToBeRemoved<ReactTestInstance[]>(
226+
() => tree.getAllByText('text'),
227+
timeout
228+
),
229+
waitForElementToBeRemoved<ReactTestInstance[]>(
230+
() => tree.getAllByText('text'),
231+
timeoutInterval
232+
),
233+
];
234+
const waitForElementToBeRemovedQueryBy: Promise<ReactTestInstance | null>[] = [
235+
waitForElementToBeRemoved<ReactTestInstance | null>(() =>
236+
tree.queryByText('text')
237+
),
238+
waitForElementToBeRemoved<ReactTestInstance | null>(
239+
() => tree.queryByText('text'),
240+
timeout
241+
),
242+
waitForElementToBeRemoved<ReactTestInstance | null>(
243+
() => tree.queryByText('text'),
244+
timeoutInterval
245+
),
246+
];
247+
const waitForElementToBeRemovedQueryAllBy: Promise<ReactTestInstance[]>[] = [
248+
waitForElementToBeRemoved<ReactTestInstance[]>(() =>
249+
tree.queryAllByText('text')
250+
),
251+
waitForElementToBeRemoved<ReactTestInstance[]>(
252+
() => tree.queryAllByText('text'),
253+
timeout
254+
),
255+
waitForElementToBeRemoved<ReactTestInstance[]>(
256+
() => tree.queryAllByText('text'),
257+
timeoutInterval
258+
),
207259
];
208260

209261
const waitForFlush: Promise<any> = flushMicrotasksQueue();

typings/index.d.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,14 @@ export type FireEventAPI = FireEventFunction & {
280280
scroll: (element: ReactTestInstance, ...data: Array<any>) => any;
281281
};
282282

283+
export declare const render: (
284+
component: React.ReactElement<any>,
285+
options?: RenderOptions
286+
) => RenderAPI;
287+
288+
export declare const cleanup: () => void;
289+
export declare const fireEvent: FireEventAPI;
290+
283291
type WaitForOptions = {
284292
timeout?: number;
285293
interval?: number;
@@ -290,14 +298,15 @@ export type WaitForFunction = <T = any>(
290298
options?: WaitForOptions
291299
) => Promise<T>;
292300

293-
export declare const render: (
294-
component: React.ReactElement<any>,
295-
options?: RenderOptions
296-
) => RenderAPI;
297-
298-
export declare const cleanup: () => void;
299-
export declare const fireEvent: FireEventAPI;
300301
export declare const waitFor: WaitForFunction;
302+
303+
export type WaitForElementToBeRemovedFunction = <T = any>(
304+
expectation: () => T,
305+
options?: WaitForOptions
306+
) => Promise<T>;
307+
308+
export declare const waitForElementToBeRemoved: WaitForElementToBeRemovedFunction;
309+
301310
export declare const act: (callback: () => void) => Thenable;
302311
export declare const within: (instance: ReactTestInstance) => Queries;
303312

0 commit comments

Comments
 (0)