@samitouri / QOS-React / commits / bc8bd24c14

Run persistent mode tests in CI (#15029)

* Add command to run tests in persistent mode * Convert Suspense fuzz tester to use noop renderer So we can run it in persistent mode, too. * Don't mutate stateNode in appendAllChildren We can't mutate the stateNode in appendAllChildren because the children could be current. This is a bit weird because now the child that we append is different from the one on the fiber stateNode. I think this makes conceptual sense, but I suspect this likely breaks an assumption in Fabric. With this approach, we no longer need to clone to unhide the children, so I removed those host config methods. Fixes bug surfaced by fuzz tester. (The test case that failed was the one that's already hard coded.) * In persistent mode, disable test that reads a ref Refs behave differently in persistent mode. I added a TODO to write a persistent mode version of this test. * Run persistent mode tests in CI * test-persistent should skip files without noop If a file doesn't reference react-noop-renderer, we shouldn't bother running it in persistent mode, since the results will be identical to the normal test run. * Remove module constructor from placeholder tests We don't need this now that we have the ability to run any test file in either mutation or persistent mode. * Revert "test-persistent should skip files without noop" Seb objected to adding shelljs as a dep and I'm too lazy to worry about Windows support so whatever I'll just revert this. * Delete duplicate file

Andrew Clark committed Mar 11, 2019 at 10:56 UTC bc8bd24c145d386be7f014f04fc927067d18412a
12 files changed +611 -699
package.json
+1
@@ -100,6 +100,7 @@
100 "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js",
101 "debug-test": "cross-env NODE_ENV=development node --inspect-brk node_modules/.bin/jest --config ./scripts/jest/config.source.js --runInBand",
102 "test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js",
103 + "test-persistent": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-persistent.js",
104 "test-fire": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-fire.js",
105 "test-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source.js",
106 "test-fire-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source-fire.js",
packages/react-native-renderer/src/ReactFabricHostConfig.js
-27
@@ -387,25 +387,6 @@ export function cloneHiddenInstance(
387 };
388 }
389
390 -export function cloneUnhiddenInstance(
391 - instance: Instance,
392 - type: string,
393 - props: Props,
394 - internalInstanceHandle: Object,
395 -): Instance {
396 - const viewConfig = instance.canonical.viewConfig;
397 - const node = instance.node;
398 - const updatePayload = diff(
399 - {...props, style: [props.style, {display: 'none'}]},
400 - props,
401 - viewConfig.validAttributes,
402 - );
403 - return {
404 - node: cloneNodeWithNewProps(node, updatePayload),
405 - canonical: instance.canonical,
406 - };
407 -}
408 -
390 export function cloneHiddenTextInstance(
391 instance: Instance,
392 text: string,
@@ -414,14 +395,6 @@ export function cloneHiddenTextInstance(
395 throw new Error('Not yet implemented.');
396 }
397
417 -export function cloneUnhiddenTextInstance(
418 - instance: Instance,
419 - text: string,
420 - internalInstanceHandle: Object,
421 -): TextInstance {
422 - throw new Error('Not yet implemented.');
423 -}
424 -
398 export function createContainerChildSet(container: Container): ChildSet {
399 return createChildNodeSet(container);
400 }
packages/react-noop-renderer/src/createReactNoop.js
-43
@@ -486,26 +486,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
486 return clone;
487 },
488
489 - cloneUnhiddenInstance(
490 - instance: Instance,
491 - type: string,
492 - props: Props,
493 - internalInstanceHandle: Object,
494 - ): Instance {
495 - const clone = cloneInstance(
496 - instance,
497 - null,
498 - type,
499 - props,
500 - props,
501 - internalInstanceHandle,
502 - true,
503 - null,
504 - );
505 - clone.hidden = props.hidden === true;
506 - return clone;
507 - },
508 -
489 cloneHiddenTextInstance(
490 instance: TextInstance,
491 text: string,
@@ -528,29 +508,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
508 });
509 return clone;
510 },
531 -
532 - cloneUnhiddenTextInstance(
533 - instance: TextInstance,
534 - text: string,
535 - internalInstanceHandle: Object,
536 - ): TextInstance {
537 - const clone = {
538 - text: instance.text,
539 - id: instanceCounter++,
540 - hidden: false,
541 - context: instance.context,
542 - };
543 - // Hide from unit tests
544 - Object.defineProperty(clone, 'id', {
545 - value: clone.id,
546 - enumerable: false,
547 - });
548 - Object.defineProperty(clone, 'context', {
549 - value: clone.context,
550 - enumerable: false,
551 - });
552 - return clone;
553 - },
511 };
512
513 const NoopRenderer = reconciler(hostConfig);
packages/react-reconciler/src/ReactFiberCompleteWork.js
+59 -69
@@ -59,9 +59,7 @@ import {
59 supportsPersistence,
60 cloneInstance,
61 cloneHiddenInstance,
62 - cloneUnhiddenInstance,
62 cloneHiddenTextInstance,
64 - cloneUnhiddenTextInstance,
63 createContainerChildSet,
64 appendChildToContainerChildSet,
65 finalizeContainerChildren,
@@ -209,31 +207,19 @@ if (supportsMutation) {
207 // eslint-disable-next-line no-labels
208 branches: if (node.tag === HostComponent) {
209 let instance = node.stateNode;
212 - if (needsVisibilityToggle) {
210 + if (needsVisibilityToggle && isHidden) {
211 + // This child is inside a timed out tree. Hide it.
212 const props = node.memoizedProps;
213 const type = node.type;
215 - if (isHidden) {
216 - // This child is inside a timed out tree. Hide it.
217 - instance = cloneHiddenInstance(instance, type, props, node);
218 - } else {
219 - // This child was previously inside a timed out tree. If it was not
220 - // updated during this render, it may need to be unhidden. Clone
221 - // again to be sure.
222 - instance = cloneUnhiddenInstance(instance, type, props, node);
223 - }
224 - node.stateNode = instance;
214 + instance = cloneHiddenInstance(instance, type, props, node);
215 }
216 appendInitialChild(parent, instance);
217 } else if (node.tag === HostText) {
218 let instance = node.stateNode;
229 - if (needsVisibilityToggle) {
219 + if (needsVisibilityToggle && isHidden) {
220 + // This child is inside a timed out tree. Hide it.
221 const text = node.memoizedProps;
231 - if (isHidden) {
232 - instance = cloneHiddenTextInstance(instance, text, node);
233 - } else {
234 - instance = cloneUnhiddenTextInstance(instance, text, node);
235 - }
236 - node.stateNode = instance;
222 + instance = cloneHiddenTextInstance(instance, text, node);
223 }
224 appendInitialChild(parent, instance);
225 } else if (node.tag === HostPortal) {
@@ -247,15 +233,22 @@ if (supportsMutation) {
233 if (newIsHidden) {
234 const primaryChildParent = node.child;
235 if (primaryChildParent !== null) {
250 - appendAllChildren(parent, primaryChildParent, true, newIsHidden);
251 - node = primaryChildParent.sibling;
252 - continue;
236 + if (primaryChildParent.child !== null) {
237 + primaryChildParent.child.return = primaryChildParent;
238 + appendAllChildren(
239 + parent,
240 + primaryChildParent,
241 + true,
242 + newIsHidden,
243 + );
244 + }
245 + const fallbackChildParent = primaryChildParent.sibling;
246 + if (fallbackChildParent !== null) {
247 + fallbackChildParent.return = node;
248 + node = fallbackChildParent;
249 + continue;
250 + }
251 }
254 - } else {
255 - const primaryChildParent = node;
256 - appendAllChildren(parent, primaryChildParent, true, newIsHidden);
257 - // eslint-disable-next-line no-labels
258 - break branches;
252 }
253 }
254 if (node.child !== null) {
@@ -299,31 +292,19 @@ if (supportsMutation) {
292 // eslint-disable-next-line no-labels
293 branches: if (node.tag === HostComponent) {
294 let instance = node.stateNode;
302 - if (needsVisibilityToggle) {
295 + if (needsVisibilityToggle && isHidden) {
296 + // This child is inside a timed out tree. Hide it.
297 const props = node.memoizedProps;
298 const type = node.type;
305 - if (isHidden) {
306 - // This child is inside a timed out tree. Hide it.
307 - instance = cloneHiddenInstance(instance, type, props, node);
308 - } else {
309 - // This child was previously inside a timed out tree. If it was not
310 - // updated during this render, it may need to be unhidden. Clone
311 - // again to be sure.
312 - instance = cloneUnhiddenInstance(instance, type, props, node);
313 - }
314 - node.stateNode = instance;
299 + instance = cloneHiddenInstance(instance, type, props, node);
300 }
301 appendChildToContainerChildSet(containerChildSet, instance);
302 } else if (node.tag === HostText) {
303 let instance = node.stateNode;
319 - if (needsVisibilityToggle) {
304 + if (needsVisibilityToggle && isHidden) {
305 + // This child is inside a timed out tree. Hide it.
306 const text = node.memoizedProps;
321 - if (isHidden) {
322 - instance = cloneHiddenTextInstance(instance, text, node);
323 - } else {
324 - instance = cloneUnhiddenTextInstance(instance, text, node);
325 - }
326 - node.stateNode = instance;
307 + instance = cloneHiddenTextInstance(instance, text, node);
308 }
309 appendChildToContainerChildSet(containerChildSet, instance);
310 } else if (node.tag === HostPortal) {
@@ -337,25 +318,22 @@ if (supportsMutation) {
318 if (newIsHidden) {
319 const primaryChildParent = node.child;
320 if (primaryChildParent !== null) {
340 - appendAllChildrenToContainer(
341 - containerChildSet,
342 - primaryChildParent,
343 - true,
344 - newIsHidden,
345 - );
346 - node = primaryChildParent.sibling;
347 - continue;
321 + if (primaryChildParent.child !== null) {
322 + primaryChildParent.child.return = primaryChildParent;
323 + appendAllChildrenToContainer(
324 + containerChildSet,
325 + primaryChildParent,
326 + true,
327 + newIsHidden,
328 + );
329 + }
330 + const fallbackChildParent = primaryChildParent.sibling;
331 + if (fallbackChildParent !== null) {
332 + fallbackChildParent.return = node;
333 + node = fallbackChildParent;
334 + continue;
335 + }
336 }
349 - } else {
350 - const primaryChildParent = node;
351 - appendAllChildrenToContainer(
352 - containerChildSet,
353 - primaryChildParent,
354 - true,
355 - newIsHidden,
356 - );
357 - // eslint-disable-next-line no-labels
358 - break branches;
337 }
338 }
339 if (node.child !== null) {
@@ -714,11 +692,23 @@ function completeWork(
692 }
693 }
694
717 - if (nextDidTimeout || prevDidTimeout) {
718 - // If the children are hidden, or if they were previous hidden, schedule
719 - // an effect to toggle their visibility. This is also used to attach a
720 - // retry listener to the promise.
721 - workInProgress.effectTag |= Update;
695 + if (supportsPersistence) {
696 + if (nextDidTimeout) {
697 + // If this boundary just timed out, schedule an effect to attach a
698 + // retry listener to the proimse. This flag is also used to hide the
699 + // primary children.
700 + workInProgress.effectTag |= Update;
701 + }
702 + }
703 + if (supportsMutation) {
704 + if (nextDidTimeout || prevDidTimeout) {
705 + // If this boundary just timed out, schedule an effect to attach a
706 + // retry listener to the proimse. This flag is also used to hide the
707 + // primary children. In mutation mode, we also need the flag to
708 + // *unhide* children that were previously hidden, so check if the
709 + // is currently timed out, too.
710 + workInProgress.effectTag |= Update;
711 + }
712 }
713 break;
714 }
packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js
+24 -29
@@ -1,6 +1,6 @@
1 let React;
2 let Suspense;
3 -let ReactTestRenderer;
3 +let ReactNoop;
4 let Scheduler;
5 let ReactFeatureFlags;
6 let Random;
@@ -26,7 +26,7 @@ describe('ReactSuspenseFuzz', () => {
26 ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
27 React = require('react');
28 Suspense = React.Suspense;
29 - ReactTestRenderer = require('react-test-renderer');
29 + ReactNoop = require('react-noop-renderer');
30 Scheduler = require('scheduler');
31 Random = require('random-seed');
32 });
@@ -143,28 +143,16 @@ describe('ReactSuspenseFuzz', () => {
143 return resolvedText;
144 }
145
146 - function renderToRoot(
147 - root,
148 - children,
149 - {shouldSuspend} = {shouldSuspend: true},
150 - ) {
151 - root.update(
152 - <ShouldSuspendContext.Provider value={shouldSuspend}>
153 - {children}
154 - </ShouldSuspendContext.Provider>,
155 - );
146 + function resolveAllTasks() {
147 Scheduler.unstable_flushWithoutYielding();
157 -
148 let elapsedTime = 0;
149 while (pendingTasks && pendingTasks.size > 0) {
150 if ((elapsedTime += 1000) > 1000000) {
151 throw new Error('Something did not resolve properly.');
152 }
163 - ReactTestRenderer.act(() => jest.advanceTimersByTime(1000));
153 + ReactNoop.act(() => jest.advanceTimersByTime(1000));
154 Scheduler.unstable_flushWithoutYielding();
155 }
166 -
167 - return root.toJSON();
156 }
157
158 function testResolvedOutput(unwrappedChildren) {
@@ -172,25 +160,32 @@ describe('ReactSuspenseFuzz', () => {
160 <Suspense fallback="Loading...">{unwrappedChildren}</Suspense>
161 );
162
175 - const expectedRoot = ReactTestRenderer.create(null);
176 - const expectedOutput = renderToRoot(expectedRoot, children, {
177 - shouldSuspend: false,
178 - });
179 - expectedRoot.unmount();
163 + resetCache();
164 + ReactNoop.renderToRootWithID(
165 + <ShouldSuspendContext.Provider value={false}>
166 + {children}
167 + </ShouldSuspendContext.Provider>,
168 + 'expected',
169 + );
170 + resolveAllTasks();
171 + const expectedOutput = ReactNoop.getChildrenAsJSX('expected');
172 + ReactNoop.renderToRootWithID(null, 'expected');
173 + Scheduler.unstable_flushWithoutYielding();
174
175 resetCache();
182 - const syncRoot = ReactTestRenderer.create(null);
183 - const syncOutput = renderToRoot(syncRoot, children);
176 + ReactNoop.renderLegacySyncRoot(children);
177 + resolveAllTasks();
178 + const syncOutput = ReactNoop.getChildrenAsJSX();
179 expect(syncOutput).toEqual(expectedOutput);
185 - syncRoot.unmount();
180 + ReactNoop.renderLegacySyncRoot(null);
181
182 resetCache();
188 - const concurrentRoot = ReactTestRenderer.create(null, {
189 - unstable_isConcurrent: true,
190 - });
191 - const concurrentOutput = renderToRoot(concurrentRoot, children);
183 + ReactNoop.renderToRootWithID(children, 'concurrent');
184 + Scheduler.unstable_flushWithoutYielding();
185 + resolveAllTasks();
186 + const concurrentOutput = ReactNoop.getChildrenAsJSX('concurrent');
187 expect(concurrentOutput).toEqual(expectedOutput);
193 - concurrentRoot.unmount();
188 + ReactNoop.renderToRootWithID(null, 'concurrent');
189 Scheduler.unstable_flushWithoutYielding();
190 }
191
packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
+468 -496
@@ -8,532 +8,504 @@
8 * @jest-environment node
9 */
10
11 -runPlaceholderTests('ReactSuspensePlaceholder (mutation)', () =>
12 - require('react-noop-renderer'),
13 -);
14 -runPlaceholderTests('ReactSuspensePlaceholder (persistence)', () =>
15 - require('react-noop-renderer/persistent'),
16 -);
17 -
18 -function runPlaceholderTests(suiteLabel, loadReactNoop) {
19 - let Profiler;
20 - let React;
21 - let ReactNoop;
22 - let Scheduler;
23 - let ReactFeatureFlags;
24 - let ReactCache;
25 - let Suspense;
26 - let TextResource;
27 - let textResourceShouldFail;
28 -
29 - describe(suiteLabel, () => {
30 - beforeEach(() => {
31 - jest.resetModules();
32 -
33 - ReactFeatureFlags = require('shared/ReactFeatureFlags');
34 - ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
35 - ReactFeatureFlags.enableProfilerTimer = true;
36 - ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
37 - React = require('react');
38 - ReactNoop = loadReactNoop();
39 - Scheduler = require('scheduler');
40 - ReactCache = require('react-cache');
41 -
42 - Profiler = React.unstable_Profiler;
43 - Suspense = React.Suspense;
44 -
45 - TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
46 - let listeners = null;
47 - let status = 'pending';
48 - let value = null;
49 - return {
50 - then(resolve, reject) {
51 - switch (status) {
52 - case 'pending': {
53 - if (listeners === null) {
54 - listeners = [{resolve, reject}];
55 - setTimeout(() => {
56 - if (textResourceShouldFail) {
57 - Scheduler.yieldValue(`Promise rejected [${text}]`);
58 - status = 'rejected';
59 - value = new Error('Failed to load: ' + text);
60 - listeners.forEach(listener => listener.reject(value));
61 - } else {
62 - Scheduler.yieldValue(`Promise resolved [${text}]`);
63 - status = 'resolved';
64 - value = text;
65 - listeners.forEach(listener => listener.resolve(value));
66 - }
67 - }, ms);
68 - } else {
69 - listeners.push({resolve, reject});
70 - }
71 - break;
72 - }
73 - case 'resolved': {
74 - resolve(value);
75 - break;
76 - }
77 - case 'rejected': {
78 - reject(value);
79 - break;
11 +let Profiler;
12 +let React;
13 +let ReactNoop;
14 +let Scheduler;
15 +let ReactFeatureFlags;
16 +let ReactCache;
17 +let Suspense;
18 +let TextResource;
19 +let textResourceShouldFail;
20 +
21 +describe('ReactSuspensePlaceholder', () => {
22 + beforeEach(() => {
23 + jest.resetModules();
24 +
25 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
26 + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
27 + ReactFeatureFlags.enableProfilerTimer = true;
28 + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
29 + React = require('react');
30 + ReactNoop = require('react-noop-renderer');
31 + Scheduler = require('scheduler');
32 + ReactCache = require('react-cache');
33 +
34 + Profiler = React.unstable_Profiler;
35 + Suspense = React.Suspense;
36 +
37 + TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
38 + let listeners = null;
39 + let status = 'pending';
40 + let value = null;
41 + return {
42 + then(resolve, reject) {
43 + switch (status) {
44 + case 'pending': {
45 + if (listeners === null) {
46 + listeners = [{resolve, reject}];
47 + setTimeout(() => {
48 + if (textResourceShouldFail) {
49 + Scheduler.yieldValue(`Promise rejected [${text}]`);
50 + status = 'rejected';
51 + value = new Error('Failed to load: ' + text);
52 + listeners.forEach(listener => listener.reject(value));
53 + } else {
54 + Scheduler.yieldValue(`Promise resolved [${text}]`);
55 + status = 'resolved';
56 + value = text;
57 + listeners.forEach(listener => listener.resolve(value));
58 + }
59 + }, ms);
60 + } else {
61 + listeners.push({resolve, reject});
62 }
63 + break;
64 }
82 - },
83 - };
84 - }, ([text, ms]) => text);
85 - textResourceShouldFail = false;
86 - });
65 + case 'resolved': {
66 + resolve(value);
67 + break;
68 + }
69 + case 'rejected': {
70 + reject(value);
71 + break;
72 + }
73 + }
74 + },
75 + };
76 + }, ([text, ms]) => text);
77 + textResourceShouldFail = false;
78 + });
79
88 - function Text({fakeRenderDuration = 0, text = 'Text'}) {
89 - Scheduler.advanceTime(fakeRenderDuration);
80 + function Text({fakeRenderDuration = 0, text = 'Text'}) {
81 + Scheduler.advanceTime(fakeRenderDuration);
82 + Scheduler.yieldValue(text);
83 + return text;
84 + }
85 +
86 + function AsyncText({fakeRenderDuration = 0, ms, text}) {
87 + Scheduler.advanceTime(fakeRenderDuration);
88 + try {
89 + TextResource.read([text, ms]);
90 Scheduler.yieldValue(text);
91 return text;
92 + } catch (promise) {
93 + if (typeof promise.then === 'function') {
94 + Scheduler.yieldValue(`Suspend! [${text}]`);
95 + } else {
96 + Scheduler.yieldValue(`Error! [${text}]`);
97 + }
98 + throw promise;
99 }
100 + }
101
94 - function AsyncText({fakeRenderDuration = 0, ms, text}) {
95 - Scheduler.advanceTime(fakeRenderDuration);
96 - try {
97 - TextResource.read([text, ms]);
102 + it('times out children that are already hidden', () => {
103 + class HiddenText extends React.PureComponent {
104 + render() {
105 + const text = this.props.text;
106 Scheduler.yieldValue(text);
99 - return text;
100 - } catch (promise) {
101 - if (typeof promise.then === 'function') {
102 - Scheduler.yieldValue(`Suspend! [${text}]`);
103 - } else {
104 - Scheduler.yieldValue(`Error! [${text}]`);
105 - }
106 - throw promise;
107 + return <span hidden={true}>{text}</span>;
108 }
109 }
110
110 - it('times out children that are already hidden', () => {
111 - class HiddenText extends React.PureComponent {
112 - render() {
113 - const text = this.props.text;
114 - Scheduler.yieldValue(text);
115 - return <span hidden={true}>{text}</span>;
116 - }
117 - }
118 -
119 - function App(props) {
120 - return (
121 - <Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
122 - <HiddenText text="A" />
123 - <span>
124 - <AsyncText ms={1000} text={props.middleText} />
125 - </span>
126 - <span>
127 - <Text text="C" />
128 - </span>
129 - </Suspense>
130 - );
131 - }
132 -
133 - // Initial mount
134 - ReactNoop.render(<App middleText="B" />);
135 -
136 - expect(Scheduler).toFlushAndYield([
137 - 'A',
138 - 'Suspend! [B]',
139 - 'C',
140 - 'Loading...',
141 - ]);
142 - expect(ReactNoop).toMatchRenderedOutput(null);
143 -
144 - jest.advanceTimersByTime(1000);
145 - expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
146 -
147 - expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
148 -
149 - expect(ReactNoop).toMatchRenderedOutput(
150 - <React.Fragment>
151 - <span hidden={true}>A</span>
152 - <span>B</span>
153 - <span>C</span>
154 - </React.Fragment>,
111 + function App(props) {
112 + return (
113 + <Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
114 + <HiddenText text="A" />
115 + <span>
116 + <AsyncText ms={1000} text={props.middleText} />
117 + </span>
118 + <span>
119 + <Text text="C" />
120 + </span>
121 + </Suspense>
122 );
123 + }
124
157 - // Update
158 - ReactNoop.render(<App middleText="B2" />);
159 - expect(Scheduler).toFlushAndYield(['Suspend! [B2]', 'C', 'Loading...']);
160 -
161 - // Time out the update
162 - jest.advanceTimersByTime(750);
163 - expect(Scheduler).toFlushAndYield([]);
164 - expect(ReactNoop).toMatchRenderedOutput(
165 - <React.Fragment>
166 - <span hidden={true}>A</span>
167 - <span hidden={true}>B</span>
168 - <span hidden={true}>C</span>
169 - Loading...
170 - </React.Fragment>,
171 - );
125 + // Initial mount
126 + ReactNoop.render(<App middleText="B" />);
127 +
128 + expect(Scheduler).toFlushAndYield(['A', 'Suspend! [B]', 'C', 'Loading...']);
129 + expect(ReactNoop).toMatchRenderedOutput(null);
130 +
131 + jest.advanceTimersByTime(1000);
132 + expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
133 +
134 + expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
135 +
136 + expect(ReactNoop).toMatchRenderedOutput(
137 + <React.Fragment>
138 + <span hidden={true}>A</span>
139 + <span>B</span>
140 + <span>C</span>
141 + </React.Fragment>,
142 + );
143 +
144 + // Update
145 + ReactNoop.render(<App middleText="B2" />);
146 + expect(Scheduler).toFlushAndYield(['Suspend! [B2]', 'C', 'Loading...']);
147 +
148 + // Time out the update
149 + jest.advanceTimersByTime(750);
150 + expect(Scheduler).toFlushAndYield([]);
151 + expect(ReactNoop).toMatchRenderedOutput(
152 + <React.Fragment>
153 + <span hidden={true}>A</span>
154 + <span hidden={true}>B</span>
155 + <span hidden={true}>C</span>
156 + Loading...
157 + </React.Fragment>,
158 + );
159 +
160 + // Resolve the promise
161 + jest.advanceTimersByTime(1000);
162 + expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
163 + expect(Scheduler).toFlushAndYield(['B2', 'C']);
164 +
165 + // Render the final update. A should still be hidden, because it was
166 + // given a `hidden` prop.
167 + expect(ReactNoop).toMatchRenderedOutput(
168 + <React.Fragment>
169 + <span hidden={true}>A</span>
170 + <span>B2</span>
171 + <span>C</span>
172 + </React.Fragment>,
173 + );
174 + });
175
173 - // Resolve the promise
174 - jest.advanceTimersByTime(1000);
175 - expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
176 - expect(Scheduler).toFlushAndYield(['B2', 'C']);
177 -
178 - // Render the final update. A should still be hidden, because it was
179 - // given a `hidden` prop.
180 - expect(ReactNoop).toMatchRenderedOutput(
181 - <React.Fragment>
182 - <span hidden={true}>A</span>
183 - <span>B2</span>
184 - <span>C</span>
185 - </React.Fragment>,
176 + it('times out text nodes', async () => {
177 + function App(props) {
178 + return (
179 + <Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
180 + <Text text="A" />
181 + <AsyncText ms={1000} text={props.middleText} />
182 + <Text text="C" />
183 + </Suspense>
184 );
187 - });
185 + }
186
189 - it('times out text nodes', async () => {
190 - function App(props) {
191 - return (
187 + // Initial mount
188 + ReactNoop.render(<App middleText="B" />);
189 +
190 + expect(Scheduler).toFlushAndYield(['A', 'Suspend! [B]', 'C', 'Loading...']);
191 +
192 + expect(ReactNoop).toMatchRenderedOutput(null);
193 +
194 + jest.advanceTimersByTime(1000);
195 + expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
196 + expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
197 + expect(ReactNoop).toMatchRenderedOutput('ABC');
198 +
199 + // Update
200 + ReactNoop.render(<App middleText="B2" />);
201 + expect(Scheduler).toFlushAndYield([
202 + 'A',
203 + 'Suspend! [B2]',
204 + 'C',
205 + 'Loading...',
206 + ]);
207 + // Time out the update
208 + jest.advanceTimersByTime(750);
209 + expect(Scheduler).toFlushAndYield([]);
210 + expect(ReactNoop).toMatchRenderedOutput('Loading...');
211 +
212 + // Resolve the promise
213 + jest.advanceTimersByTime(1000);
214 + expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
215 + expect(Scheduler).toFlushAndYield(['A', 'B2', 'C']);
216 +
217 + // Render the final update. A should still be hidden, because it was
218 + // given a `hidden` prop.
219 + expect(ReactNoop).toMatchRenderedOutput('AB2C');
220 + });
221 +
222 + it('preserves host context for text nodes', () => {
223 + function App(props) {
224 + return (
225 + // uppercase is a special type that causes React Noop to render child
226 + // text nodes as uppercase.
227 + <uppercase>
228 <Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
193 - <Text text="A" />
229 + <Text text="a" />
230 <AsyncText ms={1000} text={props.middleText} />
195 - <Text text="C" />
231 + <Text text="c" />
232 </Suspense>
197 - );
198 - }
233 + </uppercase>
234 + );
235 + }
236
200 - // Initial mount
201 - ReactNoop.render(<App middleText="B" />);
202 -
203 - expect(Scheduler).toFlushAndYield([
204 - 'A',
205 - 'Suspend! [B]',
206 - 'C',
207 - 'Loading...',
208 - ]);
209 -
210 - expect(ReactNoop).toMatchRenderedOutput(null);
211 -
212 - jest.advanceTimersByTime(1000);
213 - expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
214 - expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
215 - expect(ReactNoop).toMatchRenderedOutput('ABC');
216 -
217 - // Update
218 - ReactNoop.render(<App middleText="B2" />);
219 - expect(Scheduler).toFlushAndYield([
220 - 'A',
221 - 'Suspend! [B2]',
222 - 'C',
223 - 'Loading...',
224 - ]);
225 - // Time out the update
226 - jest.advanceTimersByTime(750);
227 - expect(Scheduler).toFlushAndYield([]);
228 - expect(ReactNoop).toMatchRenderedOutput('Loading...');
229 -
230 - // Resolve the promise
231 - jest.advanceTimersByTime(1000);
232 - expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
233 - expect(Scheduler).toFlushAndYield(['A', 'B2', 'C']);
234 -
235 - // Render the final update. A should still be hidden, because it was
236 - // given a `hidden` prop.
237 - expect(ReactNoop).toMatchRenderedOutput('AB2C');
238 - });
237 + // Initial mount
238 + ReactNoop.render(<App middleText="b" />);
239 +
240 + expect(Scheduler).toFlushAndYield(['a', 'Suspend! [b]', 'c', 'Loading...']);
241 +
242 + expect(ReactNoop).toMatchRenderedOutput(null);
243 +
244 + jest.advanceTimersByTime(1000);
245 + expect(Scheduler).toHaveYielded(['Promise resolved [b]']);
246 + expect(Scheduler).toFlushAndYield(['a', 'b', 'c']);
247 + expect(ReactNoop).toMatchRenderedOutput(<uppercase>ABC</uppercase>);
248 +
249 + // Update
250 + ReactNoop.render(<App middleText="b2" />);
251 + expect(Scheduler).toFlushAndYield([
252 + 'a',
253 + 'Suspend! [b2]',
254 + 'c',
255 + 'Loading...',
256 + ]);
257 + // Time out the update
258 + jest.advanceTimersByTime(750);
259 + expect(Scheduler).toFlushAndYield([]);
260 + expect(ReactNoop).toMatchRenderedOutput(<uppercase>LOADING...</uppercase>);
261 +
262 + // Resolve the promise
263 + jest.advanceTimersByTime(1000);
264 + expect(Scheduler).toHaveYielded(['Promise resolved [b2]']);
265 + expect(Scheduler).toFlushAndYield(['a', 'b2', 'c']);
266 +
267 + // Render the final update. A should still be hidden, because it was
268 + // given a `hidden` prop.
269 + expect(ReactNoop).toMatchRenderedOutput(<uppercase>AB2C</uppercase>);
270 + });
271 +
272 + describe('profiler durations', () => {
273 + let App;
274 + let onRender;
275
240 - it('preserves host context for text nodes', () => {
241 - function App(props) {
276 + beforeEach(() => {
277 + // Order of parameters: id, phase, actualDuration, treeBaseDuration
278 + onRender = jest.fn();
279 +
280 + const Fallback = () => {
281 + Scheduler.yieldValue('Fallback');
282 + Scheduler.advanceTime(10);
283 + return 'Loading...';
284 + };
285 +
286 + const Suspending = () => {
287 + Scheduler.yieldValue('Suspending');
288 + Scheduler.advanceTime(2);
289 + return <AsyncText ms={1000} text="Loaded" fakeRenderDuration={1} />;
290 + };
291 +
292 + App = ({shouldSuspend, text = 'Text', textRenderDuration = 5}) => {
293 + Scheduler.yieldValue('App');
294 return (
243 - // uppercase is a special type that causes React Noop to render child
244 - // text nodes as uppercase.
245 - <uppercase>
246 - <Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
247 - <Text text="a" />
248 - <AsyncText ms={1000} text={props.middleText} />
249 - <Text text="c" />
295 + <Profiler id="root" onRender={onRender}>
296 + <Suspense maxDuration={500} fallback={<Fallback />}>
297 + {shouldSuspend && <Suspending />}
298 + <Text fakeRenderDuration={textRenderDuration} text={text} />
299 </Suspense>
251 - </uppercase>
300 + </Profiler>
301 );
253 - }
254 -
255 - // Initial mount
256 - ReactNoop.render(<App middleText="b" />);
257 -
258 - expect(Scheduler).toFlushAndYield([
259 - 'a',
260 - 'Suspend! [b]',
261 - 'c',
262 - 'Loading...',
263 - ]);
264 -
265 - expect(ReactNoop).toMatchRenderedOutput(null);
266 -
267 - jest.advanceTimersByTime(1000);
268 - expect(Scheduler).toHaveYielded(['Promise resolved [b]']);
269 - expect(Scheduler).toFlushAndYield(['a', 'b', 'c']);
270 - expect(ReactNoop).toMatchRenderedOutput(<uppercase>ABC</uppercase>);
271 -
272 - // Update
273 - ReactNoop.render(<App middleText="b2" />);
274 - expect(Scheduler).toFlushAndYield([
275 - 'a',
276 - 'Suspend! [b2]',
277 - 'c',
278 - 'Loading...',
279 - ]);
280 - // Time out the update
281 - jest.advanceTimersByTime(750);
282 - expect(Scheduler).toFlushAndYield([]);
283 - expect(ReactNoop).toMatchRenderedOutput(
284 - <uppercase>LOADING...</uppercase>,
285 - );
286 -
287 - // Resolve the promise
288 - jest.advanceTimersByTime(1000);
289 - expect(Scheduler).toHaveYielded(['Promise resolved [b2]']);
290 - expect(Scheduler).toFlushAndYield(['a', 'b2', 'c']);
291 -
292 - // Render the final update. A should still be hidden, because it was
293 - // given a `hidden` prop.
294 - expect(ReactNoop).toMatchRenderedOutput(<uppercase>AB2C</uppercase>);
302 + };
303 });
304
297 - describe('profiler durations', () => {
298 - let App;
299 - let onRender;
300 -
301 - beforeEach(() => {
302 - // Order of parameters: id, phase, actualDuration, treeBaseDuration
303 - onRender = jest.fn();
304 -
305 - const Fallback = () => {
306 - Scheduler.yieldValue('Fallback');
307 - Scheduler.advanceTime(10);
308 - return 'Loading...';
309 - };
310 -
311 - const Suspending = () => {
312 - Scheduler.yieldValue('Suspending');
313 - Scheduler.advanceTime(2);
314 - return <AsyncText ms={1000} text="Loaded" fakeRenderDuration={1} />;
315 - };
316 -
317 - App = ({shouldSuspend, text = 'Text', textRenderDuration = 5}) => {
318 - Scheduler.yieldValue('App');
319 - return (
320 - <Profiler id="root" onRender={onRender}>
321 - <Suspense maxDuration={500} fallback={<Fallback />}>
322 - {shouldSuspend && <Suspending />}
323 - <Text fakeRenderDuration={textRenderDuration} text={text} />
324 - </Suspense>
325 - </Profiler>
326 - );
327 - };
305 + describe('when suspending during mount', () => {
306 + it('properly accounts for base durations when a suspended times out in a sync tree', () => {
307 + ReactNoop.renderLegacySyncRoot(<App shouldSuspend={true} />);
308 + expect(Scheduler).toHaveYielded([
309 + 'App',
310 + 'Suspending',
311 + 'Suspend! [Loaded]',
312 + 'Text',
313 + 'Fallback',
314 + ]);
315 + expect(ReactNoop).toMatchRenderedOutput('Loading...');
316 + expect(onRender).toHaveBeenCalledTimes(1);
317 +
318 + // Initial mount only shows the "Loading..." Fallback.
319 + // The treeBaseDuration then should be 10ms spent rendering Fallback,
320 + // but the actualDuration should also include the 8ms spent rendering the hidden tree.
321 + expect(onRender.mock.calls[0][2]).toBe(18);
322 + expect(onRender.mock.calls[0][3]).toBe(10);
323 +
324 + jest.advanceTimersByTime(1000);
325 +
326 + expect(Scheduler).toHaveYielded([
327 + 'Promise resolved [Loaded]',
328 + 'Loaded',
329 + ]);
330 + expect(ReactNoop).toMatchRenderedOutput('LoadedText');
331 + expect(onRender).toHaveBeenCalledTimes(2);
332 +
333 + // When the suspending data is resolved and our final UI is rendered,
334 + // the baseDuration should only include the 1ms re-rendering AsyncText,
335 + // but the treeBaseDuration should include the full 8ms spent in the tree.
336 + expect(onRender.mock.calls[1][2]).toBe(1);
337 + expect(onRender.mock.calls[1][3]).toBe(8);
338 });
339
330 - describe('when suspending during mount', () => {
331 - it('properly accounts for base durations when a suspended times out in a sync tree', () => {
332 - ReactNoop.renderLegacySyncRoot(<App shouldSuspend={true} />);
333 - expect(Scheduler).toHaveYielded([
334 - 'App',
335 - 'Suspending',
336 - 'Suspend! [Loaded]',
337 - 'Text',
338 - 'Fallback',
339 - ]);
340 - expect(ReactNoop).toMatchRenderedOutput('Loading...');
341 - expect(onRender).toHaveBeenCalledTimes(1);
342 -
343 - // Initial mount only shows the "Loading..." Fallback.
344 - // The treeBaseDuration then should be 10ms spent rendering Fallback,
345 - // but the actualDuration should also include the 8ms spent rendering the hidden tree.
346 - expect(onRender.mock.calls[0][2]).toBe(18);
347 - expect(onRender.mock.calls[0][3]).toBe(10);
348 -
349 - jest.advanceTimersByTime(1000);
350 -
351 - expect(Scheduler).toHaveYielded([
352 - 'Promise resolved [Loaded]',
353 - 'Loaded',
354 - ]);
355 - expect(ReactNoop).toMatchRenderedOutput('LoadedText');
356 - expect(onRender).toHaveBeenCalledTimes(2);
357 -
358 - // When the suspending data is resolved and our final UI is rendered,
359 - // the baseDuration should only include the 1ms re-rendering AsyncText,
360 - // but the treeBaseDuration should include the full 8ms spent in the tree.
361 - expect(onRender.mock.calls[1][2]).toBe(1);
362 - expect(onRender.mock.calls[1][3]).toBe(8);
363 - });
364 -
365 - it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
366 - ReactNoop.render(<App shouldSuspend={true} />);
367 -
368 - expect(Scheduler).toFlushAndYield([
369 - 'App',
370 - 'Suspending',
371 - 'Suspend! [Loaded]',
372 - 'Text',
373 - 'Fallback',
374 - ]);
375 - expect(ReactNoop).toMatchRenderedOutput(null);
376 -
377 - // Show the fallback UI.
378 - jest.advanceTimersByTime(750);
379 - expect(ReactNoop).toMatchRenderedOutput('Loading...');
380 - expect(onRender).toHaveBeenCalledTimes(1);
381 -
382 - // Initial mount only shows the "Loading..." Fallback.
383 - // The treeBaseDuration then should be 10ms spent rendering Fallback,
384 - // but the actualDuration should also include the 8ms spent rendering the hidden tree.
385 - expect(onRender.mock.calls[0][2]).toBe(18);
386 - expect(onRender.mock.calls[0][3]).toBe(10);
387 -
388 - // Resolve the pending promise.
389 - jest.advanceTimersByTime(250);
390 - expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
391 - expect(Scheduler).toFlushAndYield(['Suspending', 'Loaded', 'Text']);
392 - expect(ReactNoop).toMatchRenderedOutput('LoadedText');
393 - expect(onRender).toHaveBeenCalledTimes(2);
394 -
395 - // When the suspending data is resolved and our final UI is rendered,
396 - // both times should include the 8ms re-rendering Suspending and AsyncText.
397 - expect(onRender.mock.calls[1][2]).toBe(8);
398 - expect(onRender.mock.calls[1][3]).toBe(8);
399 - });
340 + it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
341 + ReactNoop.render(<App shouldSuspend={true} />);
342 +
343 + expect(Scheduler).toFlushAndYield([
344 + 'App',
345 + 'Suspending',
346 + 'Suspend! [Loaded]',
347 + 'Text',
348 + 'Fallback',
349 + ]);
350 + expect(ReactNoop).toMatchRenderedOutput(null);
351 +
352 + // Show the fallback UI.
353 + jest.advanceTimersByTime(750);
354 + expect(ReactNoop).toMatchRenderedOutput('Loading...');
355 + expect(onRender).toHaveBeenCalledTimes(1);
356 +
357 + // Initial mount only shows the "Loading..." Fallback.
358 + // The treeBaseDuration then should be 10ms spent rendering Fallback,
359 + // but the actualDuration should also include the 8ms spent rendering the hidden tree.
360 + expect(onRender.mock.calls[0][2]).toBe(18);
361 + expect(onRender.mock.calls[0][3]).toBe(10);
362 +
363 + // Resolve the pending promise.
364 + jest.advanceTimersByTime(250);
365 + expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
366 + expect(Scheduler).toFlushAndYield(['Suspending', 'Loaded', 'Text']);
367 + expect(ReactNoop).toMatchRenderedOutput('LoadedText');
368 + expect(onRender).toHaveBeenCalledTimes(2);
369 +
370 + // When the suspending data is resolved and our final UI is rendered,
371 + // both times should include the 8ms re-rendering Suspending and AsyncText.
372 + expect(onRender.mock.calls[1][2]).toBe(8);
373 + expect(onRender.mock.calls[1][3]).toBe(8);
374 + });
375 + });
376 +
377 + describe('when suspending during update', () => {
378 + it('properly accounts for base durations when a suspended times out in a sync tree', () => {
379 + ReactNoop.renderLegacySyncRoot(
380 + <App shouldSuspend={false} textRenderDuration={5} />,
381 + );
382 + expect(Scheduler).toHaveYielded(['App', 'Text']);
383 + expect(ReactNoop).toMatchRenderedOutput('Text');
384 + expect(onRender).toHaveBeenCalledTimes(1);
385 +
386 + // Initial mount only shows the "Text" text.
387 + // It should take 5ms to render.
388 + expect(onRender.mock.calls[0][2]).toBe(5);
389 + expect(onRender.mock.calls[0][3]).toBe(5);
390 +
391 + ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
392 + expect(Scheduler).toHaveYielded([
393 + 'App',
394 + 'Suspending',
395 + 'Suspend! [Loaded]',
396 + 'Text',
397 + 'Fallback',
398 + ]);
399 + expect(ReactNoop).toMatchRenderedOutput('Loading...');
400 + expect(onRender).toHaveBeenCalledTimes(2);
401 +
402 + // The suspense update should only show the "Loading..." Fallback.
403 + // Both durations should include 10ms spent rendering Fallback
404 + // plus the 8ms rendering the (hidden) components.
405 + expect(onRender.mock.calls[1][2]).toBe(18);
406 + expect(onRender.mock.calls[1][3]).toBe(18);
407 +
408 + ReactNoop.renderLegacySyncRoot(
409 + <App shouldSuspend={true} text="New" textRenderDuration={6} />,
410 + );
411 + expect(Scheduler).toHaveYielded([
412 + 'App',
413 + 'Suspending',
414 + 'Suspend! [Loaded]',
415 + 'New',
416 + 'Fallback',
417 + ]);
418 + expect(ReactNoop).toMatchRenderedOutput('Loading...');
419 + expect(onRender).toHaveBeenCalledTimes(3);
420 +
421 + // If we force another update while still timed out,
422 + // but this time the Text component took 1ms longer to render.
423 + // This should impact both actualDuration and treeBaseDuration.
424 + expect(onRender.mock.calls[2][2]).toBe(19);
425 + expect(onRender.mock.calls[2][3]).toBe(19);
426 +
427 + jest.advanceTimersByTime(1000);
428 +
429 + expect(Scheduler).toHaveYielded([
430 + 'Promise resolved [Loaded]',
431 + 'Loaded',
432 + ]);
433 + expect(ReactNoop).toMatchRenderedOutput('LoadedNew');
434 + expect(onRender).toHaveBeenCalledTimes(4);
435 +
436 + // When the suspending data is resolved and our final UI is rendered,
437 + // the baseDuration should only include the 1ms re-rendering AsyncText,
438 + // but the treeBaseDuration should include the full 9ms spent in the tree.
439 + expect(onRender.mock.calls[3][2]).toBe(1);
440 + expect(onRender.mock.calls[3][3]).toBe(9);
441 });
442
402 - describe('when suspending during update', () => {
403 - it('properly accounts for base durations when a suspended times out in a sync tree', () => {
404 - ReactNoop.renderLegacySyncRoot(
405 - <App shouldSuspend={false} textRenderDuration={5} />,
406 - );
407 - expect(Scheduler).toHaveYielded(['App', 'Text']);
408 - expect(ReactNoop).toMatchRenderedOutput('Text');
409 - expect(onRender).toHaveBeenCalledTimes(1);
410 -
411 - // Initial mount only shows the "Text" text.
412 - // It should take 5ms to render.
413 - expect(onRender.mock.calls[0][2]).toBe(5);
414 - expect(onRender.mock.calls[0][3]).toBe(5);
415 -
416 - ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
417 - expect(Scheduler).toHaveYielded([
418 - 'App',
419 - 'Suspending',
420 - 'Suspend! [Loaded]',
421 - 'Text',
422 - 'Fallback',
423 - ]);
424 - expect(ReactNoop).toMatchRenderedOutput('Loading...');
425 - expect(onRender).toHaveBeenCalledTimes(2);
426 -
427 - // The suspense update should only show the "Loading..." Fallback.
428 - // Both durations should include 10ms spent rendering Fallback
429 - // plus the 8ms rendering the (hidden) components.
430 - expect(onRender.mock.calls[1][2]).toBe(18);
431 - expect(onRender.mock.calls[1][3]).toBe(18);
432 -
433 - ReactNoop.renderLegacySyncRoot(
434 - <App shouldSuspend={true} text="New" textRenderDuration={6} />,
435 - );
436 - expect(Scheduler).toHaveYielded([
437 - 'App',
438 - 'Suspending',
439 - 'Suspend! [Loaded]',
440 - 'New',
441 - 'Fallback',
442 - ]);
443 - expect(ReactNoop).toMatchRenderedOutput('Loading...');
444 - expect(onRender).toHaveBeenCalledTimes(3);
445 -
446 - // If we force another update while still timed out,
447 - // but this time the Text component took 1ms longer to render.
448 - // This should impact both actualDuration and treeBaseDuration.
449 - expect(onRender.mock.calls[2][2]).toBe(19);
450 - expect(onRender.mock.calls[2][3]).toBe(19);
451 -
452 - jest.advanceTimersByTime(1000);
453 -
454 - expect(Scheduler).toHaveYielded([
455 - 'Promise resolved [Loaded]',
456 - 'Loaded',
457 - ]);
458 - expect(ReactNoop).toMatchRenderedOutput('LoadedNew');
459 - expect(onRender).toHaveBeenCalledTimes(4);
460 -
461 - // When the suspending data is resolved and our final UI is rendered,
462 - // the baseDuration should only include the 1ms re-rendering AsyncText,
463 - // but the treeBaseDuration should include the full 9ms spent in the tree.
464 - expect(onRender.mock.calls[3][2]).toBe(1);
465 - expect(onRender.mock.calls[3][3]).toBe(9);
466 - });
467 -
468 - it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
469 - ReactNoop.render(
470 - <App shouldSuspend={false} textRenderDuration={5} />,
471 - );
472 -
473 - expect(Scheduler).toFlushAndYield(['App', 'Text']);
474 - expect(ReactNoop).toMatchRenderedOutput('Text');
475 - expect(onRender).toHaveBeenCalledTimes(1);
476 -
477 - // Initial mount only shows the "Text" text.
478 - // It should take 5ms to render.
479 - expect(onRender.mock.calls[0][2]).toBe(5);
480 - expect(onRender.mock.calls[0][3]).toBe(5);
481 -
482 - ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
483 - expect(Scheduler).toFlushAndYield([
484 - 'App',
485 - 'Suspending',
486 - 'Suspend! [Loaded]',
487 - 'Text',
488 - 'Fallback',
489 - ]);
490 - expect(ReactNoop).toMatchRenderedOutput('Text');
491 -
492 - // Show the fallback UI.
493 - jest.advanceTimersByTime(750);
494 - expect(ReactNoop).toMatchRenderedOutput('Loading...');
495 - expect(onRender).toHaveBeenCalledTimes(2);
496 -
497 - // The suspense update should only show the "Loading..." Fallback.
498 - // The actual duration should include 10ms spent rendering Fallback,
499 - // plus the 8ms render all of the hidden, suspended subtree.
500 - // But the tree base duration should only include 10ms spent rendering Fallback,
501 - // plus the 5ms rendering the previously committed version of the hidden tree.
502 - expect(onRender.mock.calls[1][2]).toBe(18);
503 - expect(onRender.mock.calls[1][3]).toBe(15);
504 -
505 - // Update again while timed out.
506 - ReactNoop.render(
507 - <App shouldSuspend={true} text="New" textRenderDuration={6} />,
508 - );
509 - expect(Scheduler).toFlushAndYield([
510 - 'App',
511 - 'Suspending',
512 - 'Suspend! [Loaded]',
513 - 'New',
514 - 'Fallback',
515 - ]);
516 - expect(ReactNoop).toMatchRenderedOutput('Loading...');
517 - expect(onRender).toHaveBeenCalledTimes(2);
518 -
519 - // Resolve the pending promise.
520 - jest.advanceTimersByTime(250);
521 - expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
522 - expect(Scheduler).toFlushAndYield([
523 - 'App',
524 - 'Suspending',
525 - 'Loaded',
526 - 'New',
527 - ]);
528 - expect(onRender).toHaveBeenCalledTimes(3);
529 -
530 - // When the suspending data is resolved and our final UI is rendered,
531 - // both times should include the 6ms rendering Text,
532 - // the 2ms rendering Suspending, and the 1ms rendering AsyncText.
533 - expect(onRender.mock.calls[2][2]).toBe(9);
534 - expect(onRender.mock.calls[2][3]).toBe(9);
535 - });
443 + it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
444 + ReactNoop.render(<App shouldSuspend={false} textRenderDuration={5} />);
445 +
446 + expect(Scheduler).toFlushAndYield(['App', 'Text']);
447 + expect(ReactNoop).toMatchRenderedOutput('Text');
448 + expect(onRender).toHaveBeenCalledTimes(1);
449 +
450 + // Initial mount only shows the "Text" text.
451 + // It should take 5ms to render.
452 + expect(onRender.mock.calls[0][2]).toBe(5);
453 + expect(onRender.mock.calls[0][3]).toBe(5);
454 +
455 + ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
456 + expect(Scheduler).toFlushAndYield([
457 + 'App',
458 + 'Suspending',
459 + 'Suspend! [Loaded]',
460 + 'Text',
461 + 'Fallback',
462 + ]);
463 + expect(ReactNoop).toMatchRenderedOutput('Text');
464 +
465 + // Show the fallback UI.
466 + jest.advanceTimersByTime(750);
467 + expect(ReactNoop).toMatchRenderedOutput('Loading...');
468 + expect(onRender).toHaveBeenCalledTimes(2);
469 +
470 + // The suspense update should only show the "Loading..." Fallback.
471 + // The actual duration should include 10ms spent rendering Fallback,
472 + // plus the 8ms render all of the hidden, suspended subtree.
473 + // But the tree base duration should only include 10ms spent rendering Fallback,
474 + // plus the 5ms rendering the previously committed version of the hidden tree.
475 + expect(onRender.mock.calls[1][2]).toBe(18);
476 + expect(onRender.mock.calls[1][3]).toBe(15);
477 +
478 + // Update again while timed out.
479 + ReactNoop.render(
480 + <App shouldSuspend={true} text="New" textRenderDuration={6} />,
481 + );
482 + expect(Scheduler).toFlushAndYield([
483 + 'App',
484 + 'Suspending',
485 + 'Suspend! [Loaded]',
486 + 'New',
487 + 'Fallback',
488 + ]);
489 + expect(ReactNoop).toMatchRenderedOutput('Loading...');
490 + expect(onRender).toHaveBeenCalledTimes(2);
491 +
492 + // Resolve the pending promise.
493 + jest.advanceTimersByTime(250);
494 + expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
495 + expect(Scheduler).toFlushAndYield([
496 + 'App',
497 + 'Suspending',
498 + 'Loaded',
499 + 'New',
500 + ]);
501 + expect(onRender).toHaveBeenCalledTimes(3);
502 +
503 + // When the suspending data is resolved and our final UI is rendered,
504 + // both times should include the 6ms rendering Text,
505 + // the 2ms rendering Suspending, and the 1ms rendering AsyncText.
506 + expect(onRender.mock.calls[2][2]).toBe(9);
507 + expect(onRender.mock.calls[2][3]).toBe(9);
508 });
509 });
510 });
539 -}
511 +});
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+33 -30
@@ -1387,44 +1387,47 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1387 expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1388 });
1389
1390 - it('toggles visibility during the mutation phase', async () => {
1391 - const {useRef, useLayoutEffect} = React;
1390 + if (!global.__PERSISTENT__) {
1391 + // TODO: Write persistent version of this test
1392 + it('toggles visibility during the mutation phase', async () => {
1393 + const {useRef, useLayoutEffect} = React;
1394
1393 - function Parent() {
1394 - const child = useRef(null);
1395 + function Parent() {
1396 + const child = useRef(null);
1397
1396 - useLayoutEffect(() => {
1397 - Scheduler.yieldValue('Child is hidden: ' + child.current.hidden);
1398 - });
1398 + useLayoutEffect(() => {
1399 + Scheduler.yieldValue('Child is hidden: ' + child.current.hidden);
1400 + });
1401
1400 - return (
1401 - <span ref={child} hidden={false}>
1402 - <AsyncText ms={1000} text="Hi" />
1403 - </span>
1404 - );
1405 - }
1402 + return (
1403 + <span ref={child} hidden={false}>
1404 + <AsyncText ms={1000} text="Hi" />
1405 + </span>
1406 + );
1407 + }
1408
1407 - function App(props) {
1408 - return (
1409 - <Suspense fallback={<Text text="Loading..." />}>
1410 - <Parent />
1411 - </Suspense>
1412 - );
1413 - }
1409 + function App(props) {
1410 + return (
1411 + <Suspense fallback={<Text text="Loading..." />}>
1412 + <Parent />
1413 + </Suspense>
1414 + );
1415 + }
1416
1415 - ReactNoop.renderLegacySyncRoot(<App middleText="B" />);
1417 + ReactNoop.renderLegacySyncRoot(<App middleText="B" />);
1418
1417 - expect(Scheduler).toHaveYielded([
1418 - 'Suspend! [Hi]',
1419 - 'Loading...',
1420 - // The child should have already been hidden
1421 - 'Child is hidden: true',
1422 - ]);
1419 + expect(Scheduler).toHaveYielded([
1420 + 'Suspend! [Hi]',
1421 + 'Loading...',
1422 + // The child should have already been hidden
1423 + 'Child is hidden: true',
1424 + ]);
1425
1424 - await advanceTimers(1000);
1426 + await advanceTimers(1000);
1427
1426 - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']);
1427 - });
1428 + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']);
1429 + });
1430 + }
1431 });
1432
1433 it('does not call lifecycles of a suspended component', async () => {
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
-3
@@ -91,10 +91,7 @@ export const finalizeContainerChildren =
91 $$$hostConfig.finalizeContainerChildren;
92 export const replaceContainerChildren = $$$hostConfig.replaceContainerChildren;
93 export const cloneHiddenInstance = $$$hostConfig.cloneHiddenInstance;
94 -export const cloneUnhiddenInstance = $$$hostConfig.cloneUnhiddenInstance;
94 export const cloneHiddenTextInstance = $$$hostConfig.cloneHiddenTextInstance;
96 -export const cloneUnhiddenTextInstance =
97 - $$$hostConfig.cloneUnhiddenTextInstance;
95
96 // -------------------
97 // Hydration
packages/shared/HostConfigWithNoPersistence.js
-2
@@ -29,6 +29,4 @@ export const appendChildToContainerChildSet = shim;
29 export const finalizeContainerChildren = shim;
30 export const replaceContainerChildren = shim;
31 export const cloneHiddenInstance = shim;
32 -export const cloneUnhiddenInstance = shim;
32 export const cloneHiddenTextInstance = shim;
34 -export const cloneUnhiddenTextInstance = shim;
scripts/circleci/test_entry_point.sh
+1
@@ -11,6 +11,7 @@ if [ $((0 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
11 COMMANDS_TO_RUN+=('node ./scripts/tasks/flow-ci')
12 COMMANDS_TO_RUN+=('node ./scripts/tasks/eslint')
13 COMMANDS_TO_RUN+=('yarn test --maxWorkers=2')
14 + COMMANDS_TO_RUN+=('yarn test-persistent --maxWorkers=2')
15 COMMANDS_TO_RUN+=('./scripts/circleci/check_license.sh')
16 COMMANDS_TO_RUN+=('./scripts/circleci/check_modules.sh')
17 COMMANDS_TO_RUN+=('./scripts/circleci/test_print_warnings.sh')
scripts/jest/config.source-persistent.js new
+18
@@ -0,0 +1,18 @@
1 +'use strict';
2 +
3 +const baseConfig = require('./config.base');
4 +
5 +module.exports = Object.assign({}, baseConfig, {
6 + modulePathIgnorePatterns: [
7 + 'ReactIncrementalPerf',
8 + 'ReactIncrementalUpdatesMinimalism',
9 + 'ReactIncrementalTriangle',
10 + 'ReactIncrementalReflection',
11 + 'forwardRef',
12 + ],
13 + setupFiles: [
14 + ...baseConfig.setupFiles,
15 + require.resolve('./setupTests.persistent.js'),
16 + require.resolve('./setupHostConfigs.js'),
17 + ],
18 +});
scripts/jest/setupTests.persistent.js new
+7
@@ -0,0 +1,7 @@
1 +'use strict';
2 +
3 +jest.mock('react-noop-renderer', () =>
4 + require.requireActual('react-noop-renderer/persistent')
5 +);
6 +
7 +global.__PERSISTENT__ = true;