let React;
let startTransition;
let ReactNoop;
let resolveSuspenseyThing;
let getSuspenseyThingStatus;
let Suspense;
let Activity;
let SuspenseList;
let useMemo;
let Scheduler;
let act;
let assertLog;
let waitForPaint;
describe('ReactSuspenseyCommitPhase', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
Suspense = React.Suspense;
if (gate(flags => flags.enableSuspenseList)) {
SuspenseList = React.unstable_SuspenseList;
}
Activity = React.unstable_Activity;
useMemo = React.useMemo;
startTransition = React.startTransition;
resolveSuspenseyThing = ReactNoop.resolveSuspenseyThing;
getSuspenseyThingStatus = ReactNoop.getSuspenseyThingStatus;
const InternalTestUtils = require('internal-test-utils');
act = InternalTestUtils.act;
assertLog = InternalTestUtils.assertLog;
waitForPaint = InternalTestUtils.waitForPaint;
});
function Text({text}) {
Scheduler.log(text);
return text;
}
function SuspenseyImage({src}) {
return (
<suspensey-thing
src={src}
timeout={100}
onLoadStart={() => Scheduler.log(`Image requested [${src}]`)}
/>
);
}
it('suspend commit during initial mount', async () => {
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<SuspenseyImage src="A" />
</Suspense>,
);
});
});
assertLog(['Image requested [A]', 'Loading...']);
expect(getSuspenseyThingStatus('A')).toBe('pending');
expect(root).toMatchRenderedOutput('Loading...');
resolveSuspenseyThing('A');
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
});
it('suspend commit during update', async () => {
const root = ReactNoop.createRoot();
await act(() => resolveSuspenseyThing('A'));
await act(async () => {
startTransition(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<SuspenseyImage src="A" />
</Suspense>,
);
});
});
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
await act(async () => {
startTransition(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<SuspenseyImage src="B" />
</Suspense>,
);
});
});
assertLog(['Image requested [B]']);
expect(getSuspenseyThingStatus('B')).toBe('pending');
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
resolveSuspenseyThing('B');
expect(root).toMatchRenderedOutput(<suspensey-thing src="B" />);
});
it('suspend commit during initial mount at the root', async () => {
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(<SuspenseyImage src="A" />);
});
});
assertLog(['Image requested [A]']);
expect(getSuspenseyThingStatus('A')).toBe('pending');
expect(root).toMatchRenderedOutput(null);
resolveSuspenseyThing('A');
expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
});
it('suspend commit during update at the root', async () => {
const root = ReactNoop.createRoot();
await act(() => resolveSuspenseyThing('A'));
expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
expect(root).toMatchRenderedOutput(null);
await act(async () => {
startTransition(() => {
root.render(<SuspenseyImage src="A" />);
});
});
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
await act(async () => {
startTransition(() => {
root.render(<SuspenseyImage src="B" />);
});
});
assertLog(['Image requested [B]']);
expect(getSuspenseyThingStatus('B')).toBe('pending');
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
resolveSuspenseyThing('B');
expect(getSuspenseyThingStatus('B')).toBe('fulfilled');
expect(root).toMatchRenderedOutput(<suspensey-thing src="B" />);
});
it('suspend commit during urgent initial mount', async () => {
const root = ReactNoop.createRoot();
await act(async () => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<SuspenseyImage src="A" />
</Suspense>,
);
});
assertLog(['Image requested [A]', 'Loading...']);
expect(getSuspenseyThingStatus('A')).toBe('pending');
expect(root).toMatchRenderedOutput('Loading...');
resolveSuspenseyThing('A');
expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
});
it('suspend commit during urgent update', async () => {
const root = ReactNoop.createRoot();
await act(() => resolveSuspenseyThing('A'));
expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
await act(async () => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<SuspenseyImage src="A" />
</Suspense>,
);
});
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
resolveSuspenseyThing('A');
expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
await act(async () => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<SuspenseyImage src="B" />
</Suspense>,
);
});
assertLog(['Image requested [B]', 'Loading...']);
expect(getSuspenseyThingStatus('B')).toBe('pending');
expect(root).toMatchRenderedOutput(
<>
<suspensey-thing src="A" hidden={true} />
{'Loading...'}
</>,
);
resolveSuspenseyThing('B');
expect(getSuspenseyThingStatus('B')).toBe('fulfilled');
expect(root).toMatchRenderedOutput(<suspensey-thing src="B" />);
});
it('suspends commit during urgent initial mount at the root', async () => {
const root = ReactNoop.createRoot();
await act(async () => {
root.render(<SuspenseyImage src="A" />);
});
assertLog(['Image requested [A]']);
expect(getSuspenseyThingStatus('A')).toBe('pending');
expect(root).toMatchRenderedOutput(null);
resolveSuspenseyThing('A');
expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
});
it('suspends commit during urgent update at the root', async () => {
const root = ReactNoop.createRoot();
await act(() => resolveSuspenseyThing('A'));
expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
expect(root).toMatchRenderedOutput(null);
await act(async () => {
root.render(<SuspenseyImage src="A" />);
});
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
await act(async () => {
root.render(<SuspenseyImage src="B" />);
});
assertLog(['Image requested [B]']);
expect(getSuspenseyThingStatus('B')).toBe('pending');
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
resolveSuspenseyThing('B');
expect(getSuspenseyThingStatus('B')).toBe('fulfilled');
expect(root).toMatchRenderedOutput(<suspensey-thing src="B" />);
});
it('does suspend commit during urgent initial mount at the root when sync rendering', async () => {
const root = ReactNoop.createRoot();
await act(async () => {
ReactNoop.flushSync(() => {
root.render(<SuspenseyImage src="A" />);
});
});
assertLog(['Image requested [A]']);
expect(getSuspenseyThingStatus('A')).toBe('pending');
expect(root).toMatchRenderedOutput(null);
resolveSuspenseyThing('A');
expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
});
it('does suspend commit during urgent update at the root when sync rendering', async () => {
const root = ReactNoop.createRoot();
await act(() => resolveSuspenseyThing('A'));
expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
expect(root).toMatchRenderedOutput(null);
await act(async () => {
ReactNoop.flushSync(() => {
root.render(<SuspenseyImage src="A" />);
});
});
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
await act(async () => {
ReactNoop.flushSync(() => {
root.render(<SuspenseyImage src="B" />);
});
});
assertLog(['Image requested [B]']);
expect(getSuspenseyThingStatus('B')).toBe('pending');
expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
resolveSuspenseyThing('B');
expect(getSuspenseyThingStatus('B')).toBe('fulfilled');
expect(root).toMatchRenderedOutput(<suspensey-thing src="B" />);
});
it('an urgent update interrupts a suspended commit', async () => {
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<SuspenseyImage src="A" />);
});
});
assertLog(['Image requested [A]']);
expect(root).toMatchRenderedOutput(null);
await act(() => {
root.render(<Text text="Something else" />);
});
assertLog(['Something else']);
expect(root).toMatchRenderedOutput('Something else');
});
it('a transition update interrupts a suspended commit', async () => {
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<SuspenseyImage src="A" />);
});
});
assertLog(['Image requested [A]']);
expect(root).toMatchRenderedOutput(null);
await act(() => {
startTransition(() => {
root.render(<Text text="Something else" />);
});
});
assertLog(['Something else']);
expect(root).toMatchRenderedOutput('Something else');
});
it('demonstrate current behavior when used with SuspenseList (not ideal)', async () => {
function App() {
return (
<SuspenseList revealOrder="forwards">
<Suspense fallback={<Text text="Loading A" />}>
<SuspenseyImage src="A" />
</Suspense>
<Suspense fallback={<Text text="Loading B" />}>
<SuspenseyImage src="B" />
</Suspense>
<Suspense fallback={<Text text="Loading C" />}>
<SuspenseyImage src="C" />
</Suspense>
</SuspenseList>
);
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<App />);
});
});
assertLog([
'Image requested [A]',
'Loading A',
'Loading B',
'Loading C',
'Image requested [B]',
'Image requested [C]',
]);
expect(root).toMatchRenderedOutput('Loading ALoading BLoading C');
resolveSuspenseyThing('A');
expect(root).toMatchRenderedOutput('Loading ALoading BLoading C');
resolveSuspenseyThing('B');
expect(root).toMatchRenderedOutput('Loading ALoading BLoading C');
resolveSuspenseyThing('C');
expect(root).toMatchRenderedOutput(
<>
<suspensey-thing src="A" />
<suspensey-thing src="B" />
<suspensey-thing src="C" />
</>,
);
});
it('avoid triggering a fallback if resource loads immediately', async () => {
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<suspensey-thing
src="A"
onLoadStart={() => Scheduler.log('Request [A]')}>
<suspensey-thing
src="B"
onLoadStart={() => Scheduler.log('Request [B]')}
/>
</suspensey-thing>
<suspensey-thing
src="C"
onLoadStart={() => Scheduler.log('Request [C]')}
/>
</Suspense>,
);
});
await waitForPaint(['Request [B]']);
resolveSuspenseyThing('B');
await waitForPaint(['Request [A]']);
resolveSuspenseyThing('A');
await waitForPaint(['Request [C]']);
resolveSuspenseyThing('C');
});
expect(root).toMatchRenderedOutput(
<>
<suspensey-thing src="A">
<suspensey-thing src="B" />
</suspensey-thing>
<suspensey-thing src="C" />
</>,
);
});
it("host instances don't suspend during prerendering, but do suspend when they are revealed", async () => {
function More() {
Scheduler.log('More');
return <SuspenseyImage src="More" />;
}
function Details({showMore}) {
Scheduler.log('Details');
const more = useMemo(() => <More />, []);
return (
<>
<div>Main Content</div>
<Activity mode={showMore ? 'visible' : 'hidden'}>{more}</Activity>
</>
);
}
const root = ReactNoop.createRoot();
await act(async () => {
root.render(<Details showMore={false} />);
await waitForPaint(['Details']);
expect(root).toMatchRenderedOutput(<div>Main Content</div>);
});
assertLog(['More', 'Image requested [More]']);
expect(root).toMatchRenderedOutput(
<>
<div>Main Content</div>
<suspensey-thing hidden={true} src="More" />
</>,
);
await act(() => {
startTransition(() => {
root.render(<Details showMore={true} />);
});
});
assertLog(['Details']);
expect(root).toMatchRenderedOutput(
<>
<div>Main Content</div>
<suspensey-thing hidden={true} src="More" />
</>,
);
resolveSuspenseyThing('More');
expect(root).toMatchRenderedOutput(
<>
<div>Main Content</div>
<suspensey-thing src="More" />
</>,
);
});
});