Skip to content

Commit 718aaae

Browse files
committed
basic impl
1 parent 463253d commit 718aaae

File tree

4 files changed

+143
-11
lines changed

4 files changed

+143
-11
lines changed

src/__tests__/react-suspense.test.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as React from 'react';
22
import { View } from 'react-native';
3-
import { render, screen, within, configure } from '..';
43
import TestRenderer, { type ReactTestRenderer } from 'react-test-renderer';
54

5+
import { configure, renderAsync, screen, within } from '..';
6+
67
configure({
78
asyncUtilTimeout: 5000,
89
});
@@ -21,7 +22,7 @@ function Suspendable<T>({ promise }: { promise: Promise<T> }) {
2122
}
2223

2324
test('render supports components which can suspend', async () => {
24-
render(
25+
await renderAsync(
2526
<View>
2627
<React.Suspense fallback={<View testID="fallback" />}>
2728
<Suspendable promise={wait(100)} />
@@ -30,16 +31,10 @@ test('render supports components which can suspend', async () => {
3031
);
3132

3233
expect(screen.getByTestId('fallback')).toBeOnTheScreen();
33-
34-
// eslint-disable-next-line require-await
35-
await React.act(async () => {
36-
await wait(1000);
37-
});
38-
3934
expect(await screen.findByTestId('test')).toBeOnTheScreen();
4035
});
4136

42-
test.only('react test renderer supports components which can suspend', async () => {
37+
test('react test renderer supports components which can suspend', async () => {
4338
let renderer: ReactTestRenderer;
4439

4540
// eslint-disable-next-line require-await
@@ -60,7 +55,7 @@ test.only('react test renderer supports components which can suspend', async ()
6055
expect(await view.findByTestId('test')).toBeDefined();
6156
});
6257

63-
test.only('react test renderer supports components which can suspend 500', async () => {
58+
test('react test renderer supports components which can suspend 500', async () => {
6459
let renderer: ReactTestRenderer;
6560

6661
// eslint-disable-next-line require-await
@@ -81,7 +76,7 @@ test.only('react test renderer supports components which can suspend 500', async
8176
expect(await view.findByTestId('test')).toBeDefined();
8277
});
8378

84-
test.only('react test renderer supports components which can suspend 1000ms', async () => {
79+
test('react test renderer supports components which can suspend 1000ms', async () => {
8580
let renderer: ReactTestRenderer;
8681

8782
// eslint-disable-next-line require-await

src/pure.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { default as act } from './act';
22
export { default as cleanup } from './cleanup';
33
export { default as fireEvent } from './fire-event';
44
export { default as render } from './render';
5+
export { default as renderAsync } from './render-async';
56
export { default as waitFor } from './wait-for';
67
export { default as waitForElementToBeRemoved } from './wait-for-element-to-be-removed';
78
export { within, getQueriesForElement } from './within';
@@ -19,6 +20,7 @@ export type {
1920
RenderResult as RenderAPI,
2021
DebugFunction,
2122
} from './render';
23+
export type { RenderAsyncOptions, RenderAsyncResult } from './render-async';
2224
export type { RenderHookOptions, RenderHookResult } from './render-hook';
2325
export type { Config } from './config';
2426
export type { UserEventConfig } from './user-event';

src/render-act.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,19 @@ export function renderWithAct(
1818
// @ts-expect-error: `act` is synchronous, so `renderer` is already initialized here
1919
return renderer;
2020
}
21+
22+
export async function renderWithAsyncAct(
23+
component: React.ReactElement,
24+
options?: Partial<TestRendererOptions>,
25+
): Promise<ReactTestRenderer> {
26+
let renderer: ReactTestRenderer;
27+
28+
// eslint-disable-next-line require-await
29+
await act(async () => {
30+
// @ts-expect-error `TestRenderer.create` is not typed correctly
31+
renderer = TestRenderer.create(component, options);
32+
});
33+
34+
// @ts-expect-error: `renderer` is already initialized here
35+
return renderer;
36+
}

src/render-async.tsx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import * as React from 'react';
2+
import type {
3+
ReactTestInstance,
4+
ReactTestRenderer,
5+
TestRendererOptions,
6+
} from 'react-test-renderer';
7+
8+
import act from './act';
9+
import { addToCleanupQueue } from './cleanup';
10+
import { getConfig } from './config';
11+
import { getHostSelves } from './helpers/component-tree';
12+
import type { DebugOptions } from './helpers/debug';
13+
import { debug } from './helpers/debug';
14+
import { renderWithAsyncAct } from './render-act';
15+
import { setRenderResult } from './screen';
16+
import { getQueriesForElement } from './within';
17+
18+
export interface RenderAsyncOptions {
19+
/**
20+
* Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating
21+
* reusable custom render functions for common data providers.
22+
*/
23+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
24+
wrapper?: React.ComponentType<any>;
25+
26+
/**
27+
* Set to `false` to disable concurrent rendering.
28+
* Otherwise `render` will default to concurrent rendering.
29+
*/
30+
// TODO: should we assume concurrentRoot is true for react suspense?
31+
concurrentRoot?: boolean;
32+
33+
createNodeMock?: (element: React.ReactElement) => unknown;
34+
}
35+
36+
export type RenderAsyncResult = ReturnType<typeof renderAsync>;
37+
38+
/**
39+
* Renders test component deeply using React Test Renderer and exposes helpers
40+
* to assert on the output.
41+
*/
42+
export default async function renderAsync<T>(
43+
component: React.ReactElement<T>,
44+
options: RenderAsyncOptions = {},
45+
) {
46+
const { wrapper: Wrapper, concurrentRoot, ...rest } = options || {};
47+
48+
const testRendererOptions: TestRendererOptions = {
49+
...rest,
50+
// @ts-expect-error incomplete typing on RTR package
51+
unstable_isConcurrent: concurrentRoot ?? getConfig().concurrentRoot,
52+
};
53+
54+
const wrap = (element: React.ReactElement) => (Wrapper ? <Wrapper>{element}</Wrapper> : element);
55+
const renderer = await renderWithAsyncAct(wrap(component), testRendererOptions);
56+
return buildRenderResult(renderer, wrap);
57+
}
58+
59+
function buildRenderResult(
60+
renderer: ReactTestRenderer,
61+
wrap: (element: React.ReactElement) => React.JSX.Element,
62+
) {
63+
const update = updateWithAsyncAct(renderer, wrap);
64+
const instance = renderer.root;
65+
66+
// TODO: test this
67+
const unmount = async () => {
68+
// eslint-disable-next-line require-await
69+
await act(async () => {
70+
renderer.unmount();
71+
});
72+
};
73+
74+
addToCleanupQueue(unmount);
75+
76+
const result = {
77+
...getQueriesForElement(instance),
78+
update,
79+
unmount,
80+
rerender: update, // alias for `update`
81+
toJSON: renderer.toJSON,
82+
debug: makeDebug(renderer),
83+
get root(): ReactTestInstance {
84+
return getHostSelves(instance)[0];
85+
},
86+
UNSAFE_root: instance,
87+
};
88+
89+
// Add as non-enumerable property, so that it's safe to enumerate
90+
// `render` result, e.g. using destructuring rest syntax.
91+
Object.defineProperty(result, 'container', {
92+
enumerable: false,
93+
get() {
94+
throw new Error(
95+
"'container' property has been renamed to 'UNSAFE_root'.\n\n" +
96+
"Consider using 'root' property which returns root host element.",
97+
);
98+
},
99+
});
100+
101+
setRenderResult(result);
102+
103+
return result;
104+
}
105+
106+
// TODO: test this
107+
function updateWithAsyncAct(
108+
renderer: ReactTestRenderer,
109+
wrap: (innerElement: React.ReactElement) => React.ReactElement,
110+
) {
111+
return async function (component: React.ReactElement) {
112+
// eslint-disable-next-line require-await
113+
await act(async () => {
114+
renderer.update(wrap(component));
115+
});
116+
};
117+
}
118+
119+
export type DebugFunction = (options?: DebugOptions) => void;

0 commit comments

Comments
 (0)