Files
react/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js
T
Sebastian Markbåge 1c43d0aed7 Unify serverAct helpers (#33327)
This uses the richer `serverAct` helper that we already use in other
tests.

This avoids using the `Scheduler`. We don't use that package on the
server so it doesn't make sense to simulate going through it.
Additionally, we really should be getting rid of it on the client too to
favor `postTask` polyfills.
2025-05-21 16:13:54 -04:00

232 lines
6.3 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.
*
* @emails react-core
*/
'use strict';
import {
insertNodesAndExecuteScripts,
getVisibleChildren,
} from '../test-utils/FizzTestUtils';
import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
// Polyfills for test environment
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
let act;
let serverAct;
let assertLog;
let waitForPaint;
let container;
let React;
let Scheduler;
let ReactDOMServer;
let ReactDOMClient;
let useDeferredValue;
let Suspense;
describe('ReactDOMFizzForm', () => {
beforeEach(() => {
jest.resetModules();
Scheduler = require('scheduler');
patchMessageChannel();
act = require('internal-test-utils').act;
serverAct = require('internal-test-utils').serverAct;
React = require('react');
ReactDOMServer = require('react-dom/server.browser');
ReactDOMClient = require('react-dom/client');
useDeferredValue = React.useDeferredValue;
Suspense = React.Suspense;
assertLog = require('internal-test-utils').assertLog;
waitForPaint = require('internal-test-utils').waitForPaint;
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
async function readIntoContainer(stream) {
const reader = stream.getReader();
let result = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
result += Buffer.from(value).toString('utf8');
}
const temp = document.createElement('div');
temp.innerHTML = result;
insertNodesAndExecuteScripts(temp, container, null);
}
function Text({text}) {
Scheduler.log(text);
return text;
}
it('returns initialValue argument, if provided', async () => {
function App() {
return useDeferredValue('Final', 'Initial');
}
const stream = await serverAct(() =>
ReactDOMServer.renderToReadableStream(<App />),
);
await readIntoContainer(stream);
expect(container.textContent).toEqual('Initial');
// After hydration, it's updated to the final value
await act(() => ReactDOMClient.hydrateRoot(container, <App />));
expect(container.textContent).toEqual('Final');
});
// @gate enablePostpone
it(
'if initial value postpones during hydration, it will switch to the ' +
'final value instead',
async () => {
function Content() {
const isInitial = useDeferredValue(false, true);
if (isInitial) {
React.unstable_postpone();
}
return <Text text="Final" />;
}
function App() {
return (
<div>
<Suspense fallback={<Text text="Loading..." />}>
<Content />
</Suspense>
</div>
);
}
const stream = await serverAct(() =>
ReactDOMServer.renderToReadableStream(<App />),
);
await readIntoContainer(stream);
expect(container.textContent).toEqual('Loading...');
assertLog(['Loading...']);
// After hydration, it's updated to the final value
await act(() => ReactDOMClient.hydrateRoot(container, <App />));
expect(container.textContent).toEqual('Final');
assertLog(['Loading...', 'Final']);
},
);
it(
'useDeferredValue during hydration has higher priority than remaining ' +
'incremental hydration',
async () => {
function B() {
const text = useDeferredValue('B [Final]', 'B [Initial]');
return <Text text={text} />;
}
function App() {
return (
<div>
<span>
<Text text="A" />
</span>
<Suspense fallback={<Text text="Loading..." />}>
<span>
<B />
</span>
<div>
<Suspense fallback={<Text text="Loading..." />}>
<span id="C" ref={cRef}>
<Text text="C" />
</span>
</Suspense>
</div>
</Suspense>
</div>
);
}
const cRef = React.createRef();
const stream = await serverAct(() =>
ReactDOMServer.renderToReadableStream(<App />),
);
await readIntoContainer(stream);
assertLog(['A', 'B [Initial]', 'C']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>B [Initial]</span>
<div>
<span id="C">C</span>
</div>
</div>,
);
const serverRenderedC = document.getElementById('C');
// On the client, we first hydrate the initial value, then upgrade
// to final.
await act(async () => {
ReactDOMClient.hydrateRoot(container, <App />);
// First the outermost Suspense boundary hydrates.
await waitForPaint(['A']);
expect(cRef.current).toBe(null);
// Then the next level hydrates. This level includes a useDeferredValue,
// so we should prioritize upgrading it before we proceed to hydrating
// additional levels.
await waitForPaint(['B [Initial]']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>B [Initial]</span>
<div>
<span id="C">C</span>
</div>
</div>,
);
expect(cRef.current).toBe(null);
// This paint should only update B. C should still be dehydrated.
await waitForPaint(['B [Final]']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>B [Final]</span>
<div>
<span id="C">C</span>
</div>
</div>,
);
expect(cRef.current).toBe(null);
});
// Finally we can hydrate C
assertLog(['C']);
expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>B [Final]</span>
<div>
<span id="C">C</span>
</div>
</div>,
);
expect(cRef.current).toBe(serverRenderedC);
},
);
});