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;