mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
242 lines
5.9 KiB
JavaScript
242 lines
5.9 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
let React;
|
|
let ReactNoop;
|
|
let Scheduler;
|
|
let Suspense;
|
|
let getCacheForType;
|
|
let caches;
|
|
let seededCache;
|
|
let waitForAll;
|
|
|
|
describe('ReactSuspenseFallback', () => {
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
|
|
React = require('react');
|
|
ReactNoop = require('react-noop-renderer');
|
|
Scheduler = require('scheduler');
|
|
Suspense = React.Suspense;
|
|
getCacheForType = React.unstable_getCacheForType;
|
|
caches = [];
|
|
seededCache = null;
|
|
|
|
const InternalTestUtils = require('internal-test-utils');
|
|
waitForAll = InternalTestUtils.waitForAll;
|
|
});
|
|
|
|
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';
|
|
record.value = text;
|
|
thenable.pings.forEach(t => t());
|
|
}
|
|
},
|
|
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';
|
|
record.value = error;
|
|
thenable.pings.forEach(t => t());
|
|
}
|
|
},
|
|
};
|
|
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.log(`Suspend! [${text}]`);
|
|
throw record.value;
|
|
case 'rejected':
|
|
Scheduler.log(`Error! [${text}]`);
|
|
throw record.value;
|
|
case 'resolved':
|
|
return textCache.version;
|
|
}
|
|
} else {
|
|
Scheduler.log(`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.log(text);
|
|
return <span prop={text} />;
|
|
}
|
|
|
|
function AsyncText({text, showVersion}) {
|
|
const version = readText(text);
|
|
const fullText = showVersion ? `${text} [v${version}]` : text;
|
|
Scheduler.log(fullText);
|
|
return <span prop={fullText} />;
|
|
}
|
|
|
|
// @gate enableLegacyCache
|
|
it('suspends and shows fallback', async () => {
|
|
ReactNoop.render(
|
|
<Suspense fallback={<Text text="Loading..." />}>
|
|
<AsyncText text="A" />
|
|
</Suspense>,
|
|
);
|
|
|
|
await waitForAll([
|
|
'Suspend! [A]',
|
|
'Loading...',
|
|
// pre-warming
|
|
'Suspend! [A]',
|
|
]);
|
|
expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('suspends and shows null fallback', async () => {
|
|
ReactNoop.render(
|
|
<Suspense fallback={null}>
|
|
<AsyncText text="A" />
|
|
</Suspense>,
|
|
);
|
|
|
|
await waitForAll([
|
|
'Suspend! [A]',
|
|
// null
|
|
// pre-warming
|
|
'Suspend! [A]',
|
|
]);
|
|
expect(ReactNoop).toMatchRenderedOutput(null);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('suspends and shows undefined fallback', async () => {
|
|
ReactNoop.render(
|
|
<Suspense>
|
|
<AsyncText text="A" />
|
|
</Suspense>,
|
|
);
|
|
|
|
await waitForAll([
|
|
'Suspend! [A]',
|
|
// null
|
|
// pre-warming
|
|
'Suspend! [A]',
|
|
]);
|
|
expect(ReactNoop).toMatchRenderedOutput(null);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('suspends and shows inner fallback', async () => {
|
|
ReactNoop.render(
|
|
<Suspense fallback={<Text text="Should not show..." />}>
|
|
<Suspense fallback={<Text text="Loading..." />}>
|
|
<AsyncText text="A" />
|
|
</Suspense>
|
|
</Suspense>,
|
|
);
|
|
|
|
await waitForAll([
|
|
'Suspend! [A]',
|
|
'Loading...',
|
|
// pre-warming
|
|
'Suspend! [A]',
|
|
]);
|
|
expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('suspends and shows inner undefined fallback', async () => {
|
|
ReactNoop.render(
|
|
<Suspense fallback={<Text text="Should not show..." />}>
|
|
<Suspense>
|
|
<AsyncText text="A" />
|
|
</Suspense>
|
|
</Suspense>,
|
|
);
|
|
|
|
await waitForAll([
|
|
'Suspend! [A]',
|
|
// null
|
|
// pre-warming
|
|
'Suspend! [A]',
|
|
]);
|
|
expect(ReactNoop).toMatchRenderedOutput(null);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('suspends and shows inner null fallback', async () => {
|
|
ReactNoop.render(
|
|
<Suspense fallback={<Text text="Should not show..." />}>
|
|
<Suspense fallback={null}>
|
|
<AsyncText text="A" />
|
|
</Suspense>
|
|
</Suspense>,
|
|
);
|
|
|
|
await waitForAll([
|
|
'Suspend! [A]',
|
|
// null
|
|
// pre-warming
|
|
'Suspend! [A]',
|
|
]);
|
|
expect(ReactNoop).toMatchRenderedOutput(null);
|
|
});
|
|
});
|