Skip to content

Commit 2fd90fe

Browse files
committed
async fireEvent; async screen methods
1 parent 7b84041 commit 2fd90fe

10 files changed

+835
-130
lines changed

src/__tests__/fire-event-async.test.tsx

Lines changed: 668 additions & 0 deletions
Large diffs are not rendered by default.

src/__tests__/render-async-fake-timers.tsx

Lines changed: 0 additions & 62 deletions
This file was deleted.

src/__tests__/render-async.tsx

Lines changed: 0 additions & 60 deletions
This file was deleted.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as React from 'react';
2+
import { Text, View } from 'react-native';
3+
import { act, renderAsync, screen } from '..';
4+
5+
jest.useFakeTimers();
6+
7+
const testGateReact19 = React.version.startsWith('19.') ? test : test.skip;
8+
9+
function Suspending({ promise }: { promise: Promise<unknown> }) {
10+
React.use(promise);
11+
return <View testID="content" />;
12+
}
13+
14+
testGateReact19('resolves timer-controlled promise', async () => {
15+
const promise = new Promise((resolve) => {
16+
setTimeout(() => resolve(null), 100);
17+
});
18+
19+
await renderAsync(
20+
<View>
21+
<React.Suspense fallback={<Text>Loading...</Text>}>
22+
<Suspending promise={promise} />
23+
<View testID="sibling" />
24+
</React.Suspense>
25+
</View>,
26+
);
27+
expect(screen.getByText('Loading...')).toBeOnTheScreen();
28+
expect(screen.queryByTestId('content')).not.toBeOnTheScreen();
29+
expect(screen.queryByTestId('sibling')).not.toBeOnTheScreen();
30+
31+
await act(async () => jest.runOnlyPendingTimers());
32+
expect(screen.getByTestId('content')).toBeOnTheScreen();
33+
expect(screen.getByTestId('sibling')).toBeOnTheScreen();
34+
expect(screen.queryByText('Loading...')).not.toBeOnTheScreen();
35+
});

src/__tests__/suspense.test.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as React from 'react';
2+
import { Text, View } from 'react-native';
3+
import { act, renderAsync, screen } from '..';
4+
5+
const testGateReact19 = React.version.startsWith('19.') ? test : test.skip;
6+
7+
function Suspending({ promise }: { promise: Promise<unknown> }) {
8+
React.use(promise);
9+
return <View testID="content" />;
10+
}
11+
12+
testGateReact19('resolves manually-controlled promise', async () => {
13+
let resolvePromise: (value: void) => void;
14+
const promise = new Promise((resolve) => {
15+
resolvePromise = resolve;
16+
});
17+
18+
await renderAsync(
19+
<View>
20+
<React.Suspense fallback={<Text>Loading...</Text>}>
21+
<Suspending promise={promise} />
22+
<View testID="sibling" />
23+
</React.Suspense>
24+
</View>,
25+
);
26+
expect(screen.getByText('Loading...')).toBeOnTheScreen();
27+
expect(screen.queryByTestId('content')).not.toBeOnTheScreen();
28+
expect(screen.queryByTestId('sibling')).not.toBeOnTheScreen();
29+
30+
await act(async () => resolvePromise());
31+
expect(screen.getByTestId('content')).toBeOnTheScreen();
32+
expect(screen.getByTestId('sibling')).toBeOnTheScreen();
33+
expect(screen.queryByText('Loading...')).not.toBeOnTheScreen();
34+
});
35+
36+
testGateReact19('resolves timer-controlled promise', async () => {
37+
const promise = new Promise((resolve) => {
38+
setTimeout(() => resolve(null), 100);
39+
});
40+
41+
await renderAsync(
42+
<View>
43+
<React.Suspense fallback={<Text>Loading...</Text>}>
44+
<Suspending promise={promise} />
45+
<View testID="sibling" />
46+
</React.Suspense>
47+
</View>,
48+
);
49+
expect(screen.getByText('Loading...')).toBeOnTheScreen();
50+
expect(screen.queryByTestId('content')).not.toBeOnTheScreen();
51+
expect(screen.queryByTestId('sibling')).not.toBeOnTheScreen();
52+
53+
expect(await screen.findByTestId('content')).toBeOnTheScreen();
54+
expect(screen.getByTestId('sibling')).toBeOnTheScreen();
55+
expect(screen.queryByText('Loading...')).not.toBeOnTheScreen();
56+
});

src/fire-event.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,36 @@ fireEvent.changeText = (element: ReactTestInstance, ...data: unknown[]) =>
134134
fireEvent.scroll = (element: ReactTestInstance, ...data: unknown[]) =>
135135
fireEvent(element, 'scroll', ...data);
136136

137+
async function fireEventAsync(element: ReactTestInstance, eventName: EventName, ...data: unknown[]) {
138+
if (!isElementMounted(element)) {
139+
return;
140+
}
141+
142+
setNativeStateIfNeeded(element, eventName, data[0]);
143+
144+
const handler = findEventHandler(element, eventName);
145+
if (!handler) {
146+
return;
147+
}
148+
149+
let returnValue;
150+
await act(async () => {
151+
returnValue = handler(...data);
152+
});
153+
154+
return returnValue;
155+
}
156+
157+
fireEventAsync.press = async (element: ReactTestInstance, ...data: unknown[]) =>
158+
fireEventAsync(element, 'press', ...data);
159+
160+
fireEventAsync.changeText = async (element: ReactTestInstance, ...data: unknown[]) =>
161+
fireEventAsync(element, 'changeText', ...data);
162+
163+
fireEventAsync.scroll = async (element: ReactTestInstance, ...data: unknown[]) =>
164+
fireEventAsync(element, 'scroll', ...data);
165+
166+
export { fireEventAsync };
137167
export default fireEvent;
138168

139169
const scrollEventNames = new Set([

src/pure.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export { default as act } from './act';
22
export { default as cleanup } from './cleanup';
3-
export { default as fireEvent } from './fire-event';
3+
export { default as fireEvent, fireEventAsync } from './fire-event';
44
export { default as render } from './render';
55
export { default as renderAsync } from './render-async';
66
export { default as waitFor } from './wait-for';

src/render-async.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,25 @@ function buildRenderResult(
6060
renderer: ReactTestRenderer,
6161
wrap: (element: React.ReactElement) => React.JSX.Element,
6262
) {
63-
const update = updateWithAsyncAct(renderer, wrap);
6463
const instance = renderer.root;
6564

66-
// TODO: test this
67-
const unmount = async () => {
68-
// eslint-disable-next-line require-await
65+
const update = function (component: React.ReactElement) {
66+
void act(() => {
67+
renderer.update(wrap(component));
68+
});
69+
};
70+
const updateAsync = async function (component: React.ReactElement) {
71+
await act(async () => {
72+
renderer.update(wrap(component));
73+
});
74+
};
75+
76+
const unmount = () => {
77+
void act(() => {
78+
renderer.unmount();
79+
});
80+
};
81+
const unmountAsync = async () => {
6982
await act(async () => {
7083
renderer.unmount();
7184
});
@@ -76,8 +89,11 @@ function buildRenderResult(
7689
const result = {
7790
...getQueriesForElement(instance),
7891
update,
79-
unmount,
92+
updateAsync,
8093
rerender: update, // alias for `update`
94+
rerenderAsync: updateAsync, // alias for `update`
95+
unmount,
96+
unmountAsync,
8197
toJSON: renderer.toJSON,
8298
debug: makeDebug(renderer),
8399
get root(): ReactTestInstance {

src/render.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { validateStringsRenderedWithinText } from './helpers/string-validation';
1515
import { renderWithAct } from './render-act';
1616
import { setRenderResult } from './screen';
1717
import { getQueriesForElement } from './within';
18+
import renderAsync from './render-async';
1819

1920
export interface RenderOptions {
2021
/**
@@ -98,22 +99,40 @@ function buildRenderResult(
9899
renderer: ReactTestRenderer,
99100
wrap: (element: React.ReactElement) => React.JSX.Element,
100101
) {
101-
const update = updateWithAct(renderer, wrap);
102102
const instance = renderer.root;
103103

104+
const update = function (component: React.ReactElement) {
105+
void act(() => {
106+
renderer.update(wrap(component));
107+
});
108+
};
109+
const updateAsync = async function (component: React.ReactElement) {
110+
await act(async () => {
111+
renderer.update(wrap(component));
112+
});
113+
};
114+
104115
const unmount = () => {
105116
void act(() => {
106117
renderer.unmount();
107118
});
108119
};
120+
const unmountAsync = async () => {
121+
await act(async () => {
122+
renderer.unmount();
123+
});
124+
};
109125

110126
addToCleanupQueue(unmount);
111127

112128
const result = {
113129
...getQueriesForElement(instance),
114130
update,
115-
unmount,
131+
updateAsync,
116132
rerender: update, // alias for `update`
133+
rerenderAsync: updateAsync, // alias for `update`
134+
unmount,
135+
unmountAsync,
117136
toJSON: renderer.toJSON,
118137
debug: makeDebug(renderer),
119138
get root(): ReactTestInstance {

src/screen.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ const defaultScreen: Screen = {
2626
},
2727
debug: notImplementedDebug,
2828
update: notImplemented,
29+
updateAsync: notImplemented,
2930
unmount: notImplemented,
31+
unmountAsync: notImplemented,
3032
rerender: notImplemented,
33+
rerenderAsync: notImplemented,
3134
toJSON: notImplemented,
3235
getByLabelText: notImplemented,
3336
getAllByLabelText: notImplemented,

0 commit comments

Comments
 (0)