Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions src/__tests__/waitForElementToBeRemoved.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// @flow
import React, { useState } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { render, fireEvent, waitForElementToBeRemoved } from '..';

const TestSetup = () => {
const [isAdded, setIsAdded] = useState(true);

const removeElement = async () => {
setTimeout(() => setIsAdded(false), 300);
};

return (
<View>
{isAdded && <Text>Observed Element</Text>}

<TouchableOpacity onPress={removeElement}>
<Text>Remove Element</Text>
</TouchableOpacity>
</View>
);
};

test('waits when using getBy query', async () => {
const screen = render(<TestSetup />);

fireEvent.press(screen.getByText('Remove Element'));
expect(screen.getByText('Observed Element')).toBeTruthy();

await waitForElementToBeRemoved(() => screen.getByText('Observed Element'));
expect(screen.queryByText('Observed Element')).toBeNull();
});

test('waits when using getAllBy query', async () => {
const screen = render(<TestSetup />);

fireEvent.press(screen.getByText('Remove Element'));
expect(screen.getByText('Observed Element')).toBeTruthy();

await waitForElementToBeRemoved(() =>
screen.getAllByText('Observed Element')
);
expect(screen.queryByText('Observed Element')).toBeNull();
});

test('waits when using queryBy query', async () => {
const screen = render(<TestSetup />);

fireEvent.press(screen.getByText('Remove Element'));
expect(screen.getByText('Observed Element')).toBeTruthy();

await waitForElementToBeRemoved(() => screen.queryByText('Observed Element'));
expect(screen.queryByText('Observed Element')).toBeNull();
});

test('waits when using queryAllBy query', async () => {
const screen = render(<TestSetup />);

fireEvent.press(screen.getByText('Remove Element'));
expect(screen.getByText('Observed Element')).toBeTruthy();

await waitForElementToBeRemoved(() =>
screen.queryAllByText('Observed Element')
);
expect(screen.queryByText('Observed Element')).toBeNull();
});

test('waits until timeout', async () => {
const screen = render(<TestSetup />);

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(() => <View />);

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

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

test('works with fake timers', async () => {
jest.useFakeTimers();

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

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

jest.advanceTimersByTime(400);
expect(mockFn).toHaveBeenCalledTimes(3);
});
2 changes: 2 additions & 0 deletions src/pure.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -15,4 +16,5 @@ export { flushMicrotasksQueue };
export { render };
export { shallow };
export { waitFor, waitForElement };
export { waitForElementToBeRemoved };
export { within };
29 changes: 29 additions & 0 deletions src/waitForElementToBeRemoved.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// @flow
import waitFor from './waitFor';
import type { WaitForOptions } from './waitFor';

const isRemoved = (result) =>
!result || (Array.isArray(result) && !result.length);

export default async function waitForElementToBeRemoved<T>(
expectation: () => T,
options?: WaitForOptions
): Promise<null> {
Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure if null is a proper type here. But we do not have anything meaningful to return.
RTL declares its version in TS as Promise<T> but uses return true in their impl.

Copy link
Member Author

Choose a reason for hiding this comment

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

IMO I would keep the Promise<T> return type and return the initial result of expectation call. Since in code we require that initially expectation is successful (returns something) and only then starts throwing/returning nothing.

Copy link
Member

Choose a reason for hiding this comment

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

yup, let's do it

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

Copy link
Member Author

Choose a reason for hiding this comment

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

Also logged issue in Dom TL that they have similar discrepancy.

// Created here so we get a nice stacktrace
const timeoutError = new Error('Timed out in waitForElementToBeRemoved.');

return waitFor(() => {
let result;
try {
result = expectation();
} catch (error) {
return null;
}

if (!isRemoved(result)) {
throw timeoutError;
}

return null;
}, options);
}
61 changes: 61 additions & 0 deletions typings/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
fireEvent,
flushMicrotasksQueue,
waitFor,
waitForElementToBeRemoved,
act,
within,
} from '../..';
Expand Down Expand Up @@ -206,6 +207,66 @@ const waitGetAllBy: Promise<ReactTestInstance[]>[] = [
}),
];

// waitForElementToBeRemoved API
const waitForElementToBeRemovedAPI: Promise<null>[] = [
waitForElementToBeRemoved<ReactTestInstance>(() => tree.getByText('text')),
waitForElementToBeRemoved<ReactTestInstance>(() => tree.getByText('text'), {
timeout: 10,
}),
waitForElementToBeRemoved<ReactTestInstance>(() => tree.getByText('text'), {
timeout: 100,
interval: 10,
}),
waitForElementToBeRemoved<ReactTestInstance[]>(() =>
tree.getAllByText('text')
),
waitForElementToBeRemoved<ReactTestInstance[]>(
() => tree.getAllByText('text'),
{
timeout: 10,
}
),
waitForElementToBeRemoved<ReactTestInstance[]>(
() => tree.getAllByText('text'),
{
timeout: 100,
interval: 10,
}
),
waitForElementToBeRemoved<ReactTestInstance | null>(() =>
tree.queryByText('text')
),
waitForElementToBeRemoved<ReactTestInstance | null>(
() => tree.queryByText('text'),
{
timeout: 10,
}
),
waitForElementToBeRemoved<ReactTestInstance | null>(
() => tree.queryByText('text'),
{
timeout: 100,
interval: 10,
}
),
waitForElementToBeRemoved<ReactTestInstance[]>(() =>
tree.queryAllByText('text')
),
waitForElementToBeRemoved<ReactTestInstance[]>(
() => tree.queryAllByText('text'),
{
timeout: 10,
}
),
waitForElementToBeRemoved<ReactTestInstance[]>(
() => tree.queryAllByText('text'),
{
timeout: 100,
interval: 10,
}
),
];

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

// act API
Expand Down
23 changes: 16 additions & 7 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,14 @@ export type FireEventAPI = FireEventFunction & {
scroll: (element: ReactTestInstance, ...data: Array<any>) => any;
};

export declare const render: (
component: React.ReactElement<any>,
options?: RenderOptions
) => RenderAPI;

export declare const cleanup: () => void;
export declare const fireEvent: FireEventAPI;

type WaitForOptions = {
timeout?: number;
interval?: number;
Expand All @@ -290,14 +298,15 @@ export type WaitForFunction = <T = any>(
options?: WaitForOptions
) => Promise<T>;

export declare const render: (
component: React.ReactElement<any>,
options?: RenderOptions
) => RenderAPI;

export declare const cleanup: () => void;
export declare const fireEvent: FireEventAPI;
export declare const waitFor: WaitForFunction;

export type WaitForElementToBeRemovedFunction = <T = any>(
expectation: () => T,
options?: WaitForOptions
) => Promise<null>;

export declare const waitForElementToBeRemoved: WaitForElementToBeRemovedFunction;

export declare const act: (callback: () => void) => Thenable;
export declare const within: (instance: ReactTestInstance) => Queries;

Expand Down