diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js index cbcdf8b4f6f5..3fc2e8e573ba 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 d4323bde51f9..d46a26fb942e 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) {