TestRenderer toJSON should not expose the Array wrapper Suspense uses for hidden trees (#14392)
Brian Vaughn committed
Dec 5, 2018 at 11:09 UTC
98eb5ae5319e1d26b5b89a33ead80716db725f83
3 files changed
+86
-4
packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
+3
-3
@@ -267,7 +267,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
267
describe('when suspending during mount', () => {
268
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
269
const root = ReactTestRenderer.create(<App shouldSuspend={true} />);
270
- expect(root.toJSON()).toEqual(['Loading...']);
270
+ expect(root.toJSON()).toEqual('Loading...');
271
expect(onRender).toHaveBeenCalledTimes(1);
272
273
// Initial mount only shows the "Loading..." Fallback.
@@ -343,7 +343,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
343
expect(onRender.mock.calls[0][3]).toBe(5);
344
345
root.update(<App shouldSuspend={true} textRenderDuration={5} />);
346
- expect(root.toJSON()).toEqual(['Loading...']);
346
+ expect(root.toJSON()).toEqual('Loading...');
347
expect(onRender).toHaveBeenCalledTimes(2);
348
349
// The suspense update should only show the "Loading..." Fallback.
@@ -355,7 +355,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
355
root.update(
356
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
357
);
358
- expect(root.toJSON()).toEqual(['Loading...']);
358
+ expect(root.toJSON()).toEqual('Loading...');
359
expect(onRender).toHaveBeenCalledTimes(3);
360
361
// If we force another update while still timed out,
packages/react-test-renderer/src/ReactTestRenderer.js
+9
-1
@@ -461,7 +461,15 @@ const ReactTestRendererFiber = {
461
if (container.children.length === 1) {
462
return toJSON(container.children[0]);
463
}
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
+ }
473
let renderedChildren = null;
474
if (container.children && container.children.length) {
475
for (let i = 0; i < container.children.length; i++) {
packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js
+74
@@ -13,6 +13,8 @@ const ReactDOM = require('react-dom');
13
14
// Isolate test renderer.
15
jest.resetModules();
16
+const React = require('react');
17
+const ReactCache = require('react-cache');
18
const ReactTestRenderer = require('react-test-renderer');
19
20
describe('ReactTestRenderer', () => {
@@ -26,4 +28,76 @@ describe('ReactTestRenderer', () => {
28
withoutStack: true,
29
});
30
});
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
+ });
103
});