diff --git a/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js b/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js
index 55c3b2162a57c..8d7bb3e75668c 100644
--- a/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js
@@ -166,9 +166,11 @@ describe('ReactBlockingMode', () => {
assertLog(['A1', 'B1']);
expect(root).toMatchRenderedOutput('A1B1');
} else {
+ // Only the second update should have flushed synchronously
assertLog(['B1']);
expect(root).toMatchRenderedOutput('A0B1');
+ // Now flush the first update
await waitForAll(['A1']);
expect(root).toMatchRenderedOutput('A1B1');
}
diff --git a/packages/react-reconciler/src/__tests__/ReactCPUSuspense-test.js b/packages/react-reconciler/src/__tests__/ReactCPUSuspense-test.js
index ec6d9d619078f..3444f3f2a406a 100644
--- a/packages/react-reconciler/src/__tests__/ReactCPUSuspense-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactCPUSuspense-test.js
@@ -143,6 +143,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
>,
);
});
+ // Inner contents finish in separate commit from outer
assertLog(['Inner']);
expect(root).toMatchRenderedOutput(
<>
@@ -178,6 +179,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
root.render();
});
+ // Inner contents finish in separate commit from outer
assertLog(['Outer', 'Loading...', 'Inner [0]']);
expect(root).toMatchRenderedOutput(
<>
@@ -190,6 +192,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
setCount(1);
});
+ // Entire update finishes in a single commit
assertLog(['Outer', 'Inner [1]']);
expect(root).toMatchRenderedOutput(
<>
@@ -227,6 +230,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
>,
);
});
+ // Inner contents suspended, so we continue showing a fallback.
assertLog(['Suspend! [Inner]']);
expect(root).toMatchRenderedOutput(
<>
@@ -276,6 +280,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
root.render();
});
+ // Each level commits separately
assertLog(['A', 'Loading B...', 'B', 'Loading C...', 'C']);
expect(root).toMatchRenderedOutput(
<>
diff --git a/packages/react-reconciler/src/__tests__/ReactCache-test.js b/packages/react-reconciler/src/__tests__/ReactCache-test.js
index 47a96971261fd..25e0dbebb00b3 100644
--- a/packages/react-reconciler/src/__tests__/ReactCache-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactCache-test.js
@@ -219,6 +219,7 @@ describe('ReactCache', () => {
await act(async () => {
root.render('Bye');
});
+ // no cleanup: cache is still retained at the root
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
});
@@ -245,6 +246,7 @@ describe('ReactCache', () => {
await act(async () => {
root.render('Bye');
});
+ // no cleanup: cache is still retained at the root
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
});
@@ -273,6 +275,8 @@ describe('ReactCache', () => {
root.render();
});
+ // Even though there are two new trees, they should share the same
+ // data cache. So there should be only a single cache miss for A.
assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...Loading...');
@@ -285,6 +289,7 @@ describe('ReactCache', () => {
await act(async () => {
root.render('Bye');
});
+ // no cleanup: cache is still retained at the root
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
});
@@ -320,6 +325,8 @@ describe('ReactCache', () => {
await act(async () => {
root.render();
});
+ // Even though there are two new trees, they should share the same
+ // data cache. So there should be only a single cache miss for A.
assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...Loading...');
@@ -332,6 +339,9 @@ describe('ReactCache', () => {
await act(async () => {
root.render('Bye');
});
+ // cleanup occurs for the cache shared by the inner cache boundaries (which
+ // are not shared w the root because they were added in an update)
+ // note that no cache is created for the root since the cache is never accessed
assertLog(['Cache cleanup: A [v1]']);
expect(root).toMatchRenderedOutput('Bye');
});
@@ -356,6 +366,8 @@ describe('ReactCache', () => {
await act(async () => {
root.render();
});
+ // Even though there is a nested boundary, it should share the same
+ // data cache as the root. So there should be only a single cache miss for A.
assertLog(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
@@ -368,6 +380,7 @@ describe('ReactCache', () => {
await act(async () => {
root.render('Bye');
});
+ // no cleanup: cache is still retained at the root
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
},
@@ -412,6 +425,7 @@ describe('ReactCache', () => {
await act(async () => {
root.render('Bye');
});
+ // no cleanup: cache is still retained at the root
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
});
@@ -468,6 +482,8 @@ describe('ReactCache', () => {
await act(async () => {
root.render('Bye!');
});
+ // Cleanup occurs for the *second* cache instance: the first is still
+ // referenced by the root
assertLog(['Cache cleanup: A [v2]']);
expect(root).toMatchRenderedOutput('Bye!');
});
@@ -549,6 +565,7 @@ describe('ReactCache', () => {
await act(async () => {
root.render('Bye');
});
+ // no cleanup: cache is still retained at the root
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
});
@@ -728,12 +745,14 @@ describe('ReactCache', () => {
await act(async () => {
resolveMostRecentTextCache('A');
});
+ // Note that the version has updated, and the previous cache is cleared
assertLog(['A [v2]', 'Cache cleanup: A [v1]']);
expect(root).toMatchRenderedOutput('A [v2]');
await act(async () => {
root.render('Bye');
});
+ // the original root cache already cleaned up when the refresh completed
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
});
@@ -781,12 +800,14 @@ describe('ReactCache', () => {
await act(async () => {
resolveMostRecentTextCache('A');
});
+ // Note that the version has updated, and the previous cache is cleared
assertLog(['A [v2]']);
expect(root).toMatchRenderedOutput('A [v2]');
await act(async () => {
root.render('Bye');
});
+ // the original root cache already cleaned up when the refresh completed
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
});
@@ -841,12 +862,15 @@ describe('ReactCache', () => {
}),
);
});
+ // The root should re-render without a cache miss.
+ // The cache is not cleared up yet, since it's still reference by the root
assertLog(['A [v2]']);
expect(root).toMatchRenderedOutput('A [v2]');
await act(async () => {
root.render('Bye');
});
+ // the refreshed cache boundary is unmounted and cleans up
assertLog(['Cache cleanup: A [v2]']);
expect(root).toMatchRenderedOutput('Bye');
});
@@ -920,6 +944,8 @@ describe('ReactCache', () => {
await act(async () => {
root.render('Bye!');
});
+ // Unmounting children releases the refreshed cache instance only; the root
+ // still retains the original cache instance used for the first render
assertLog(['Cache cleanup: A [v3]']);
expect(root).toMatchRenderedOutput('Bye!');
});
@@ -967,6 +993,8 @@ describe('ReactCache', () => {
root.render();
});
+ // Even though there are two new trees, they should share the same
+ // data cache. So there should be only a single cache miss for A.
assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...Loading...');
@@ -1064,6 +1092,9 @@ describe('ReactCache', () => {
await act(async () => {
root.render('Bye!');
});
+ // Unmounting children releases both cache boundaries, but the original
+ // cache instance (used by second boundary) is still referenced by the root.
+ // only the second cache instance is freed.
assertLog(['Cache cleanup: A [v2]']);
expect(root).toMatchRenderedOutput('Bye!');
},
diff --git a/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js b/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js
index 65eccd01dddd6..49db1ea0fe159 100644
--- a/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js
@@ -339,6 +339,11 @@ describe('ReactLazyContextPropagation', () => {
setOtherValue(1);
setOtherValue(0);
});
+ // NOTE: If this didn't yield anything, that indicates that we never visited
+ // the consumer during the render phase, which probably means the eager
+ // bailout mechanism kicked in. Because we're testing the _lazy_ bailout
+ // mechanism, update this test to foil the _eager_ bailout, somehow. Perhaps
+ // by switching to useReducer.
assertLog(['Consumer']);
expect(root).toMatchRenderedOutput('0');
});
diff --git a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
index dd9053afc1c3e..328e77b1850d8 100644
--- a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
@@ -78,6 +78,7 @@ describe('ReactDeferredValue', () => {
root.render();
await waitForPaint(['Original: 2']);
+ // The deferred value updates in a separate render
await waitForPaint(['Deferred: 2']);
});
expect(root).toMatchRenderedOutput(
@@ -92,6 +93,7 @@ describe('ReactDeferredValue', () => {
startTransition(() => {
root.render();
});
+ // The deferred value updates in the same render as the original
await waitForPaint(['Original: 3', 'Deferred: 3']);
});
expect(root).toMatchRenderedOutput(
@@ -137,6 +139,7 @@ describe('ReactDeferredValue', () => {
root.render();
await waitForPaint(['Original: 2']);
+ // The deferred value updates in a separate render
await waitForPaint(['Deferred: 2']);
});
expect(root).toMatchRenderedOutput(
@@ -151,6 +154,7 @@ describe('ReactDeferredValue', () => {
startTransition(() => {
root.render();
});
+ // The deferred value updates in the same render as the original
await waitForPaint(['Original: 3', 'Deferred: 3']);
});
expect(root).toMatchRenderedOutput(
@@ -201,6 +205,7 @@ describe('ReactDeferredValue', () => {
root.render();
await waitForPaint(['Original: 2']);
+ // The deferred value updates in a separate render
await waitForPaint(['Deferred: 2']);
});
expect(root).toMatchRenderedOutput(
@@ -215,6 +220,7 @@ describe('ReactDeferredValue', () => {
startTransition(() => {
root.render();
});
+ // The deferred value updates in the same render as the original
await waitForPaint(['Original: 3', 'Deferred: 3']);
});
expect(root).toMatchRenderedOutput(
@@ -270,6 +276,9 @@ describe('ReactDeferredValue', () => {
startTransition(() => {
root.render();
});
+ // In the regression, the memoized value was not updated during non-urgent
+ // updates, so this would flip the deferred value back to the initial
+ // value (1) instead of reusing the current one (2).
await waitForPaint(['Original: 2', 'Deferred: 2']);
expect(root).toMatchRenderedOutput(
diff --git a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js
index 2556a16ac3b21..1903ab2eee25f 100644
--- a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js
@@ -171,14 +171,17 @@ describe('ReactExpiration', () => {
});
// Advance the timer.
Scheduler.unstable_advanceTime(2000);
+ // Partially flush the first update, then interrupt it.
await waitFor(['A [render]']);
interrupt();
+ // Don't advance time by enough to expire the first update.
assertLog([]);
expect(ReactNoop).toMatchRenderedOutput(null);
// Schedule another update.
ReactNoop.render(
);
+ // Both updates are batched
await waitForAll(['B [render]', 'B [commit]']);
expect(ReactNoop).toMatchRenderedOutput(
);
@@ -190,6 +193,8 @@ describe('ReactExpiration', () => {
expect(ReactNoop).toMatchRenderedOutput(
);
// Schedule another update.
ReactNoop.render(
);
+ // The updates should flush in the same batch, since as far as the scheduler
+ // knows, they may have occurred inside the same event.
await waitForAll(['B [render]', 'B [commit]']);
});
@@ -223,14 +228,17 @@ describe('ReactExpiration', () => {
});
// Advance the timer.
Scheduler.unstable_advanceTime(2000);
+ // Partially flush the first update, then interrupt it.
await waitFor(['A [render]']);
interrupt();
+ // Don't advance time by enough to expire the first update.
assertLog([]);
expect(ReactNoop).toMatchRenderedOutput(null);
// Schedule another update.
ReactNoop.render(
);
+ // Both updates are batched
await waitForAll(['B [render]', 'B [commit]']);
expect(ReactNoop).toMatchRenderedOutput(
);
@@ -247,6 +255,8 @@ describe('ReactExpiration', () => {
// Schedule another update.
ReactNoop.render(
);
+ // The updates should flush in the same batch, since as far as the scheduler
+ // knows, they may have occurred inside the same event.
await waitForAll(['B [render]', 'B [commit]']);
},
);
@@ -471,7 +481,9 @@ describe('ReactExpiration', () => {
// In other words, we can flush just the first child without flushing
// the rest.
Scheduler.unstable_flushNumberOfYields(1);
+ // Yield right after first child.
assertLog(['Sync pri: 1']);
+ // Now do the rest.
await waitForAll(['Normal pri: 1']);
});
expect(root).toMatchRenderedOutput('Sync pri: 1, Normal pri: 1');
@@ -533,6 +545,7 @@ describe('ReactExpiration', () => {
await waitFor(['Sync pri: 0']);
updateSyncPri();
});
+ // Same thing should happen as last time
assertLog([
// Interrupt idle update to render sync update
'Sync pri: 1',
@@ -733,6 +746,7 @@ describe('ReactExpiration', () => {
Scheduler.unstable_flushNumberOfYields(1);
assertLog(['A1', 'B1', 'C1']);
});
+ // The effect flushes after paint.
assertLog(['Effect: 1']);
});
});
diff --git a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
index 4e3e948c46268..9b9d2abe35e78 100644
--- a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
@@ -52,6 +52,7 @@ describe('ReactFlushSync', () => {
React.startTransition(() => {
root.render(
);
});
+ // This will yield right before the passive effect fires
await waitForPaint(['0, 0']);
// The passive effect will schedule a sync update and a normal update.
@@ -67,6 +68,7 @@ describe('ReactFlushSync', () => {
if (gate(flags => flags.enableUnifiedSyncLane)) {
await waitForPaint([]);
} else {
+ // Now flush it.
await waitForPaint(['1, 1']);
}
});
@@ -112,9 +114,11 @@ describe('ReactFlushSync', () => {
});
});
});
+ // Only the sync update should have flushed
assertLog(['1, 0']);
expect(root).toMatchRenderedOutput('1, 0');
});
+ // Now the async update has flushed, too.
assertLog(['1, 1']);
expect(root).toMatchRenderedOutput('1, 1');
});
@@ -162,6 +166,7 @@ describe('ReactFlushSync', () => {
]);
expect(root).toMatchRenderedOutput('Child');
});
+ // Effect flushes after paint.
assertLog(['Effect']);
});
@@ -217,6 +222,7 @@ describe('ReactFlushSync', () => {
]);
expect(root).toMatchRenderedOutput('Child');
});
+ // Effect flushes after paint.
assertLog(['Effect']);
});
@@ -236,8 +242,10 @@ describe('ReactFlushSync', () => {
// Passive effects are pending. Calling flushSync should not affect them.
ReactNoop.flushSync();
+ // Effects still haven't fired.
assertLog([]);
});
+ // Now the effects have fired.
assertLog(['Effect']);
});
});
diff --git a/packages/react-reconciler/src/__tests__/ReactFragment-test.js b/packages/react-reconciler/src/__tests__/ReactFragment-test.js
index 7de3300bf33b7..e6b9c185ca40e 100644
--- a/packages/react-reconciler/src/__tests__/ReactFragment-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactFragment-test.js
@@ -944,6 +944,7 @@ describe('ReactFragment', () => {
);
ReactNoop.render(
);
+ // The key warning gets deduped because it's in the same component.
await waitForAll([]);
expect(ops).toEqual(['Update Stateful']);
@@ -955,6 +956,7 @@ describe('ReactFragment', () => {
);
ReactNoop.render(
);
+ // The key warning gets deduped because it's in the same component.
await waitForAll([]);
expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
index 3a268504136df..0c857c232424b 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
@@ -299,6 +299,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Resolve the data
await resolveText('A');
+ // Renders successfully
await waitForAll(['Foo', 'Bar', 'A', 'B']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -375,6 +376,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
,
);
});
+ // B suspends. Continue rendering the remaining siblings.
await waitForAll(['A', 'Suspend! [B]', 'C', 'D', 'Loading...']);
// Did not commit yet.
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -382,6 +384,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Wait for data to resolve
await resolveText('B');
await waitForAll(['A', 'B', 'C', 'D']);
+ // Renders successfully
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -730,6 +733,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// the update.
ReactNoop.expire(10000);
await advanceTimers(10000);
+ // No additional rendering work is required, since we already prepared
+ // the placeholder.
assertLog([]);
// Should have committed the placeholder.
expect(ReactNoop).toMatchRenderedOutput(
@@ -974,6 +979,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// but not by enough to flush the promise or reach the true expiration time.
ReactNoop.expire(2000);
await advanceTimers(2000);
+ // Even flushing won't yield a fallback in a transition.
expect(ReactNoop).toMatchRenderedOutput(null);
await waitForAll([]);
@@ -1014,6 +1020,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await resolveText('Async');
+ // Because we're already showing a fallback, interrupt the current render
+ // and restart immediately.
await waitForAll(['Async', 'Sibling']);
expect(root).toMatchRenderedOutput(
<>
@@ -1081,6 +1089,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Resolve the promise
await resolveText('Async');
+ // We can now resume rendering
await waitForAll(['Async']);
expect(ReactNoop).toMatchRenderedOutput(
);
});
@@ -1106,6 +1115,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Schedule an update, and suspend for up to 5 seconds.
React.startTransition(() => ReactNoop.render(
));
+ // The update should suspend.
await waitForAll(['Suspend! [A]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput(
);
@@ -1117,6 +1127,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Schedule another low priority update.
React.startTransition(() => ReactNoop.render(
));
+ // This update should also suspend.
await waitForAll(['Suspend! [B]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput(
);
@@ -1129,6 +1140,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Flush the remaining work.
await resolveText('A');
await resolveText('B');
+ // Nothing else to render.
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
);
});
@@ -1778,6 +1790,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Commit root',
]);
+ // Flush passive effects.
await waitForAll([
'Effect [A]',
// B's effect should not fire because it suspended
@@ -1823,6 +1836,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Commit root',
]);
+ // Flush passive effects.
await waitForAll([
// B2's effect should not fire because it suspended
// 'Effect [B2]',
@@ -1863,6 +1877,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
Scheduler.unstable_advanceTime(100);
await advanceTimers(100);
+ // Start rendering
await waitFor(['Foo']);
// For some reason it took a long time to render Foo.
Scheduler.unstable_advanceTime(1250);
@@ -1878,12 +1893,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Flush some of the time
Scheduler.unstable_advanceTime(450);
await advanceTimers(450);
+ // Because we've already been waiting for so long we can
+ // wait a bit longer. Still nothing...
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(null);
// Eventually we'll show the fallback.
Scheduler.unstable_advanceTime(500);
await advanceTimers(500);
+ // No need to rerender.
await waitForAll([]);
// Since this is a transition, we never fallback.
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -1891,6 +1909,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Flush the promise completely
await resolveText('A');
await waitForAll(['Foo', 'A']);
+ // Renders successfully
+ // TODO: Why does this render Foo
expect(ReactNoop).toMatchRenderedOutput(
);
});
@@ -1909,6 +1929,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}
ReactNoop.render(
);
+ // Start rendering
await waitForAll([
'Foo',
// A suspends
@@ -1925,6 +1946,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
Scheduler.unstable_advanceTime(5000);
await advanceTimers(5000);
+ // Retry with the new content.
await waitForAll([
'A',
// B still suspends
@@ -1943,6 +1965,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Flush the last promise completely
await resolveText('B');
+ // Renders successfully
await waitForAll(['B']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -1967,6 +1990,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}
ReactNoop.render(
);
+ // Start rendering
await waitForAll([
'Foo',
// A suspends
@@ -1980,6 +2004,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await resolveText('A');
+ // Retry with the new content.
await waitForAll([
'A',
// B still suspends
@@ -1995,6 +2020,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Before we commit another Promise resolves.
// We're still showing the first loading state.
expect(ReactNoop).toMatchRenderedOutput(
);
+ // Restart and render the complete content.
await waitForAll(['A', 'B']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -2040,6 +2066,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
Scheduler.unstable_advanceTime(500);
jest.advanceTimersByTime(500);
+ // We should have already shown the fallback.
+ // When we wrote this test, we inferred the start time of high priority
+ // updates as way earlier in the past. This test ensures that we don't
+ // use this assumption to add a very long JND.
await waitForAll([]);
// Transitions never fallback.
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -2481,6 +2511,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Schedule an update at idle pri.
ReactNoop.idleUpdates(() => ReactNoop.render(
));
+ // We won't even work on Idle priority.
await waitForAll([]);
// We're still suspended.
@@ -3040,6 +3071,9 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
foo.setState({suspend: true});
+ // In the regression that this covers, we would neglect to reset the
+ // current debug phase after suspending (in the catch block), so React
+ // thinks we're still inside the render phase.
await waitFor(['Suspend!']);
// Then when this setState happens, React would incorrectly fire a warning
@@ -3130,6 +3164,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await waitFor(['Commit']);
expect(ReactNoop).toMatchRenderedOutput(
);
+ // Partially render through the hidden content.
await waitFor(['Suspend! [A]']);
// Start transition.
@@ -3270,6 +3305,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
>,
);
+ // Now flush the remaining work. The Idle update successfully finishes.
await waitForAll(['C']);
expect(root).toMatchRenderedOutput(
);
});
@@ -3433,6 +3469,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
setText('D');
});
+ // Even though the fragment fiber is not part of the return path, we should
+ // be able to finish rendering.
assertLog(['D']);
expect(root).toMatchRenderedOutput(
);
},
@@ -3513,6 +3551,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
setText('E');
});
});
+ // Even though the fragment fiber is not part of the return path, we should
+ // be able to finish rendering.
assertLog(['Suspend! [D]', 'E']);
expect(root).toMatchRenderedOutput(
);
},
@@ -3622,6 +3662,9 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
});
+ // Only the outer part can update. The inner part should still show a
+ // fallback because we haven't finished loading B yet. Otherwise, the
+ // inner text would be inconsistent with the outer text.
assertLog([
'Outer text: B',
'Outer step: 1',
@@ -3882,11 +3925,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.idleUpdates(() => {
setText('B');
});
+ // Suspend the first update. The second update doesn't run because it has
+ // Idle priority.
await waitForAll(['Suspend! [B]', 'Loading...']);
// Commit the fallback. Now we'll try working on Idle.
jest.runAllTimers();
+ // It also suspends.
await waitForAll(['Suspend! [B]']);
});
@@ -3934,6 +3980,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Before the retry happens, schedule a new update.
setText('B');
+ // The update should be allowed to finish before the retry is attempted.
await waitForPaint(['B']);
expect(root).toMatchRenderedOutput(
<>
@@ -3942,6 +3989,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
>,
);
});
+ // Then do the retry.
assertLog(['Async']);
expect(root).toMatchRenderedOutput(
<>