Skip to content

Commit 98eb5ae

Browse files
authored
TestRenderer toJSON should not expose the Array wrapper Suspense uses for hidden trees (#14392)
1 parent 39489e7 commit 98eb5ae

File tree

3 files changed

+86
-4
lines changed

3 files changed

+86
-4
lines changed

packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
267267
describe('when suspending during mount', () => {
268268
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
269269
const root = ReactTestRenderer.create(<App shouldSuspend={true} />);
270-
expect(root.toJSON()).toEqual(['Loading...']);
270+
expect(root.toJSON()).toEqual('Loading...');
271271
expect(onRender).toHaveBeenCalledTimes(1);
272272

273273
// Initial mount only shows the "Loading..." Fallback.
@@ -343,7 +343,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
343343
expect(onRender.mock.calls[0][3]).toBe(5);
344344

345345
root.update(<App shouldSuspend={true} textRenderDuration={5} />);
346-
expect(root.toJSON()).toEqual(['Loading...']);
346+
expect(root.toJSON()).toEqual('Loading...');
347347
expect(onRender).toHaveBeenCalledTimes(2);
348348

349349
// The suspense update should only show the "Loading..." Fallback.
@@ -355,7 +355,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
355355
root.update(
356356
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
357357
);
358-
expect(root.toJSON()).toEqual(['Loading...']);
358+
expect(root.toJSON()).toEqual('Loading...');
359359
expect(onRender).toHaveBeenCalledTimes(3);
360360

361361
// If we force another update while still timed out,

packages/react-test-renderer/src/ReactTestRenderer.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,15 @@ const ReactTestRendererFiber = {
461461
if (container.children.length === 1) {
462462
return toJSON(container.children[0]);
463463
}
464-
464+
if (
465+
container.children.length === 2 &&
466+
container.children[0].isHidden === true &&
467+
container.children[1].isHidden === false
468+
) {
469+
// Omit timed out children from output entirely, including the fact that we
470+
// temporarily wrap fallback and timed out children in an array.
471+
return toJSON(container.children[1]);
472+
}
465473
let renderedChildren = null;
466474
if (container.children && container.children.length) {
467475
for (let i = 0; i < container.children.length; i++) {

packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js

+74
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const ReactDOM = require('react-dom');
1313

1414
// Isolate test renderer.
1515
jest.resetModules();
16+
const React = require('react');
17+
const ReactCache = require('react-cache');
1618
const ReactTestRenderer = require('react-test-renderer');
1719

1820
describe('ReactTestRenderer', () => {
@@ -26,4 +28,76 @@ describe('ReactTestRenderer', () => {
2628
withoutStack: true,
2729
});
2830
});
31+
32+
describe('timed out Suspense hidden subtrees should not be observable via toJSON', () => {
33+
let AsyncText;
34+
let PendingResources;
35+
let TextResource;
36+
37+
beforeEach(() => {
38+
PendingResources = {};
39+
TextResource = ReactCache.unstable_createResource(
40+
text =>
41+
new Promise(resolve => {
42+
PendingResources[text] = resolve;
43+
}),
44+
text => text,
45+
);
46+
47+
AsyncText = ({text}) => {
48+
const value = TextResource.read(text);
49+
return value;
50+
};
51+
});
52+
53+
it('for root Suspense components', async done => {
54+
const App = ({text}) => {
55+
return (
56+
<React.Suspense fallback="fallback">
57+
<AsyncText text={text} />
58+
</React.Suspense>
59+
);
60+
};
61+
62+
const root = ReactTestRenderer.create(<App text="initial" />);
63+
PendingResources.initial('initial');
64+
await Promise.resolve();
65+
expect(root.toJSON()).toEqual('initial');
66+
67+
root.update(<App text="dynamic" />);
68+
expect(root.toJSON()).toEqual('fallback');
69+
70+
PendingResources.dynamic('dynamic');
71+
await Promise.resolve();
72+
expect(root.toJSON()).toEqual('dynamic');
73+
74+
done();
75+
});
76+
77+
it('for nested Suspense components', async done => {
78+
const App = ({text}) => {
79+
return (
80+
<div>
81+
<React.Suspense fallback="fallback">
82+
<AsyncText text={text} />
83+
</React.Suspense>
84+
</div>
85+
);
86+
};
87+
88+
const root = ReactTestRenderer.create(<App text="initial" />);
89+
PendingResources.initial('initial');
90+
await Promise.resolve();
91+
expect(root.toJSON().children).toEqual(['initial']);
92+
93+
root.update(<App text="dynamic" />);
94+
expect(root.toJSON().children).toEqual(['fallback']);
95+
96+
PendingResources.dynamic('dynamic');
97+
await Promise.resolve();
98+
expect(root.toJSON().children).toEqual(['dynamic']);
99+
100+
done();
101+
});
102+
});
29103
});

0 commit comments

Comments
 (0)