Skip to content

Commit e58ecda

Browse files
authored
Suspense fuzz tester (#14147)
* Don't warn if an unmounted component is pinged * Suspense fuzz tester The fuzzer works by generating a random tree of React elements. The tree two types of custom components: - A Text component suspends rendering on initial mount for a fuzzy duration of time. It may update a fuzzy number of times; each update supsends for a fuzzy duration of time. - A Container component wraps some children. It may remount its children a fuzzy number of times, by updating its key. The tree may also include nested Suspense components. After this tree is generated, the tester sets a flag to temporarily disable Text components from suspending. The tree is rendered synchronously. The output of this render is the expected output. Then the tester flips the flag back to enable suspending. It renders the tree again. This time the Text components will suspend for the amount of time configured by the props. The tester waits until everything has resolved. The resolved output is then compared to the expected output generated in the previous step. Finally, we render once more, but this time in concurrent mode. Once again, the resolved output is compared to the expected output. I tested by commenting out various parts of the Suspense implementation to see if broke in the expected way. I also confirmed that it would have caught #14133, a recent bug related to deletions. * When a generated test case fails, log its input * 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. * Use seeded random number generator So if there's a failure, we can bisect.
1 parent 7fd1661 commit e58ecda

File tree

4 files changed

+411
-11
lines changed

4 files changed

+411
-11
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

0 commit comments

Comments
 (0)