diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
index cbcdf8b4f6f57..3fc2e8e573ba0 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
@@ -3,11 +3,10 @@ let Fragment;
let ReactNoop;
let Scheduler;
let Suspense;
-let textCache;
+let getCacheForType;
-let readText;
-let resolveText;
-let rejectText;
+let caches;
+let seededCache;
describe('ReactSuspenseWithNoopRenderer', () => {
beforeEach(() => {
@@ -19,80 +18,142 @@ describe('ReactSuspenseWithNoopRenderer', () => {
Scheduler = require('scheduler');
Suspense = React.Suspense;
- textCache = new Map();
-
- readText = text => {
- const record = textCache.get(text);
- if (record !== undefined) {
- switch (record.status) {
- case 'pending':
- throw record.promise;
- case 'rejected':
- throw Error('Failed to load: ' + text);
- case 'resolved':
- return text;
- }
- } else {
- let ping;
- const promise = new Promise(resolve => (ping = resolve));
- const newRecord = {
- status: 'pending',
- ping: ping,
- promise,
- };
- textCache.set(text, newRecord);
- throw promise;
- }
- };
+ getCacheForType = React.unstable_getCacheForType;
+
+ caches = [];
+ seededCache = null;
+ });
- resolveText = text => {
- const record = textCache.get(text);
- if (record !== undefined) {
- if (record.status === 'pending') {
- Scheduler.unstable_yieldValue(`Promise resolved [${text}]`);
- record.ping();
- record.ping = null;
+ function createTextCache() {
+ if (seededCache !== null) {
+ // Trick to seed a cache before it exists.
+ // TODO: Need a built-in API to seed data before the initial render (i.e.
+ // not a refresh because nothing has mounted yet).
+ const cache = seededCache;
+ seededCache = null;
+ return cache;
+ }
+
+ const data = new Map();
+ const version = caches.length + 1;
+ const cache = {
+ version,
+ data,
+ resolve(text) {
+ const record = data.get(text);
+ if (record === undefined) {
+ const newRecord = {
+ status: 'resolved',
+ value: text,
+ };
+ data.set(text, newRecord);
+ } else if (record.status === 'pending') {
+ const thenable = record.value;
record.status = 'resolved';
- clearTimeout(record.promise._timer);
- record.promise = null;
+ record.value = text;
+ thenable.pings.forEach(t => t());
}
- } else {
- const newRecord = {
- ping: null,
- status: 'resolved',
- promise: null,
- };
- textCache.set(text, newRecord);
- }
- };
-
- rejectText = text => {
- const record = textCache.get(text);
- if (record !== undefined) {
- if (record.status === 'pending') {
- Scheduler.unstable_yieldValue(`Promise rejected [${text}]`);
- record.ping();
+ },
+ reject(text, error) {
+ const record = data.get(text);
+ if (record === undefined) {
+ const newRecord = {
+ status: 'rejected',
+ value: error,
+ };
+ data.set(text, newRecord);
+ } else if (record.status === 'pending') {
+ const thenable = record.value;
record.status = 'rejected';
- clearTimeout(record.promise._timer);
- record.promise = null;
+ record.value = error;
+ thenable.pings.forEach(t => t());
}
- } else {
- const newRecord = {
- ping: null,
- status: 'rejected',
- promise: null,
- };
- textCache.set(text, newRecord);
- }
+ },
};
- });
+ caches.push(cache);
+ return cache;
+ }
+
+ function readText(text) {
+ const textCache = getCacheForType(createTextCache);
+ const record = textCache.data.get(text);
+ if (record !== undefined) {
+ switch (record.status) {
+ case 'pending':
+ Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
+ throw record.value;
+ case 'rejected':
+ Scheduler.unstable_yieldValue(`Error! [${text}]`);
+ throw record.value;
+ case 'resolved':
+ return textCache.version;
+ }
+ } else {
+ Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
+
+ const thenable = {
+ pings: [],
+ then(resolve) {
+ if (newRecord.status === 'pending') {
+ thenable.pings.push(resolve);
+ } else {
+ Promise.resolve().then(() => resolve(newRecord.value));
+ }
+ },
+ };
+
+ const newRecord = {
+ status: 'pending',
+ value: thenable,
+ };
+ textCache.data.set(text, newRecord);
+
+ throw thenable;
+ }
+ }
+
+ function Text({text}) {
+ Scheduler.unstable_yieldValue(text);
+ return ;
+ }
+
+ function AsyncText({text, showVersion}) {
+ const version = readText(text);
+ const fullText = showVersion ? `${text} [v${version}]` : text;
+ Scheduler.unstable_yieldValue(fullText);
+ return ;
+ }
+
+ function seedNextTextCache(text) {
+ if (seededCache === null) {
+ seededCache = createTextCache();
+ }
+ seededCache.resolve(text);
+ }
+
+ function resolveMostRecentTextCache(text) {
+ if (caches.length === 0) {
+ throw Error('Cache does not exist.');
+ } else {
+ // Resolve the most recently created cache. An older cache can by
+ // resolved with `caches[index].resolve(text)`.
+ caches[caches.length - 1].resolve(text);
+ }
+ }
- // function div(...children) {
- // children = children.map(
- // c => (typeof c === 'string' ? {text: c, hidden: false} : c),
- // );
- // return {type: 'div', children, prop: undefined, hidden: false};
- // }
+ const resolveText = resolveMostRecentTextCache;
+
+ function rejectMostRecentTextCache(text, error) {
+ if (caches.length === 0) {
+ throw Error('Cache does not exist.');
+ } else {
+ // Resolve the most recently created cache. An older cache can by
+ // resolved with `caches[index].reject(text, error)`.
+ caches[caches.length - 1].reject(text, error);
+ }
+ }
+
+ const rejectText = rejectMostRecentTextCache;
function span(prop) {
return {type: 'span', children: [], prop, hidden: false};
@@ -114,32 +175,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
return Promise.resolve().then(() => {});
}
- function Text(props) {
- Scheduler.unstable_yieldValue(props.text);
- return ;
- }
-
- function AsyncText(props) {
- const text = props.text;
- try {
- readText(text);
- Scheduler.unstable_yieldValue(text);
- return ;
- } catch (promise) {
- if (typeof promise.then === 'function') {
- Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
- if (typeof props.ms === 'number' && promise._timer === undefined) {
- promise._timer = setTimeout(() => {
- resolveText(text);
- }, props.ms);
- }
- } else {
- Scheduler.unstable_yieldValue(`Error! [${text}]`);
- }
- throw promise;
- }
- }
-
// Note: This is based on a similar component we use in www. We can delete
// once the extra div wrapper is no longer necessary.
function LegacyHiddenDiv({children, mode}) {
@@ -153,6 +188,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
}
+ // @gate enableCache
it('does not restart rendering for initial render', async () => {
function Bar(props) {
Scheduler.unstable_yieldValue('Bar');
@@ -190,9 +226,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([]);
// Flush the promise completely
- Scheduler.unstable_advanceTime(100);
- await advanceTimers(100);
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
+ await resolveText('A');
// Even though the promise has resolved, we should now flush
// and commit the in progress render instead of restarting.
@@ -217,6 +251,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
});
+ // @gate enableCache
it('suspends rendering and continues later', async () => {
function Bar(props) {
Scheduler.unstable_yieldValue('Bar');
@@ -229,7 +264,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}>
{renderBar ? (
-
+
) : null}
@@ -254,29 +289,23 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
expect(ReactNoop.getChildren()).toEqual([]);
- // Flush some of the time
- await advanceTimers(50);
- // Still nothing...
- expect(Scheduler).toFlushWithoutYielding();
- expect(ReactNoop.getChildren()).toEqual([]);
-
- // Flush the promise completely
- await advanceTimers(50);
+ // Resolve the data
+ await resolveText('A');
// Renders successfully
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'A', 'B']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
});
+ // @gate enableCache
it('suspends siblings and later recovers each independently', async () => {
// Render two sibling Suspense components
ReactNoop.render(
}>
-
+
}>
-
+
,
);
@@ -291,26 +320,22 @@ describe('ReactSuspenseWithNoopRenderer', () => {
span('Loading B...'),
]);
- // Advance time by enough that the first Suspense's promise resolves and
- // switches back to the normal view. The second Suspense should still
- // show the placeholder
- ReactNoop.expire(5000);
- await advanceTimers(5000);
+ // Resolve first Suspense's promise so that it switches switches back to the
+ // normal view. The second Suspense should still show the placeholder.
+ await resolveText('A');
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading B...')]);
- // Advance time by enough that the second Suspense's promise resolves
- // and switches back to the normal view
- ReactNoop.expire(1000);
- await advanceTimers(1000);
+ // Resolve the second Suspense's promise so that it switches back to the
+ // normal view.
+ await resolveText('B');
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['B']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
});
+ // @gate enableCache
it('continues rendering siblings after suspending', async () => {
// A shell is needed. The update cause it to suspend.
ReactNoop.render(} />);
@@ -338,7 +363,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Wait for data to resolve
await resolveText('B');
// Renders successfully
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['A', 'B', 'C', 'D']);
expect(ReactNoop.getChildren()).toEqual([
span('A'),
@@ -351,6 +375,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Second condition is redundant but guarantees that the test runs in prod.
// TODO: Delete this feature flag.
// @gate !replayFailedUnitOfWorkWithInvokeGuardedCallback || !__DEV__
+ // @gate enableCache
it('retries on error', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
@@ -389,9 +414,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
expect(ReactNoop.getChildren()).toEqual([]);
- await rejectText('Result');
-
- expect(Scheduler).toHaveYielded(['Promise rejected [Result]']);
+ await rejectText('Result', new Error('Failed to load: Result'));
expect(Scheduler).toFlushAndYield([
'Error! [Result]',
@@ -410,6 +433,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Second condition is redundant but guarantees that the test runs in prod.
// TODO: Delete this feature flag.
// @gate !replayFailedUnitOfWorkWithInvokeGuardedCallback || !__DEV__
+ // @gate enableCache
it('retries on error after falling back to a placeholder', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
@@ -442,9 +466,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
- await rejectText('Result');
+ await rejectText('Result', new Error('Failed to load: Result'));
- expect(Scheduler).toHaveYielded(['Promise rejected [Result]']);
expect(Scheduler).toFlushAndYield([
'Error! [Result]',
@@ -460,6 +483,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
});
+ // @gate enableCache
it('can update at a higher priority while in a suspended state', async () => {
function App(props) {
return (
@@ -474,7 +498,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render();
expect(Scheduler).toFlushAndYield(['A', 'Suspend! [1]', 'Loading...']);
await resolveText('1');
- expect(Scheduler).toHaveYielded(['Promise resolved [1]']);
expect(Scheduler).toFlushAndYield(['A', '1']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('1')]);
@@ -497,10 +520,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Unblock the low-pri text and finish
await resolveText('2');
- expect(Scheduler).toHaveYielded(['Promise resolved [2]']);
expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]);
});
+ // @gate enableCache
it('keeps working on lower priority work after being pinged', async () => {
// Advance the virtual time so that we're close to the edge of a bucket.
ReactNoop.expire(149);
@@ -530,11 +553,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([]);
await resolveText('A');
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A', 'B']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
});
+ // @gate enableCache
it('tries rendering a lower priority pending update even if a higher priority one suspends', async () => {
function App(props) {
if (props.hide) {
@@ -567,6 +590,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Note: This test was written to test a heuristic used in the expiration
// times model. Might not make sense in the new model.
+ // @gate enableCache
it('tries each subsequent level after suspending', async () => {
const root = ReactNoop.createRoot();
@@ -638,6 +662,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
});
+ // @gate enableCache
it('forces an expiration after an update times out', async () => {
ReactNoop.render(
@@ -649,7 +674,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(
}>
-
+
,
@@ -667,7 +692,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([]);
// Advance both React's virtual time and Jest's timers by enough to expire
- // the update, but not by enough to flush the suspending promise.
+ // the update.
ReactNoop.expire(10000);
await advanceTimers(10000);
// No additional rendering work is required, since we already prepared
@@ -677,12 +702,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
// Once the promise resolves, we render the suspended view
- await advanceTimers(10000);
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
+ await resolveText('Async');
expect(Scheduler).toFlushAndYield(['Async']);
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
});
+ // @gate enableCache
it('switches to an inner fallback after suspending for a while', async () => {
// Advance the virtual time so that we're closer to the edge of a bucket.
ReactNoop.expire(200);
@@ -714,9 +739,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
// Resolve the outer promise.
- ReactNoop.expire(300);
- await advanceTimers(300);
- expect(Scheduler).toHaveYielded(['Promise resolved [Outer content]']);
+ await resolveText('Outer content');
expect(Scheduler).toFlushAndYield([
'Outer content',
'Suspend! [Inner content]',
@@ -740,9 +763,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
// Finally, flush the inner promise. We should see the complete screen.
- ReactNoop.expire(1000);
- await advanceTimers(1000);
- expect(Scheduler).toHaveYielded(['Promise resolved [Inner content]']);
+ await resolveText('Inner content');
expect(Scheduler).toFlushAndYield(['Inner content']);
expect(ReactNoop.getChildren()).toEqual([
span('Sync'),
@@ -751,6 +772,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
});
+ // @gate enableCache
it('renders an expiration boundary synchronously', async () => {
spyOnDev(console, 'error');
// Synchronously render a tree that suspends
@@ -777,11 +799,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Once the promise resolves, we render the suspended view
await resolveText('Async');
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
expect(Scheduler).toFlushAndYield(['Async']);
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
});
+ // @gate enableCache
it('suspending inside an expired expiration boundary will bubble to the next one', async () => {
ReactNoop.flushSync(() =>
ReactNoop.render(
@@ -805,6 +827,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Loading (outer)...')]);
});
+ // @gate enableCache
it('expires early by default', async () => {
ReactNoop.render(
@@ -816,7 +839,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(
}>
-
+
,
@@ -841,12 +864,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
// Once the promise resolves, we render the suspended view
- await advanceTimers(1000);
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
+ await resolveText('Async');
expect(Scheduler).toFlushAndYield(['Async']);
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
});
+ // @gate enableCache
it('resolves successfully even if fallback render is pending', async () => {
ReactNoop.render(
<>
@@ -858,17 +881,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(
<>
}>
-
+
>,
);
expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']);
- await advanceTimers(1500);
- expect(Scheduler).toHaveYielded([]);
- expect(ReactNoop.getChildren()).toEqual([]);
- // Before we have a chance to flush, the promise resolves.
- await advanceTimers(2000);
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
+
+ await resolveText('Async');
expect(Scheduler).toFlushAndYield([
// We've now pinged the boundary but we don't know if we should restart yet,
// because we haven't completed the suspense boundary.
@@ -879,67 +898,58 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Async')]);
});
+ // @gate enableCache
it('throws a helpful error when an update is suspends without a placeholder', () => {
- ReactNoop.render();
+ ReactNoop.render();
expect(Scheduler).toFlushAndThrow(
'AsyncText suspended while rendering, but no fallback UI was specified.',
);
});
+ // @gate enableCache
it('a Suspense component correctly handles more than one suspended child', async () => {
ReactNoop.render(
}>
-
-
+
+
,
);
- Scheduler.unstable_advanceTime(10000);
- expect(Scheduler).toFlushExpired([
+ expect(Scheduler).toFlushAndYield([
'Suspend! [A]',
'Suspend! [B]',
'Loading...',
]);
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
- await advanceTimers(100);
+ await resolveText('A');
+ await resolveText('B');
- expect(Scheduler).toHaveYielded([
- 'Promise resolved [A]',
- 'Promise resolved [B]',
- ]);
expect(Scheduler).toFlushAndYield(['A', 'B']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
});
+ // @gate enableCache
it('can resume rendering earlier than a timeout', async () => {
ReactNoop.render(} />);
expect(Scheduler).toFlushAndYield([]);
ReactNoop.render(
}>
-
+
,
);
expect(Scheduler).toFlushAndYield(['Suspend! [Async]', 'Loading...']);
expect(ReactNoop.getChildren()).toEqual([]);
- // Advance time by an amount slightly smaller than what's necessary to
- // resolve the promise
- await advanceTimers(99);
-
- // Nothing has rendered yet
- expect(Scheduler).toFlushWithoutYielding();
- expect(ReactNoop.getChildren()).toEqual([]);
-
// Resolve the promise
- await advanceTimers(1);
+ await resolveText('Async');
// We can now resume rendering
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
expect(Scheduler).toFlushAndYield(['Async']);
expect(ReactNoop.getChildren()).toEqual([span('Async')]);
});
// @gate experimental
+ // @gate enableCache
it('starts working on an update even if its priority falls between two suspended levels', async () => {
function App(props) {
return (
@@ -947,7 +957,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
{props.text === 'C' || props.text === 'S' ? (
) : (
-
+
)}
);
@@ -982,17 +992,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(Scheduler).toFlushAndYield(['C']);
expect(ReactNoop.getChildren()).toEqual([span('C')]);
- await advanceTimers(10000);
// Flush the remaining work.
- expect(Scheduler).toHaveYielded([
- 'Promise resolved [A]',
- 'Promise resolved [B]',
- ]);
+ await resolveText('A');
+ await resolveText('B');
// Nothing else to render.
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren()).toEqual([span('C')]);
});
+ // @gate enableCache
it('flushes all expired updates in a single batch', async () => {
class Foo extends React.Component {
componentDidUpdate() {
@@ -1004,7 +1012,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
render() {
return (
}>
-
+
);
}
@@ -1031,22 +1039,21 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
- Scheduler.unstable_advanceTime(20000);
- await advanceTimers(20000);
- expect(Scheduler).toHaveYielded(['Promise resolved [goodbye]']);
+ await resolveText('goodbye');
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
expect(Scheduler).toFlushAndYield(['goodbye']);
expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
});
+ // @gate enableCache
it('a suspended update that expires', async () => {
// Regression test. This test used to fall into an infinite loop.
function ExpensiveText({text}) {
// This causes the update to expire.
Scheduler.unstable_advanceTime(10000);
// Then something suspends.
- return ;
+ return ;
}
function App() {
@@ -1067,12 +1074,9 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
expect(ReactNoop).toMatchRenderedOutput('Loading...');
- await advanceTimers(200000);
- expect(Scheduler).toHaveYielded([
- 'Promise resolved [A]',
- 'Promise resolved [B]',
- 'Promise resolved [C]',
- ]);
+ await resolveText('A');
+ await resolveText('B');
+ await resolveText('C');
expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
expect(ReactNoop).toMatchRenderedOutput(
@@ -1085,11 +1089,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
describe('legacy mode mode', () => {
+ // @gate enableCache
it('times out immediately', async () => {
function App() {
return (
}>
-
+
);
}
@@ -1099,19 +1104,18 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(Scheduler).toHaveYielded(['Suspend! [Result]', 'Loading...']);
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
- ReactNoop.expire(100);
- await advanceTimers(100);
+ await resolveText('Result');
- expect(Scheduler).toHaveYielded(['Promise resolved [Result]']);
expect(Scheduler).toFlushExpired(['Result']);
expect(ReactNoop.getChildren()).toEqual([span('Result')]);
});
+ // @gate enableCache
it('times out immediately when Suspense is in legacy mode', async () => {
class UpdatingText extends React.Component {
state = {step: 1};
render() {
- return ;
+ return ;
}
}
@@ -1136,17 +1140,9 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}
// Initial mount.
+ await seedNextTextCache('Step: 1');
ReactNoop.renderLegacySyncRoot();
- await advanceTimers(100);
- expect(Scheduler).toHaveYielded([
- 'Suspend! [Step: 1]',
- 'Sibling',
- 'Loading (1)',
- 'Loading (2)',
- 'Loading (3)',
- 'Promise resolved [Step: 1]',
- ]);
- expect(Scheduler).toFlushExpired(['Step: 1']);
+ expect(Scheduler).toHaveYielded(['Step: 1', 'Sibling']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -1176,8 +1172,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
>,
);
- await advanceTimers(100);
- expect(Scheduler).toHaveYielded(['Promise resolved [Step: 2]']);
+ await resolveText('Step: 2');
expect(Scheduler).toFlushExpired(['Step: 2']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -1187,6 +1182,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
});
+ // @gate enableCache
it('does not re-render siblings in loose mode', async () => {
class TextWithLifecycle extends React.Component {
componentDidMount() {
@@ -1216,7 +1212,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
return (
}>
-
+
);
@@ -1247,10 +1243,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
>,
);
- ReactNoop.expire(1000);
- await advanceTimers(1000);
+ await resolveText('B');
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushExpired(['B']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -1261,23 +1255,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
});
+ // @gate enableCache
it('suspends inside constructor', async () => {
class AsyncTextInConstructor extends React.Component {
constructor(props) {
super(props);
const text = props.text;
Scheduler.unstable_yieldValue('constructor');
- try {
- readText(text);
- this.state = {text};
- } catch (promise) {
- if (typeof promise.then === 'function') {
- Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
- } else {
- Scheduler.unstable_yieldValue(`Error! [${text}]`);
- }
- throw promise;
- }
+ readText(text);
+ this.state = {text};
}
componentDidMount() {
Scheduler.unstable_yieldValue('componentDidMount');
@@ -1290,7 +1276,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.renderLegacySyncRoot(
}>
-
+
,
);
@@ -1303,7 +1289,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await resolveText('Hi');
- expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
expect(Scheduler).toFlushExpired([
'constructor',
'Hi',
@@ -1312,6 +1297,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
});
+ // @gate enableCache
it('does not infinite loop if fallback contains lifecycle method', async () => {
class Fallback extends React.Component {
state = {
@@ -1331,7 +1317,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
render() {
return (
}>
-
+
);
}
@@ -1346,13 +1332,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Loading...',
]);
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
- await advanceTimers(100);
- expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
+ await resolveText('Hi');
expect(Scheduler).toFlushExpired(['Hi']);
expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
});
if (global.__PERSISTENT__) {
+ // @gate enableCache
it('hides/unhides suspended children before layout effects fire (persistent)', async () => {
const {useRef, useLayoutEffect} = React;
@@ -1365,7 +1351,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
return (
-
+
);
}
@@ -1390,12 +1376,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
>,
]);
- await advanceTimers(1000);
-
- expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
+ await resolveText('Hi');
expect(Scheduler).toFlushExpired(['Hi']);
});
} else {
+ // @gate enableCache
it('hides/unhides suspended children before layout effects fire (mutation)', async () => {
const {useRef, useLayoutEffect} = React;
@@ -1410,7 +1395,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
return (
-
+
);
}
@@ -1432,13 +1417,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Child is hidden: true',
]);
- await advanceTimers(1000);
+ await resolveText('Hi');
- expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
expect(Scheduler).toFlushExpired(['Hi']);
});
}
+ // @gate enableCache
it('handles errors in the return path of a component that suspends', async () => {
// Covers an edge case where an error is thrown inside the complete phase
// of a component that is in the return path of a component that suspends.
@@ -1461,7 +1446,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
-
+
,
@@ -1523,6 +1508,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
});
+ // @gate enableCache
it('does not call lifecycles of a suspended component', async () => {
class TextWithLifecycle extends React.Component {
componentDidMount() {
@@ -1551,18 +1537,9 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}
render() {
const text = this.props.text;
- try {
- readText(text);
- Scheduler.unstable_yieldValue(text);
- return ;
- } catch (promise) {
- if (typeof promise.then === 'function') {
- Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
- } else {
- Scheduler.unstable_yieldValue(`Error! [${text}]`);
- }
- throw promise;
- }
+ readText(text);
+ Scheduler.unstable_yieldValue(text);
+ return ;
}
}
@@ -1570,7 +1547,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
return (
}>
-
+
);
@@ -1601,6 +1578,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
});
+ // @gate enableCache
it('does not call lifecycles of a suspended component (hooks)', async () => {
function TextWithLifecycle(props) {
React.useLayoutEffect(() => {
@@ -1636,25 +1614,16 @@ describe('ReactSuspenseWithNoopRenderer', () => {
};
}, [props.text]);
const text = props.text;
- try {
- readText(text);
- Scheduler.unstable_yieldValue(text);
- return ;
- } catch (promise) {
- if (typeof promise.then === 'function') {
- Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
- } else {
- Scheduler.unstable_yieldValue(`Error! [${text}]`);
- }
- throw promise;
- }
+ readText(text);
+ Scheduler.unstable_yieldValue(text);
+ return ;
}
function App({text}) {
return (
}>
-
+
);
@@ -1696,8 +1665,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await resolveText('B');
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
-
expect(Scheduler).toFlushAndYield([
'B',
'Destroy Layout Effect [Loading...]',
@@ -1732,7 +1699,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await resolveText('B2');
- expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
expect(Scheduler).toFlushAndYield([
'B2',
'Destroy Layout Effect [Loading...]',
@@ -1744,12 +1710,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
});
+ // @gate enableCache
it('suspends for longer if something took a long (CPU bound) time to render', async () => {
function Foo({renderContent}) {
Scheduler.unstable_yieldValue('Foo');
return (
}>
- {renderContent ? : null}
+ {renderContent ? : null}
);
}
@@ -1789,22 +1756,21 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
// Flush the promise completely
- Scheduler.unstable_advanceTime(4500);
- await advanceTimers(4500);
+ await resolveText('A');
// Renders successfully
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A']);
expect(ReactNoop.getChildren()).toEqual([span('A')]);
});
+ // @gate enableCache
it('does not suspends if a fallback has been shown for a long time', async () => {
function Foo() {
Scheduler.unstable_yieldValue('Foo');
return (
}>
-
+
}>
-
+
);
@@ -1823,10 +1789,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
+ await resolveText('A');
// Wait a long time.
Scheduler.unstable_advanceTime(5000);
await advanceTimers(5000);
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
// Retry with the new content.
expect(Scheduler).toFlushAndYield([
@@ -1843,22 +1809,21 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
// Flush the last promise completely
- Scheduler.unstable_advanceTime(5000);
- await advanceTimers(5000);
+ await resolveText('B');
// Renders successfully
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['B']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
});
+ // @gate enableCache
it('does suspend if a fallback has been shown for a short time', async () => {
function Foo() {
Scheduler.unstable_yieldValue('Foo');
return (
}>
-
+
}>
-
+
);
@@ -1877,10 +1842,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
- // Wait a short time.
- Scheduler.unstable_advanceTime(250);
- await advanceTimers(250);
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
+ await resolveText('A');
// Retry with the new content.
expect(Scheduler).toFlushAndYield([
@@ -1893,11 +1855,9 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// wait a bit longer. Still nothing...
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
- Scheduler.unstable_advanceTime(200);
- await advanceTimers(200);
+ await resolveText('B');
// Before we commit another Promise resolves.
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
// We're still showing the first loading state.
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
// Restart and render the complete content.
@@ -1905,12 +1865,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
});
+ // @gate enableCache
it('does not suspend for very long after a higher priority update', async () => {
function Foo({renderContent}) {
Scheduler.unstable_yieldValue('Foo');
return (
}>
- {renderContent ? : null}
+ {renderContent ? : null}
);
}
@@ -1949,6 +1910,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// TODO: flip to "warns" when this is implemented again.
+ // @gate enableCache
it('does not warn when a low priority update suspends inside a high priority update for functional components', async () => {
let _setShow;
function App() {
@@ -1975,6 +1937,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// TODO: flip to "warns" when this is implemented again.
+ // @gate enableCache
it('does not warn when a low priority update suspends inside a high priority update for class components', async () => {
let show;
class App extends React.Component {
@@ -2003,6 +1966,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
});
+ // @gate enableCache
it('does not warn about wrong Suspense priority if no new fallbacks are shown', async () => {
let showB;
class App extends React.Component {
@@ -2037,6 +2001,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// TODO: flip to "warns" when this is implemented again.
+ // @gate enableCache
it(
'does not warn when component that triggered user-blocking update is between Suspense boundary ' +
'and component that suspended',
@@ -2068,6 +2033,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
},
);
+ // @gate enableCache
it('normal priority updates suspending do not warn for class components', async () => {
let show;
class App extends React.Component {
@@ -2093,9 +2059,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(Scheduler).toHaveYielded(['Suspend! [A]']);
await resolveText('A');
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
+ expect(ReactNoop).toMatchRenderedOutput('Loading...');
});
+ // @gate enableCache
it('normal priority updates suspending do not warn for functional components', async () => {
let _setShow;
function App() {
@@ -2118,9 +2085,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(Scheduler).toHaveYielded(['Suspend! [A]']);
await resolveText('A');
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
+ expect(ReactNoop).toMatchRenderedOutput('Loading...');
});
+ // @gate enableCache
it('shows the parent fallback if the inner fallback should be avoided', async () => {
function Foo({showC}) {
Scheduler.unstable_yieldValue('Foo');
@@ -2129,8 +2097,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}>
-
- {showC ? : null}
+
+ {showC ? : null}
@@ -2147,9 +2115,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Initial load...')]);
// Eventually we resolve and show the data.
- Scheduler.unstable_advanceTime(5000);
- await advanceTimers(5000);
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
+ await resolveText('A');
expect(Scheduler).toFlushAndYield(['A', 'B']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
@@ -2174,13 +2140,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
// Later we load the data.
- Scheduler.unstable_advanceTime(5000);
- await advanceTimers(5000);
- expect(Scheduler).toHaveYielded(['Promise resolved [C]']);
+ await resolveText('C');
expect(Scheduler).toFlushAndYield(['A', 'C']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('C'), span('B')]);
});
+ // @gate enableCache
it('favors showing the inner fallback for nested top level avoided fallback', async () => {
function Foo({showB}) {
Scheduler.unstable_yieldValue('Foo');
@@ -2192,7 +2157,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}>
-
+
);
@@ -2212,6 +2177,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading B...')]);
});
+ // @gate enableCache
it('keeps showing an avoided parent fallback if it is already showing', async () => {
function Foo({showB}) {
Scheduler.unstable_yieldValue('Foo');
@@ -2225,7 +2191,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}>
-
+
) : null}
@@ -2255,12 +2221,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading B...')]);
});
+ // @gate enableCache
it('commits a suspended idle pri render within a reasonable time', async () => {
function Foo({renderContent}) {
return (
}>
- {renderContent ? : null}
+ {renderContent ? : null}
);
@@ -2306,11 +2273,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
describe('startTransition', () => {
// @gate experimental
+ // @gate enableCache
it('top level render', async () => {
function App({page}) {
return (
}>
-
+
);
}
@@ -2325,9 +2293,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
// Later we load the data.
- Scheduler.unstable_advanceTime(5000);
- await advanceTimers(5000);
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
+ await resolveText('A');
expect(Scheduler).toFlushAndYield(['A']);
expect(ReactNoop.getChildren()).toEqual([span('A')]);
@@ -2341,14 +2307,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// loading state.
expect(ReactNoop.getChildren()).toEqual([span('A')]);
// Later we load the data.
- Scheduler.unstable_advanceTime(3000);
- await advanceTimers(3000);
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
+ await resolveText('B');
expect(Scheduler).toFlushAndYield(['B']);
expect(ReactNoop.getChildren()).toEqual([span('B')]);
});
// @gate experimental
+ // @gate enableCache
it('hooks', async () => {
let transitionToPage;
function App() {
@@ -2380,7 +2345,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Later we load the data.
await resolveText('A');
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A']);
expect(ReactNoop.getChildren()).toEqual([span('A')]);
@@ -2397,12 +2361,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// Later we load the data.
await resolveText('B');
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['B']);
expect(ReactNoop.getChildren()).toEqual([span('B')]);
});
// @gate experimental
+ // @gate enableCache
it('classes', async () => {
let transitionToPage;
class App extends React.Component {
@@ -2437,7 +2401,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Later we load the data.
await resolveText('A');
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A']);
expect(ReactNoop.getChildren()).toEqual([span('A')]);
@@ -2454,7 +2417,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// Later we load the data.
await resolveText('B');
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['B']);
expect(ReactNoop.getChildren()).toEqual([span('B')]);
});
@@ -2462,6 +2424,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
describe('delays transitions when using React.startTranistion', () => {
// @gate experimental
+ // @gate enableCache
it('top level render', async () => {
function App({page}) {
return (
@@ -2482,7 +2445,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Later we load the data.
await resolveText('A');
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A']);
expect(ReactNoop.getChildren()).toEqual([span('A')]);
@@ -2498,7 +2460,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Later we load the data.
await resolveText('B');
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['B']);
expect(ReactNoop.getChildren()).toEqual([span('B')]);
@@ -2514,6 +2475,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// @gate experimental
+ // @gate enableCache
it('hooks', async () => {
let transitionToPage;
function App() {
@@ -2545,7 +2507,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Later we load the data.
await resolveText('A');
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A']);
expect(ReactNoop.getChildren()).toEqual([span('A')]);
@@ -2564,7 +2525,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Later we load the data.
await resolveText('B');
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['B']);
expect(ReactNoop.getChildren()).toEqual([span('B')]);
@@ -2583,6 +2543,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// @gate experimental
+ // @gate enableCache
it('classes', async () => {
let transitionToPage;
class App extends React.Component {
@@ -2617,7 +2578,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Later we load the data.
await resolveText('A');
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A']);
expect(ReactNoop.getChildren()).toEqual([span('A')]);
@@ -2635,7 +2595,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Later we load the data.
await resolveText('B');
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['B']);
expect(ReactNoop.getChildren()).toEqual([span('B')]);
@@ -2655,6 +2614,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// @gate experimental
+ // @gate enableCache
it('do not show placeholder when updating an avoided boundary with startTransition', async () => {
function App({page}) {
return (
@@ -2673,7 +2633,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render();
expect(Scheduler).toFlushAndYield(['Hi!', 'Suspend! [A]', 'Loading...']);
await resolveText('A');
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['Hi!', 'A']);
expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
@@ -2690,7 +2649,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// We should still be suspended here because this loading state should be avoided.
expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
await resolveText('B');
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['Hi!', 'B']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -2701,6 +2659,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// @gate experimental
+ // @gate enableCache
it('do not show placeholder when mounting an avoided boundary with startTransition', async () => {
function App({page}) {
return (
@@ -2742,7 +2701,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
>,
);
await resolveText('B');
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['Hi!', 'B']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -2754,6 +2712,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// TODO: This test is specifically about avoided commits that suspend for a
// JND. We may remove this behavior.
+ // @gate enableCache
it("suspended commit remains suspended even if there's another update at same expiration", async () => {
// Regression test
function App({text}) {
@@ -2774,7 +2733,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await ReactNoop.act(async () => {
await resolveText('Initial');
});
- expect(Scheduler).toHaveYielded(['Promise resolved [Initial]', 'Initial']);
+ expect(Scheduler).toHaveYielded(['Initial']);
expect(root).toMatchRenderedOutput();
await ReactNoop.act(async () => {
@@ -2853,6 +2812,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// @gate experimental
+ // @gate enableCache
it('should not render hidden content while suspended on higher pri', async () => {
function Offscreen() {
Scheduler.unstable_yieldValue('Offscreen');
@@ -2885,9 +2845,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
- Scheduler.unstable_advanceTime(2000);
- await advanceTimers(2000);
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
+ await resolveText('A');
expect(Scheduler).toFlushAndYieldThrough(['A', 'Commit']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -2905,6 +2863,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// @gate experimental
+ // @gate enableCache
it('should be able to unblock higher pri content before suspended hidden', async () => {
function Offscreen() {
Scheduler.unstable_yieldValue('Offscreen');
@@ -2917,10 +2876,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
return (
}>
-
+
- {showContent ? : null}
+ {showContent ? : null}
);
}
@@ -2939,9 +2898,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
- Scheduler.unstable_advanceTime(2000);
- await advanceTimers(2000);
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
+ await resolveText('A');
expect(Scheduler).toFlushAndYieldThrough(['A', 'Commit']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -2961,6 +2918,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
});
+ // @gate enableCache
it(
'multiple updates originating inside a Suspense boundary at different ' +
'priority levels are not dropped',
@@ -2985,7 +2943,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
return ;
}
- await resolveText('A');
+ await seedNextTextCache('A');
await ReactNoop.act(async () => {
root.render();
});
@@ -3014,6 +2972,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
},
);
+ // @gate enableCache
it(
'multiple updates originating inside a Suspense boundary at different ' +
'priority levels are not dropped, including Idle updates',
@@ -3038,7 +2997,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
return ;
}
- await resolveText('A');
+ await seedNextTextCache('A');
await ReactNoop.act(async () => {
root.render();
});
@@ -3081,6 +3040,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
},
);
+ // @gate enableCache
it(
'fallback component can update itself even after a high pri update to ' +
'the primary tree suspends',
@@ -3109,7 +3069,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}
// Resolve the initial tree
- await resolveText('A');
+ await seedNextTextCache('A');
await ReactNoop.act(async () => {
root.render();
});
@@ -3165,6 +3125,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
},
);
+ // @gate enableCache
it(
'regression: primary fragment fiber is not always part of setState ' +
'return path',
@@ -3193,7 +3154,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}
// Mount an initial tree. Resolve A so that it doesn't suspend.
- await resolveText('A');
+ await seedNextTextCache('A');
await ReactNoop.act(async () => {
root.render();
});
@@ -3243,6 +3204,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
},
);
+ // @gate enableCache
it(
'regression: primary fragment fiber is not always part of setState ' +
'return path (another case)',
@@ -3269,7 +3231,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}
// Mount an initial tree. Resolve A so that it doesn't suspend.
- await resolveText('A');
+ await seedNextTextCache('A');
await ReactNoop.act(async () => {
root.render();
});
@@ -3327,6 +3289,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
},
);
+ // @gate enableCache
it(
'after showing fallback, should not flip back to primary content until ' +
'the update that suspended finishes',
@@ -3380,7 +3343,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}
// Mount an initial tree. Resolve A so that it doesn't suspend.
- await resolveText('Inner text: A');
+ await seedNextTextCache('Inner text: A');
await ReactNoop.act(async () => {
root.render();
});
@@ -3455,7 +3418,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await resolveText('Inner text: B');
});
expect(Scheduler).toHaveYielded([
- 'Promise resolved [Inner text: B]',
'Inner text: B',
'Inner step: 1',
'Commit Child',
@@ -3471,6 +3433,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
},
);
+ // @gate enableCache
it('a high pri update can unhide a boundary that suspended at a different level', async () => {
const {useState, useEffect} = React;
const root = ReactNoop.createRoot();
@@ -3519,7 +3482,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}
// Mount an initial tree. Resolve A so that it doesn't suspend.
- await resolveText('Inner: A0');
+ await seedNextTextCache('Inner: A0');
await ReactNoop.act(async () => {
root.render();
});
@@ -3567,6 +3530,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
});
+ // @gate enableCache
it('regression: empty render at high priority causes update to be dropped', async () => {
// Reproduces a bug where flushDiscreteUpdates starts a new (empty) render
// pass which cancels a scheduled timeout and causes the fallback never to
@@ -3616,6 +3580,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// @gate experimental
+ // @gate enableCache
it('regression: ping at high priority causes update to be dropped', async () => {
const {useState, unstable_useTransition: useTransition} = React;
@@ -3655,8 +3620,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
- await resolveText('A');
- await resolveText('B');
+ await seedNextTextCache('A');
+ await seedNextTextCache('B');
root.render();
});
expect(Scheduler).toHaveYielded(['A', 'B']);
@@ -3698,7 +3663,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
await resolveText('A1');
- expect(Scheduler).toHaveYielded(['Promise resolved [A1]']);
expect(Scheduler).toFlushAndYield([
'A1',
'Suspend! [A2]',
@@ -3716,12 +3680,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await resolveText('A2');
await resolveText('B2');
});
- expect(Scheduler).toHaveYielded([
- 'Promise resolved [A2]',
- 'Promise resolved [B2]',
- 'A2',
- 'B2',
- ]);
+ expect(Scheduler).toHaveYielded(['A2', 'B2']);
expect(root).toMatchRenderedOutput(
<>
@@ -3732,6 +3691,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Regression: https://github.com/facebook/react/issues/18486
// @gate experimental
+ // @gate enableCache
it('does not get stuck in pending state with render phase updates', async () => {
let setTextWithShortTransition;
let setTextWithLongTransition;
@@ -3827,7 +3787,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await ReactNoop.act(async () => {
await resolveText('a');
- expect(Scheduler).toHaveYielded(['Promise resolved [a]']);
expect(Scheduler).toFlushAndYield(['Suspend! [b]', 'Loading...']);
expect(root).toMatchRenderedOutput(
<>
@@ -3840,12 +3799,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await ReactNoop.act(async () => {
await resolveText('b');
});
- expect(Scheduler).toHaveYielded(['Promise resolved [b]', 'b']);
+ expect(Scheduler).toHaveYielded(['b']);
// The bug was that the pending state got stuck forever.
expect(root).toMatchRenderedOutput();
});
});
+ // @gate enableCache
it('regression: #18657', async () => {
const {useState} = React;
@@ -3858,7 +3818,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
- await resolveText('A');
+ await seedNextTextCache('A');
root.render(
}>
@@ -3891,10 +3851,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
setText('B');
await resolveText('B');
});
- expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']);
+ expect(Scheduler).toHaveYielded(['B']);
expect(root).toMatchRenderedOutput();
});
+ // @gate enableCache
it('retries have lower priority than normal updates', async () => {
const {useState} = React;
@@ -3927,7 +3888,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await ReactNoop.act(async () => {
// Resolve the promise. This will trigger a retry.
await resolveText('Async');
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
// Before the retry happens, schedule a new update.
setText('B');
@@ -3950,6 +3910,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
});
+ // @gate enableCache
it('should fire effect clean-up when deleting suspended tree', async () => {
const {useEffect} = React;
@@ -3997,6 +3958,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(Scheduler).toHaveYielded(['Unmount Child']);
});
+ // @gate enableCache
it('should fire effect clean-up when deleting suspended tree (legacy)', async () => {
const {useEffect} = React;
diff --git a/scripts/jest/TestFlags.js b/scripts/jest/TestFlags.js
index d4323bde51f95..d46a26fb942e9 100644
--- a/scripts/jest/TestFlags.js
+++ b/scripts/jest/TestFlags.js
@@ -84,6 +84,11 @@ function getTestFlags() {
...featureFlags,
...environmentFlags,
+
+ // FIXME: www-classic has enableCache on, but when running the source
+ // tests, Jest doesn't expose the API correctly. Fix then remove
+ // this override.
+ enableCache: __EXPERIMENTAL__,
},
{
get(flags, flagName) {