Skip to content

Commit 481e594

Browse files
committed
Moar fuzziness
Adds more fuzziness to the generated tests. Specifcally, introduces nested Suspense cases, where the fallback of a Suspense component also suspends. This flushed out a bug (yay!) whose test case I've hard coded.
1 parent b6629ca commit 481e594

File tree

4 files changed

+70
-12
lines changed

4 files changed

+70
-12
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,7 +1178,6 @@ function updateSuspenseComponent(
11781178
currentPrimaryChildFragment.pendingProps,
11791179
NoWork,
11801180
);
1181-
primaryChildFragment.effectTag |= Placement;
11821181

11831182
if ((workInProgress.mode & ConcurrentMode) === NoContext) {
11841183
// Outside of concurrent mode, we commit the effects from the
@@ -1213,7 +1212,6 @@ function updateSuspenseComponent(
12131212
nextFallbackChildren,
12141213
currentFallbackChildFragment.expirationTime,
12151214
));
1216-
fallbackChildFragment.effectTag |= Placement;
12171215
child = primaryChildFragment;
12181216
primaryChildFragment.childExpirationTime = NoWork;
12191217
// Skip the primary children, and continue working on the
@@ -1257,11 +1255,14 @@ function updateSuspenseComponent(
12571255
NoWork,
12581256
null,
12591257
);
1260-
1261-
primaryChildFragment.effectTag |= Placement;
12621258
primaryChildFragment.child = currentPrimaryChild;
12631259
currentPrimaryChild.return = primaryChildFragment;
12641260

1261+
// Even though we're creating a new fiber, there are no new children,
1262+
// because we're reusing an already mounted tree. So we don't need to
1263+
// schedule a placement.
1264+
// primaryChildFragment.effectTag |= Placement;
1265+
12651266
if ((workInProgress.mode & ConcurrentMode) === NoContext) {
12661267
// Outside of concurrent mode, we commit the effects from the
12671268
// partially completed, timed-out tree, too.

packages/react-reconciler/src/ReactFiberCommitWork.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,16 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
558558
} else {
559559
unhideTextInstance(instance, node.memoizedProps);
560560
}
561+
} else if (
562+
node.tag === SuspenseComponent &&
563+
node.memoizedState !== null
564+
) {
565+
// Found a nested Suspense component that timed out. Skip over the
566+
// primary child fragment, which should remain hidden.
567+
const fallbackChildFragment: Fiber = (node.child: any).sibling;
568+
fallbackChildFragment.return = node;
569+
node = fallbackChildFragment;
570+
continue;
561571
} else if (node.child !== null) {
562572
node.child.return = node;
563573
node = node.child;

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
Update,
4646
NoEffect,
4747
DidCapture,
48+
Deletion,
4849
} from 'shared/ReactSideEffectTags';
4950
import invariant from 'shared/invariant';
5051

@@ -82,7 +83,6 @@ import {
8283
popHydrationState,
8384
} from './ReactFiberHydrationContext';
8485
import {ConcurrentMode, NoContext} from './ReactTypeOfMode';
85-
import {reconcileChildFibers} from './ReactChildFiber';
8686

8787
function markUpdate(workInProgress: Fiber) {
8888
// Tag the fiber with an update effect. This turns a Placement into
@@ -715,12 +715,16 @@ function completeWork(
715715
// the stateNode during the begin phase?
716716
const currentFallbackChild: Fiber | null = (current.child: any).sibling;
717717
if (currentFallbackChild !== null) {
718-
reconcileChildFibers(
719-
workInProgress,
720-
currentFallbackChild,
721-
null,
722-
renderExpirationTime,
723-
);
718+
// Deletions go at the beginning of the return fiber's effect list
719+
const first = workInProgress.firstEffect;
720+
if (first !== null) {
721+
workInProgress.firstEffect = currentFallbackChild;
722+
currentFallbackChild.nextEffect = first;
723+
} else {
724+
workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChild;
725+
currentFallbackChild.nextEffect = null;
726+
}
727+
currentFallbackChild.effectTag = Deletion;
724728
}
725729
}
726730

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,28 @@ describe('ReactSuspenseFuzz', () => {
296296
{value: randomInteger(0, 5000), weight: 1},
297297
]);
298298

299-
return React.createElement(Suspense, {maxDuration}, ...children);
299+
const fallbackType = pickRandomWeighted([
300+
{value: 'none', weight: 1},
301+
{value: 'normal', weight: 1},
302+
{value: 'nested suspense', weight: 1},
303+
]);
304+
305+
let fallback;
306+
if (fallbackType === 'normal') {
307+
fallback = 'Loading...';
308+
} else if (fallbackType === 'nested suspense') {
309+
fallback = React.createElement(
310+
React.Fragment,
311+
null,
312+
...createRandomChildren(3),
313+
);
314+
}
315+
316+
return React.createElement(
317+
Suspense,
318+
{maxDuration, fallback},
319+
...children,
320+
);
300321
}
301322
case 'return':
302323
default:
@@ -333,6 +354,28 @@ describe('ReactSuspenseFuzz', () => {
333354
);
334355
});
335356

357+
it('hard-coded cases', () => {
358+
const {Text, testResolvedOutput} = createFuzzer();
359+
360+
testResolvedOutput(
361+
<React.Fragment>
362+
<Text
363+
initialDelay={20}
364+
text="A"
365+
updates={[{beginAfter: 10, suspendFor: 20}]}
366+
/>
367+
<Suspense fallback="Loading... (B)">
368+
<Text
369+
initialDelay={10}
370+
text="B"
371+
updates={[{beginAfter: 30, suspendFor: 50}]}
372+
/>
373+
<Text text="C" />
374+
</Suspense>
375+
</React.Fragment>,
376+
);
377+
});
378+
336379
it('generative tests', () => {
337380
const {generateTestCase, testResolvedOutput} = createFuzzer();
338381

0 commit comments

Comments
 (0)