@samitouri / QOS-React / commits / 3f4852fa5f

Run Placeholder tests in persistent mode, too (#15013)

* Convert ReactSuspensePlaceholder tests to use noop Instead of the test renderer, since test renderer does not support running in persistent mode. * Run Placeholder tests in persistent mode, too * Fix Flow and lint * Hidden text instances should have correct host context Adds a test for a subtle edge case that only occurs in persistent mode. * createHiddenTextInstance -> cloneHiddenTextInstance This sidesteps the problem where createHiddenTextInstance needs access to the host context.

Andrew Clark committed Mar 8, 2019 at 18:53 UTC 3f4852fa5f932e5e815ef1be39c72ac8be4687d4
7 files changed +328 -167
packages/react-native-renderer/src/ReactFabricHostConfig.js
+10 -3
@@ -406,10 +406,17 @@ export function cloneUnhiddenInstance(
406 };
407 }
408
409 -export function createHiddenTextInstance(
409 +export function cloneHiddenTextInstance(
410 + instance: Instance,
411 + text: string,
412 + internalInstanceHandle: Object,
413 +): TextInstance {
414 + throw new Error('Not yet implemented.');
415 +}
416 +
417 +export function cloneUnhiddenTextInstance(
418 + instance: Instance,
419 text: string,
411 - rootContainerInstance: Container,
412 - hostContext: HostContext,
420 internalInstanceHandle: Object,
421 ): TextInstance {
422 throw new Error('Not yet implemented.');
packages/react-noop-renderer/src/createReactNoop.js
+96 -16
@@ -41,10 +41,18 @@ type Instance = {|
41 text: string | null,
42 prop: any,
43 hidden: boolean,
44 + context: HostContext,
45 |};
45 -type TextInstance = {|text: string, id: number, hidden: boolean|};
46 +type TextInstance = {|
47 + text: string,
48 + id: number,
49 + hidden: boolean,
50 + context: HostContext,
51 +|};
52 +type HostContext = Object;
53
54 const NO_CONTEXT = {};
55 +const UPPERCASE_CONTEXT = {};
56 const UPDATE_SIGNAL = {};
57 if (__DEV__) {
58 Object.freeze(NO_CONTEXT);
@@ -190,10 +198,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
198 type: type,
199 children: keepChildren ? instance.children : [],
200 text: shouldSetTextContent(type, newProps)
193 - ? (newProps.children: any) + ''
201 + ? computeText((newProps.children: any) + '', instance.context)
202 : null,
203 prop: newProps.prop,
204 hidden: newProps.hidden === true,
205 + context: instance.context,
206 };
207 Object.defineProperty(clone, 'id', {
208 value: clone.id,
@@ -203,6 +212,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
212 value: clone.text,
213 enumerable: false,
214 });
215 + Object.defineProperty(clone, 'context', {
216 + value: clone.context,
217 + enumerable: false,
218 + });
219 hostCloneCounter++;
220 return clone;
221 }
@@ -216,12 +229,23 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
229 );
230 }
231
232 + function computeText(rawText, hostContext) {
233 + return hostContext === UPPERCASE_CONTEXT ? rawText.toUpperCase() : rawText;
234 + }
235 +
236 const sharedHostConfig = {
237 getRootHostContext() {
238 return NO_CONTEXT;
239 },
240
224 - getChildHostContext() {
241 + getChildHostContext(
242 + parentHostContext: HostContext,
243 + type: string,
244 + rootcontainerInstance: Container,
245 + ) {
246 + if (type === 'uppercase') {
247 + return UPPERCASE_CONTEXT;
248 + }
249 return NO_CONTEXT;
250 },
251
@@ -229,7 +253,12 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
253 return instance;
254 },
255
232 - createInstance(type: string, props: Props): Instance {
256 + createInstance(
257 + type: string,
258 + props: Props,
259 + rootContainerInstance: Container,
260 + hostContext: HostContext,
261 + ): Instance {
262 if (type === 'errorInCompletePhase') {
263 throw new Error('Error in host config.');
264 }
@@ -238,10 +267,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
267 type: type,
268 children: [],
269 text: shouldSetTextContent(type, props)
241 - ? (props.children: any) + ''
270 + ? computeText((props.children: any) + '', hostContext)
271 : null,
272 prop: props.prop,
273 hidden: props.hidden === true,
274 + context: hostContext,
275 };
276 // Hide from unit tests
277 Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false});
@@ -249,6 +279,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
279 value: inst.text,
280 enumerable: false,
281 });
282 + Object.defineProperty(inst, 'context', {
283 + value: inst.context,
284 + enumerable: false,
285 + });
286 return inst;
287 },
288
@@ -298,9 +332,21 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
332 hostContext: Object,
333 internalInstanceHandle: Object,
334 ): TextInstance {
301 - const inst = {text: text, id: instanceCounter++, hidden: false};
335 + if (hostContext === UPPERCASE_CONTEXT) {
336 + text = text.toUpperCase();
337 + }
338 + const inst = {
339 + text: text,
340 + id: instanceCounter++,
341 + hidden: false,
342 + context: hostContext,
343 + };
344 // Hide from unit tests
345 Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false});
346 + Object.defineProperty(inst, 'context', {
347 + value: inst.context,
348 + enumerable: false,
349 + });
350 return inst;
351 },
352
@@ -343,7 +389,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
389 instance.prop = newProps.prop;
390 instance.hidden = newProps.hidden === true;
391 if (shouldSetTextContent(type, newProps)) {
346 - instance.text = (newProps.children: any) + '';
392 + instance.text = computeText(
393 + (newProps.children: any) + '',
394 + instance.context,
395 + );
396 }
397 },
398
@@ -353,7 +402,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
402 newText: string,
403 ): void {
404 hostUpdateCounter++;
356 - textInstance.text = newText;
405 + textInstance.text = computeText(newText, textInstance.context);
406 },
407
408 appendChild,
@@ -453,23 +502,54 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
502 true,
503 null,
504 );
456 - clone.hidden = props.hidden;
505 + clone.hidden = props.hidden === true;
506 + return clone;
507 + },
508 +
509 + cloneHiddenTextInstance(
510 + instance: TextInstance,
511 + text: string,
512 + internalInstanceHandle: Object,
513 + ): TextInstance {
514 + const clone = {
515 + text: instance.text,
516 + id: instanceCounter++,
517 + hidden: true,
518 + context: instance.context,
519 + };
520 + // Hide from unit tests
521 + Object.defineProperty(clone, 'id', {
522 + value: clone.id,
523 + enumerable: false,
524 + });
525 + Object.defineProperty(clone, 'context', {
526 + value: clone.context,
527 + enumerable: false,
528 + });
529 return clone;
530 },
531
460 - createHiddenTextInstance(
532 + cloneUnhiddenTextInstance(
533 + instance: TextInstance,
534 text: string,
462 - rootContainerInstance: Container,
463 - hostContext: Object,
535 internalInstanceHandle: Object,
536 ): TextInstance {
466 - const inst = {text: text, id: instanceCounter++, hidden: true};
537 + const clone = {
538 + text: instance.text,
539 + id: instanceCounter++,
540 + hidden: false,
541 + context: instance.context,
542 + };
543 // Hide from unit tests
468 - Object.defineProperty(inst, 'id', {
469 - value: inst.id,
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 });
472 - return inst;
552 + return clone;
553 },
554 };
555
packages/react-reconciler/src/ReactFiberCommitWork.js
+54 -44
@@ -1131,6 +1131,13 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1131 commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
1132 return;
1133 }
1134 + case Profiler: {
1135 + return;
1136 + }
1137 + case SuspenseComponent: {
1138 + commitSuspenseComponent(finishedWork);
1139 + return;
1140 + }
1141 }
1142
1143 commitContainer(finishedWork);
@@ -1199,50 +1206,7 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1206 return;
1207 }
1208 case SuspenseComponent: {
1202 - let newState: SuspenseState | null = finishedWork.memoizedState;
1203 -
1204 - let newDidTimeout;
1205 - let primaryChildParent = finishedWork;
1206 - if (newState === null) {
1207 - newDidTimeout = false;
1208 - } else {
1209 - newDidTimeout = true;
1210 - primaryChildParent = finishedWork.child;
1211 - if (newState.timedOutAt === NoWork) {
1212 - // If the children had not already timed out, record the time.
1213 - // This is used to compute the elapsed time during subsequent
1214 - // attempts to render the children.
1215 - newState.timedOutAt = requestCurrentTime();
1216 - }
1217 - }
1218 -
1219 - if (primaryChildParent !== null) {
1220 - hideOrUnhideAllChildren(primaryChildParent, newDidTimeout);
1221 - }
1222 -
1223 - // If this boundary just timed out, then it will have a set of thenables.
1224 - // For each thenable, attach a listener so that when it resolves, React
1225 - // attempts to re-render the boundary in the primary (pre-timeout) state.
1226 - const thenables: Set<Thenable> | null = (finishedWork.updateQueue: any);
1227 - if (thenables !== null) {
1228 - finishedWork.updateQueue = null;
1229 - let retryCache = finishedWork.stateNode;
1230 - if (retryCache === null) {
1231 - retryCache = finishedWork.stateNode = new PossiblyWeakSet();
1232 - }
1233 - thenables.forEach(thenable => {
1234 - // Memoize using the boundary fiber to prevent redundant listeners.
1235 - let retry = resolveRetryThenable.bind(null, finishedWork, thenable);
1236 - if (enableSchedulerTracing) {
1237 - retry = Schedule_tracing_wrap(retry);
1238 - }
1239 - if (!retryCache.has(thenable)) {
1240 - retryCache.add(thenable);
1241 - thenable.then(retry, retry);
1242 - }
1243 - });
1244 - }
1245 -
1209 + commitSuspenseComponent(finishedWork);
1210 return;
1211 }
1212 case IncompleteClassComponent: {
@@ -1258,6 +1222,52 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1222 }
1223 }
1224
1225 +function commitSuspenseComponent(finishedWork: Fiber) {
1226 + let newState: SuspenseState | null = finishedWork.memoizedState;
1227 +
1228 + let newDidTimeout;
1229 + let primaryChildParent = finishedWork;
1230 + if (newState === null) {
1231 + newDidTimeout = false;
1232 + } else {
1233 + newDidTimeout = true;
1234 + primaryChildParent = finishedWork.child;
1235 + if (newState.timedOutAt === NoWork) {
1236 + // If the children had not already timed out, record the time.
1237 + // This is used to compute the elapsed time during subsequent
1238 + // attempts to render the children.
1239 + newState.timedOutAt = requestCurrentTime();
1240 + }
1241 + }
1242 +
1243 + if (supportsMutation && primaryChildParent !== null) {
1244 + hideOrUnhideAllChildren(primaryChildParent, newDidTimeout);
1245 + }
1246 +
1247 + // If this boundary just timed out, then it will have a set of thenables.
1248 + // For each thenable, attach a listener so that when it resolves, React
1249 + // attempts to re-render the boundary in the primary (pre-timeout) state.
1250 + const thenables: Set<Thenable> | null = (finishedWork.updateQueue: any);
1251 + if (thenables !== null) {
1252 + finishedWork.updateQueue = null;
1253 + let retryCache = finishedWork.stateNode;
1254 + if (retryCache === null) {
1255 + retryCache = finishedWork.stateNode = new PossiblyWeakSet();
1256 + }
1257 + thenables.forEach(thenable => {
1258 + // Memoize using the boundary fiber to prevent redundant listeners.
1259 + let retry = resolveRetryThenable.bind(null, finishedWork, thenable);
1260 + if (enableSchedulerTracing) {
1261 + retry = Schedule_tracing_wrap(retry);
1262 + }
1263 + if (!retryCache.has(thenable)) {
1264 + retryCache.add(thenable);
1265 + thenable.then(retry, retry);
1266 + }
1267 + });
1268 + }
1269 +}
1270 +
1271 function commitResetTextContent(current: Fiber) {
1272 if (!supportsMutation) {
1273 return;
packages/react-reconciler/src/ReactFiberCompleteWork.js
+31 -52
@@ -17,7 +17,6 @@ import type {
17 Container,
18 ChildSet,
19 } from './ReactFiberHostConfig';
20 -import type {SuspenseState} from './ReactFiberSuspenseComponent';
20
21 import {
22 IndeterminateComponent,
@@ -53,7 +52,6 @@ import invariant from 'shared/invariant';
52 import {
53 createInstance,
54 createTextInstance,
56 - createHiddenTextInstance,
55 appendInitialChild,
56 finalizeInitialChildren,
57 prepareUpdate,
@@ -62,6 +60,8 @@ import {
60 cloneInstance,
61 cloneHiddenInstance,
62 cloneUnhiddenInstance,
63 + cloneHiddenTextInstance,
64 + cloneUnhiddenTextInstance,
65 createContainerChildSet,
66 appendChildToContainerChildSet,
67 finalizeContainerChildren,
@@ -228,22 +228,10 @@ if (supportsMutation) {
228 let instance = node.stateNode;
229 if (needsVisibilityToggle) {
230 const text = node.memoizedProps;
231 - const rootContainerInstance = getRootHostContainer();
232 - const currentHostContext = getHostContext();
231 if (isHidden) {
234 - instance = createHiddenTextInstance(
235 - text,
236 - rootContainerInstance,
237 - currentHostContext,
238 - workInProgress,
239 - );
232 + instance = cloneHiddenTextInstance(instance, text, node);
233 } else {
241 - instance = createTextInstance(
242 - text,
243 - rootContainerInstance,
244 - currentHostContext,
245 - workInProgress,
246 - );
234 + instance = cloneUnhiddenTextInstance(instance, text, node);
235 }
236 node.stateNode = instance;
237 }
@@ -253,20 +241,19 @@ if (supportsMutation) {
241 // down its children. Instead, we'll get insertions from each child in
242 // the portal directly.
243 } else if (node.tag === SuspenseComponent) {
256 - const current = node.alternate;
257 - if (current !== null) {
258 - const oldState: SuspenseState = current.memoizedState;
259 - const newState: SuspenseState = node.memoizedState;
260 - const oldIsHidden = oldState !== null;
261 - const newIsHidden = newState !== null;
262 - if (oldIsHidden !== newIsHidden) {
263 - // The placeholder either just timed out or switched back to the normal
264 - // children after having previously timed out. Toggle the visibility of
265 - // the direct host children.
266 - const primaryChildParent = newIsHidden ? node.child : node;
244 + if ((node.effectTag & Update) !== NoEffect) {
245 + // Need to toggle the visibility of the primary children.
246 + const newIsHidden = node.memoizedState !== null;
247 + if (newIsHidden) {
248 + const primaryChildParent = node.child;
249 if (primaryChildParent !== null) {
250 appendAllChildren(parent, primaryChildParent, true, newIsHidden);
251 + node = primaryChildParent.sibling;
252 + continue;
253 }
254 + } else {
255 + const primaryChildParent = node;
256 + appendAllChildren(parent, primaryChildParent, true, newIsHidden);
257 // eslint-disable-next-line no-labels
258 break branches;
259 }
@@ -331,22 +318,10 @@ if (supportsMutation) {
318 let instance = node.stateNode;
319 if (needsVisibilityToggle) {
320 const text = node.memoizedProps;
334 - const rootContainerInstance = getRootHostContainer();
335 - const currentHostContext = getHostContext();
321 if (isHidden) {
337 - instance = createHiddenTextInstance(
338 - text,
339 - rootContainerInstance,
340 - currentHostContext,
341 - workInProgress,
342 - );
322 + instance = cloneHiddenTextInstance(instance, text, node);
323 } else {
344 - instance = createTextInstance(
345 - text,
346 - rootContainerInstance,
347 - currentHostContext,
348 - workInProgress,
349 - );
324 + instance = cloneUnhiddenTextInstance(instance, text, node);
325 }
326 node.stateNode = instance;
327 }
@@ -356,17 +331,11 @@ if (supportsMutation) {
331 // down its children. Instead, we'll get insertions from each child in
332 // the portal directly.
333 } else if (node.tag === SuspenseComponent) {
359 - const current = node.alternate;
360 - if (current !== null) {
361 - const oldState: SuspenseState = current.memoizedState;
362 - const newState: SuspenseState = node.memoizedState;
363 - const oldIsHidden = oldState !== null;
364 - const newIsHidden = newState !== null;
365 - if (oldIsHidden !== newIsHidden) {
366 - // The placeholder either just timed out or switched back to the normal
367 - // children after having previously timed out. Toggle the visibility of
368 - // the direct host children.
369 - const primaryChildParent = newIsHidden ? node.child : node;
334 + if ((node.effectTag & Update) !== NoEffect) {
335 + // Need to toggle the visibility of the primary children.
336 + const newIsHidden = node.memoizedState !== null;
337 + if (newIsHidden) {
338 + const primaryChildParent = node.child;
339 if (primaryChildParent !== null) {
340 appendAllChildrenToContainer(
341 containerChildSet,
@@ -374,7 +343,17 @@ if (supportsMutation) {
343 true,
344 newIsHidden,
345 );
346 + node = primaryChildParent.sibling;
347 + continue;
348 }
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;
359 }
packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
+132 -50
@@ -8,12 +8,9 @@
8 * @jest-environment node
9 */
10
11 -// TODO: This does nothing since it was migrated from noop renderer to test
12 -// renderer! Switch back to noop renderer, or add persistent mode to test
13 -// renderer, or merge the two renderers into one somehow.
14 -// runPlaceholderTests('ReactSuspensePlaceholder (mutation)', () =>
15 -// require('react-noop-renderer'),
16 -// );
11 +runPlaceholderTests('ReactSuspensePlaceholder (mutation)', () =>
12 + require('react-noop-renderer'),
13 +);
14 runPlaceholderTests('ReactSuspensePlaceholder (persistence)', () =>
15 require('react-noop-renderer/persistent'),
16 );
@@ -21,7 +18,7 @@ runPlaceholderTests('ReactSuspensePlaceholder (persistence)', () =>
18 function runPlaceholderTests(suiteLabel, loadReactNoop) {
19 let Profiler;
20 let React;
24 - let ReactTestRenderer;
21 + let ReactNoop;
22 let Scheduler;
23 let ReactFeatureFlags;
24 let ReactCache;
@@ -38,7 +35,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
35 ReactFeatureFlags.enableProfilerTimer = true;
36 ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
37 React = require('react');
41 - ReactTestRenderer = require('react-test-renderer');
38 + ReactNoop = loadReactNoop();
39 Scheduler = require('scheduler');
40 ReactCache = require('react-cache');
41
@@ -134,9 +131,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
131 }
132
133 // Initial mount
137 - const root = ReactTestRenderer.create(<App middleText="B" />, {
138 - unstable_isConcurrent: true,
139 - });
134 + ReactNoop.render(<App middleText="B" />);
135
136 expect(Scheduler).toFlushAndYield([
137 'A',
@@ -144,14 +139,14 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
139 'C',
140 'Loading...',
141 ]);
147 - expect(root).toMatchRenderedOutput(null);
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
154 - expect(root).toMatchRenderedOutput(
149 + expect(ReactNoop).toMatchRenderedOutput(
150 <React.Fragment>
151 <span hidden={true}>A</span>
152 <span>B</span>
@@ -160,13 +155,20 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
155 );
156
157 // Update
163 - root.update(<App middleText="B2" />);
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([]);
169 - expect(root).toMatchRenderedOutput('Loading...');
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 + );
172
173 // Resolve the promise
174 jest.advanceTimersByTime(1000);
@@ -175,7 +177,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
177
178 // Render the final update. A should still be hidden, because it was
179 // given a `hidden` prop.
178 - expect(root).toMatchRenderedOutput(
180 + expect(ReactNoop).toMatchRenderedOutput(
181 <React.Fragment>
182 <span hidden={true}>A</span>
183 <span>B2</span>
@@ -196,9 +198,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
198 }
199
200 // Initial mount
199 - const root = ReactTestRenderer.create(<App middleText="B" />, {
200 - unstable_isConcurrent: true,
201 - });
201 + ReactNoop.render(<App middleText="B" />);
202
203 expect(Scheduler).toFlushAndYield([
204 'A',
@@ -207,15 +207,15 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
207 'Loading...',
208 ]);
209
210 - expect(root).toMatchRenderedOutput(null);
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(root).toMatchRenderedOutput('ABC');
215 + expect(ReactNoop).toMatchRenderedOutput('ABC');
216
217 // Update
218 - root.update(<App middleText="B2" />);
218 + ReactNoop.render(<App middleText="B2" />);
219 expect(Scheduler).toFlushAndYield([
220 'A',
221 'Suspend! [B2]',
@@ -225,7 +225,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
225 // Time out the update
226 jest.advanceTimersByTime(750);
227 expect(Scheduler).toFlushAndYield([]);
228 - expect(root).toMatchRenderedOutput('Loading...');
228 + expect(ReactNoop).toMatchRenderedOutput('Loading...');
229
230 // Resolve the promise
231 jest.advanceTimersByTime(1000);
@@ -234,7 +234,64 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
234
235 // Render the final update. A should still be hidden, because it was
236 // given a `hidden` prop.
237 - expect(root).toMatchRenderedOutput('AB2C');
237 + expect(ReactNoop).toMatchRenderedOutput('AB2C');
238 + });
239 +
240 + it('preserves host context for text nodes', () => {
241 + function App(props) {
242 + 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" />
250 + </Suspense>
251 + </uppercase>
252 + );
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>);
295 });
296
297 describe('profiler durations', () => {
@@ -272,8 +329,15 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
329
330 describe('when suspending during mount', () => {
331 it('properly accounts for base durations when a suspended times out in a sync tree', () => {
275 - const root = ReactTestRenderer.create(<App shouldSuspend={true} />);
276 - expect(root.toJSON()).toEqual('Loading...');
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.
@@ -284,7 +348,11 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
348
349 jest.advanceTimersByTime(1000);
350
287 - expect(root.toJSON()).toEqual(['Loaded', 'Text']);
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,
@@ -295,9 +363,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
363 });
364
365 it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
298 - const root = ReactTestRenderer.create(<App shouldSuspend={true} />, {
299 - unstable_isConcurrent: true,
300 - });
366 + ReactNoop.render(<App shouldSuspend={true} />);
367
368 expect(Scheduler).toFlushAndYield([
369 'App',
@@ -306,11 +372,11 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
372 'Text',
373 'Fallback',
374 ]);
309 - expect(root).toMatchRenderedOutput(null);
375 + expect(ReactNoop).toMatchRenderedOutput(null);
376
377 // Show the fallback UI.
378 jest.advanceTimersByTime(750);
313 - expect(root).toMatchRenderedOutput('Loading...');
379 + expect(ReactNoop).toMatchRenderedOutput('Loading...');
380 expect(onRender).toHaveBeenCalledTimes(1);
381
382 // Initial mount only shows the "Loading..." Fallback.
@@ -323,7 +389,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
389 jest.advanceTimersByTime(250);
390 expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
391 expect(Scheduler).toFlushAndYield(['Suspending', 'Loaded', 'Text']);
326 - expect(root).toMatchRenderedOutput('LoadedText');
392 + expect(ReactNoop).toMatchRenderedOutput('LoadedText');
393 expect(onRender).toHaveBeenCalledTimes(2);
394
395 // When the suspending data is resolved and our final UI is rendered,
@@ -335,10 +401,11 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
401
402 describe('when suspending during update', () => {
403 it('properly accounts for base durations when a suspended times out in a sync tree', () => {
338 - const root = ReactTestRenderer.create(
404 + ReactNoop.renderLegacySyncRoot(
405 <App shouldSuspend={false} textRenderDuration={5} />,
406 );
341 - expect(root.toJSON()).toEqual('Text');
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.
@@ -346,8 +413,15 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
413 expect(onRender.mock.calls[0][2]).toBe(5);
414 expect(onRender.mock.calls[0][3]).toBe(5);
415
349 - root.update(<App shouldSuspend={true} textRenderDuration={5} />);
350 - expect(root.toJSON()).toEqual('Loading...');
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.
@@ -356,10 +430,17 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
430 expect(onRender.mock.calls[1][2]).toBe(18);
431 expect(onRender.mock.calls[1][3]).toBe(18);
432
359 - root.update(
433 + ReactNoop.renderLegacySyncRoot(
434 <App shouldSuspend={true} text="New" textRenderDuration={6} />,
435 );
362 - expect(root.toJSON()).toEqual('Loading...');
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,
@@ -370,7 +451,11 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
451
452 jest.advanceTimersByTime(1000);
453
373 - expect(root.toJSON()).toEqual(['Loaded', 'New']);
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,
@@ -381,15 +466,12 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
466 });
467
468 it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
384 - const root = ReactTestRenderer.create(
469 + ReactNoop.render(
470 <App shouldSuspend={false} textRenderDuration={5} />,
386 - {
387 - unstable_isConcurrent: true,
388 - },
471 );
472
473 expect(Scheduler).toFlushAndYield(['App', 'Text']);
392 - expect(root).toMatchRenderedOutput('Text');
474 + expect(ReactNoop).toMatchRenderedOutput('Text');
475 expect(onRender).toHaveBeenCalledTimes(1);
476
477 // Initial mount only shows the "Text" text.
@@ -397,7 +479,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
479 expect(onRender.mock.calls[0][2]).toBe(5);
480 expect(onRender.mock.calls[0][3]).toBe(5);
481
400 - root.update(<App shouldSuspend={true} textRenderDuration={5} />);
482 + ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
483 expect(Scheduler).toFlushAndYield([
484 'App',
485 'Suspending',
@@ -405,11 +487,11 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
487 'Text',
488 'Fallback',
489 ]);
408 - expect(root).toMatchRenderedOutput('Text');
490 + expect(ReactNoop).toMatchRenderedOutput('Text');
491
492 // Show the fallback UI.
493 jest.advanceTimersByTime(750);
412 - expect(root).toMatchRenderedOutput('Loading...');
494 + expect(ReactNoop).toMatchRenderedOutput('Loading...');
495 expect(onRender).toHaveBeenCalledTimes(2);
496
497 // The suspense update should only show the "Loading..." Fallback.
@@ -421,7 +503,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
503 expect(onRender.mock.calls[1][3]).toBe(15);
504
505 // Update again while timed out.
424 - root.update(
506 + ReactNoop.render(
507 <App shouldSuspend={true} text="New" textRenderDuration={6} />,
508 );
509 expect(Scheduler).toFlushAndYield([
@@ -431,7 +513,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
513 'New',
514 'Fallback',
515 ]);
434 - expect(root).toMatchRenderedOutput('Loading...');
516 + expect(ReactNoop).toMatchRenderedOutput('Loading...');
517 expect(onRender).toHaveBeenCalledTimes(2);
518
519 // Resolve the pending promise.
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+3 -1
@@ -92,7 +92,9 @@ export const finalizeContainerChildren =
92 export const replaceContainerChildren = $$$hostConfig.replaceContainerChildren;
93 export const cloneHiddenInstance = $$$hostConfig.cloneHiddenInstance;
94 export const cloneUnhiddenInstance = $$$hostConfig.cloneUnhiddenInstance;
95 -export const createHiddenTextInstance = $$$hostConfig.createHiddenTextInstance;
95 +export const cloneHiddenTextInstance = $$$hostConfig.cloneHiddenTextInstance;
96 +export const cloneUnhiddenTextInstance =
97 + $$$hostConfig.cloneUnhiddenTextInstance;
98
99 // -------------------
100 // Hydration
packages/shared/HostConfigWithNoPersistence.js
+2 -1
@@ -30,4 +30,5 @@ export const finalizeContainerChildren = shim;
30 export const replaceContainerChildren = shim;
31 export const cloneHiddenInstance = shim;
32 export const cloneUnhiddenInstance = shim;
33 -export const createHiddenTextInstance = shim;
33 +export const cloneHiddenTextInstance = shim;
34 +export const cloneUnhiddenTextInstance = shim;