'use strict';
import {createEventTarget} from 'dom-event-testing-library';
let React;
let ReactDOM;
let ReactDOMClient;
let ReactDOMServer;
let ReactFeatureFlags;
let Scheduler;
let Activity;
let act;
let assertLog;
let waitForAll;
let waitFor;
let waitForPaint;
let IdleEventPriority;
let ContinuousEventPriority;
function dispatchMouseHoverEvent(to, from) {
if (!to) {
to = null;
}
if (!from) {
from = null;
}
if (from) {
const mouseOutEvent = document.createEvent('MouseEvents');
mouseOutEvent.initMouseEvent(
'mouseout',
true,
true,
window,
0,
50,
50,
50,
50,
false,
false,
false,
false,
0,
to,
);
from.dispatchEvent(mouseOutEvent);
}
if (to) {
const mouseOverEvent = document.createEvent('MouseEvents');
mouseOverEvent.initMouseEvent(
'mouseover',
true,
true,
window,
0,
50,
50,
50,
50,
false,
false,
false,
false,
0,
from,
);
to.dispatchEvent(mouseOverEvent);
}
}
function dispatchClickEvent(target) {
const mouseOutEvent = document.createEvent('MouseEvents');
mouseOutEvent.initMouseEvent(
'click',
true,
true,
window,
0,
50,
50,
50,
50,
false,
false,
false,
false,
0,
target,
);
return target.dispatchEvent(mouseOutEvent);
}
function TODO_scheduleIdleDOMSchedulerTask(fn) {
ReactDOM.unstable_runWithPriority(IdleEventPriority, () => {
const prevEvent = window.event;
window.event = {type: 'message'};
try {
fn();
} finally {
window.event = prevEvent;
}
});
}
function TODO_scheduleContinuousSchedulerTask(fn) {
ReactDOM.unstable_runWithPriority(ContinuousEventPriority, () => {
const prevEvent = window.event;
window.event = {type: 'message'};
try {
fn();
} finally {
window.event = prevEvent;
}
});
}
describe('ReactDOMServerSelectiveHydrationActivity', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableCreateEventHandleAPI = true;
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
ReactDOMServer = require('react-dom/server');
act = require('internal-test-utils').act;
Scheduler = require('scheduler');
Activity = React.unstable_Activity;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
waitForPaint = InternalTestUtils.waitForPaint;
IdleEventPriority = require('react-reconciler/constants').IdleEventPriority;
ContinuousEventPriority =
require('react-reconciler/constants').ContinuousEventPriority;
});
it('hydrates the target boundary synchronously during a click', async () => {
function Child({text}) {
Scheduler.log(text);
return (
<span
onClick={e => {
e.preventDefault();
Scheduler.log('Clicked ' + text);
}}>
{text}
</span>
);
}
function App() {
Scheduler.log('App');
return (
<div>
<Activity>
<Child text="A" />
</Activity>
<Activity>
<Child text="B" />
</Activity>
</div>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['App', 'A', 'B']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[1];
ReactDOMClient.hydrateRoot(container, <App />);
assertLog([]);
const result = dispatchClickEvent(span);
expect(result).toBe(false);
assertLog(['App', 'B', 'Clicked B']);
await waitForAll(['A']);
document.body.removeChild(container);
});
it('hydrates at higher pri if sync did not work first time', async () => {
let suspend = false;
let resolve;
const promise = new Promise(resolvePromise => (resolve = resolvePromise));
function Child({text}) {
if ((text === 'A' || text === 'D') && suspend) {
throw promise;
}
Scheduler.log(text);
return (
<span
onClick={e => {
e.preventDefault();
Scheduler.log('Clicked ' + text);
}}>
{text}
</span>
);
}
function App() {
Scheduler.log('App');
return (
<div>
<Activity>
<Child text="A" />
</Activity>
<Activity>
<Child text="B" />
</Activity>
<Activity>
<Child text="C" />
</Activity>
<Activity>
<Child text="D" />
</Activity>
</div>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['App', 'A', 'B', 'C', 'D']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
const spanD = container.getElementsByTagName('span')[3];
suspend = true;
ReactDOMClient.hydrateRoot(container, <App />);
assertLog([]);
await act(() => {
const result = dispatchClickEvent(spanD);
expect(result).toBe(true);
});
assertLog([
'App',
'B',
'C',
]);
await act(async () => {
suspend = false;
resolve();
await promise;
});
assertLog(['D', 'A']);
document.body.removeChild(container);
});
it('hydrates at higher pri for secondary discrete events', async () => {
let suspend = false;
let resolve;
const promise = new Promise(resolvePromise => (resolve = resolvePromise));
function Child({text}) {
if ((text === 'A' || text === 'D') && suspend) {
throw promise;
}
Scheduler.log(text);
return (
<span
onClick={e => {
e.preventDefault();
Scheduler.log('Clicked ' + text);
}}>
{text}
</span>
);
}
function App() {
Scheduler.log('App');
return (
<div>
<Activity>
<Child text="A" />
</Activity>
<Activity>
<Child text="B" />
</Activity>
<Activity>
<Child text="C" />
</Activity>
<Activity>
<Child text="D" />
</Activity>
</div>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['App', 'A', 'B', 'C', 'D']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
const spanA = container.getElementsByTagName('span')[0];
const spanC = container.getElementsByTagName('span')[2];
const spanD = container.getElementsByTagName('span')[3];
suspend = true;
ReactDOMClient.hydrateRoot(container, <App />);
assertLog([]);
dispatchClickEvent(spanA);
dispatchClickEvent(spanC);
dispatchClickEvent(spanD);
assertLog(['App', 'C', 'Clicked C']);
await act(async () => {
suspend = false;
resolve();
await promise;
});
assertLog([
'A',
'D',
'B',
]);
document.body.removeChild(container);
});
it('hydrates the target boundary synchronously during a click (createEventHandle)', async () => {
const setClick = ReactDOM.unstable_createEventHandle('click');
let isServerRendering = true;
function Child({text}) {
const ref = React.useRef(null);
Scheduler.log(text);
if (!isServerRendering) {
React.useLayoutEffect(() => {
return setClick(ref.current, () => {
Scheduler.log('Clicked ' + text);
});
});
}
return <span ref={ref}>{text}</span>;
}
function App() {
Scheduler.log('App');
return (
<div>
<Activity>
<Child text="A" />
</Activity>
<Activity>
<Child text="B" />
</Activity>
</div>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['App', 'A', 'B']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
isServerRendering = false;
ReactDOMClient.hydrateRoot(container, <App />);
assertLog([]);
const span = container.getElementsByTagName('span')[1];
const target = createEventTarget(span);
target.virtualclick();
assertLog(['App', 'B', 'Clicked B']);
await waitForAll(['A']);
document.body.removeChild(container);
});
it('hydrates at higher pri if sync did not work first time (createEventHandle)', async () => {
let suspend = false;
let isServerRendering = true;
let resolve;
const promise = new Promise(resolvePromise => (resolve = resolvePromise));
const setClick = ReactDOM.unstable_createEventHandle('click');
function Child({text}) {
const ref = React.useRef(null);
if ((text === 'A' || text === 'D') && suspend) {
throw promise;
}
Scheduler.log(text);
if (!isServerRendering) {
React.useLayoutEffect(() => {
return setClick(ref.current, () => {
Scheduler.log('Clicked ' + text);
});
});
}
return <span ref={ref}>{text}</span>;
}
function App() {
Scheduler.log('App');
return (
<div>
<Activity>
<Child text="A" />
</Activity>
<Activity>
<Child text="B" />
</Activity>
<Activity>
<Child text="C" />
</Activity>
<Activity>
<Child text="D" />
</Activity>
</div>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['App', 'A', 'B', 'C', 'D']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
const spanD = container.getElementsByTagName('span')[3];
suspend = true;
isServerRendering = false;
ReactDOMClient.hydrateRoot(container, <App />);
assertLog([]);
await act(() => {
const target = createEventTarget(spanD);
target.virtualclick();
});
assertLog(['App', 'B', 'C']);
await act(async () => {
suspend = false;
resolve();
await promise;
});
assertLog(['D', 'A']);
document.body.removeChild(container);
});
it('hydrates at higher pri for secondary discrete events (createEventHandle)', async () => {
const setClick = ReactDOM.unstable_createEventHandle('click');
let suspend = false;
let isServerRendering = true;
let resolve;
const promise = new Promise(resolvePromise => (resolve = resolvePromise));
function Child({text}) {
const ref = React.useRef(null);
if ((text === 'A' || text === 'D') && suspend) {
throw promise;
}
Scheduler.log(text);
if (!isServerRendering) {
React.useLayoutEffect(() => {
return setClick(ref.current, () => {
Scheduler.log('Clicked ' + text);
});
});
}
return <span ref={ref}>{text}</span>;
}
function App() {
Scheduler.log('App');
return (
<div>
<Activity>
<Child text="A" />
</Activity>
<Activity>
<Child text="B" />
</Activity>
<Activity>
<Child text="C" />
</Activity>
<Activity>
<Child text="D" />
</Activity>
</div>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['App', 'A', 'B', 'C', 'D']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
const spanA = container.getElementsByTagName('span')[0];
const spanC = container.getElementsByTagName('span')[2];
const spanD = container.getElementsByTagName('span')[3];
suspend = true;
isServerRendering = false;
ReactDOMClient.hydrateRoot(container, <App />);
assertLog([]);
createEventTarget(spanA).virtualclick();
createEventTarget(spanC).virtualclick();
createEventTarget(spanD).virtualclick();
assertLog(['App', 'C', 'Clicked C']);
await act(async () => {
suspend = false;
resolve();
await promise;
});
assertLog([
'A',
'D',
'B',
]);
document.body.removeChild(container);
});
it('hydrates the hovered targets as higher priority for continuous events', async () => {
let suspend = false;
let resolve;
const promise = new Promise(resolvePromise => (resolve = resolvePromise));
function Child({text}) {
if ((text === 'A' || text === 'D') && suspend) {
throw promise;
}
Scheduler.log(text);
return (
<span
onClick={e => {
e.preventDefault();
Scheduler.log('Clicked ' + text);
}}
onMouseEnter={e => {
e.preventDefault();
Scheduler.log('Hover ' + text);
}}>
{text}
</span>
);
}
function App() {
Scheduler.log('App');
return (
<div>
<Activity>
<Child text="A" />
</Activity>
<Activity>
<Child text="B" />
</Activity>
<Activity>
<Child text="C" />
</Activity>
<Activity>
<Child text="D" />
</Activity>
</div>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['App', 'A', 'B', 'C', 'D']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
const spanB = container.getElementsByTagName('span')[1];
const spanC = container.getElementsByTagName('span')[2];
const spanD = container.getElementsByTagName('span')[3];
suspend = true;
ReactDOMClient.hydrateRoot(container, <App />);
assertLog([]);
await act(() => {
dispatchMouseHoverEvent(spanD, null);
dispatchClickEvent(spanD);
dispatchMouseHoverEvent(spanB, spanD);
dispatchMouseHoverEvent(spanC, spanB);
assertLog(['App']);
suspend = false;
resolve();
});
assertLog([
'D',
'B',
'C',
'Hover C',
'A',
]);
document.body.removeChild(container);
});
it('replays capture phase for continuous events and respects stopPropagation', async () => {
let suspend = false;
let resolve;
const promise = new Promise(resolvePromise => (resolve = resolvePromise));
function Child({text}) {
if ((text === 'A' || text === 'D') && suspend) {
throw promise;
}
Scheduler.log(text);
return (
<span
id={text}
onClickCapture={e => {
e.preventDefault();
Scheduler.log('Capture Clicked ' + text);
}}
onClick={e => {
e.preventDefault();
Scheduler.log('Clicked ' + text);
}}
onMouseEnter={e => {
e.preventDefault();
Scheduler.log('Mouse Enter ' + text);
}}
onMouseOut={e => {
e.preventDefault();
Scheduler.log('Mouse Out ' + text);
}}
onMouseOutCapture={e => {
e.preventDefault();
e.stopPropagation();
Scheduler.log('Mouse Out Capture ' + text);
}}
onMouseOverCapture={e => {
e.preventDefault();
e.stopPropagation();
Scheduler.log('Mouse Over Capture ' + text);
}}
onMouseOver={e => {
e.preventDefault();
Scheduler.log('Mouse Over ' + text);
}}>
<div
onMouseOverCapture={e => {
e.preventDefault();
Scheduler.log('Mouse Over Capture Inner ' + text);
}}>
{text}
</div>
</span>
);
}
function App() {
Scheduler.log('App');
return (
<div
onClickCapture={e => {
e.preventDefault();
Scheduler.log('Capture Clicked Parent');
}}
onMouseOverCapture={e => {
Scheduler.log('Mouse Over Capture Parent');
}}>
<Activity>
<Child text="A" />
</Activity>
<Activity>
<Child text="B" />
</Activity>
<Activity>
<Child text="C" />
</Activity>
<Activity>
<Child text="D" />
</Activity>
</div>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['App', 'A', 'B', 'C', 'D']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
const spanB = document.getElementById('B').firstChild;
const spanC = document.getElementById('C').firstChild;
const spanD = document.getElementById('D').firstChild;
suspend = true;
ReactDOMClient.hydrateRoot(container, <App />);
assertLog([]);
await act(async () => {
dispatchMouseHoverEvent(spanD, null);
dispatchClickEvent(spanD);
dispatchMouseHoverEvent(spanB, spanD);
dispatchMouseHoverEvent(spanC, spanB);
assertLog(['App']);
suspend = false;
resolve();
});
assertLog([
'D',
'B',
'C',
'Mouse Over Capture Parent',
'Mouse Over Capture C',
'A',
]);
dispatchMouseHoverEvent(spanC, spanB);
assertLog([
'Mouse Out Capture B',
'Mouse Over Capture Parent',
'Mouse Over Capture C',
]);
document.body.removeChild(container);
});
it('replays event with null target when tree is dismounted', async () => {
let suspend = false;
let resolve;
const promise = new Promise(resolvePromise => {
resolve = () => {
suspend = false;
resolvePromise();
};
});
function Child() {
if (suspend) {
throw promise;
}
Scheduler.log('Child');
return (
<div
onMouseOver={() => {
Scheduler.log('on mouse over');
}}>
Child
</div>
);
}
function App() {
return (
<Activity>
<Child />
</Activity>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['Child']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
suspend = true;
ReactDOMClient.hydrateRoot(container, <App />);
const childDiv = container.firstElementChild;
await act(async () => {
dispatchMouseHoverEvent(childDiv);
assertLog([]);
resolve();
await waitFor(['Child']);
ReactDOM.flushSync(() => {
container.removeChild(childDiv);
const container2 = document.createElement('div');
container2.addEventListener('mouseover', () => {
Scheduler.log('container2 mouse over');
});
container2.appendChild(childDiv);
});
});
assertLog(['container2 mouse over']);
document.body.removeChild(container);
});
it('hydrates the last target path first for continuous events', async () => {
let suspend = false;
let resolve;
const promise = new Promise(resolvePromise => (resolve = resolvePromise));
function Child({text}) {
if ((text === 'A' || text === 'D') && suspend) {
throw promise;
}
Scheduler.log(text);
return (
<span
onMouseEnter={e => {
e.preventDefault();
Scheduler.log('Hover ' + text);
}}>
{text}
</span>
);
}
function App() {
Scheduler.log('App');
return (
<div>
<Activity>
<Child text="A" />
</Activity>
<Activity>
<div>
<Activity>
<Child text="B" />
</Activity>
</div>
<Child text="C" />
</Activity>
<Activity>
<Child text="D" />
</Activity>
</div>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['App', 'A', 'B', 'C', 'D']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
const spanB = container.getElementsByTagName('span')[1];
const spanC = container.getElementsByTagName('span')[2];
const spanD = container.getElementsByTagName('span')[3];
suspend = true;
ReactDOMClient.hydrateRoot(container, <App />);
assertLog([]);
dispatchMouseHoverEvent(spanB, spanD);
dispatchMouseHoverEvent(spanC, spanB);
await act(async () => {
suspend = false;
resolve();
await promise;
});
assertLog(['App', 'C', 'Hover C', 'A', 'B', 'D']);
document.body.removeChild(container);
});
it('hydrates the last explicitly hydrated target at higher priority', async () => {
function Child({text}) {
Scheduler.log(text);
return <span>{text}</span>;
}
function App() {
Scheduler.log('App');
return (
<div>
<Activity>
<Child text="A" />
</Activity>
<Activity>
<Child text="B" />
</Activity>
<Activity>
<Child text="C" />
</Activity>
</div>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['App', 'A', 'B', 'C']);
const container = document.createElement('div');
container.innerHTML = finalHTML;
const spanB = container.getElementsByTagName('span')[1];
const spanC = container.getElementsByTagName('span')[2];
const root = ReactDOMClient.hydrateRoot(container, <App />);
assertLog([]);
root.unstable_scheduleHydration(spanB);
root.unstable_scheduleHydration(spanC);
await waitForAll(['App', 'C', 'B', 'A']);
});
it('hydrates before an update even if hydration moves away from it', async () => {
function Child({text}) {
Scheduler.log(text);
return <span>{text}</span>;
}
const ChildWithBoundary = React.memo(function ({text}) {
return (
<Activity>
<Child text={text} />
<Child text={text.toLowerCase()} />
</Activity>
);
});
function App({a}) {
Scheduler.log('App');
React.useEffect(() => {
Scheduler.log('Commit');
});
return (
<div>
<ChildWithBoundary text={a} />
<ChildWithBoundary text="B" />
<ChildWithBoundary text="C" />
</div>
);
}
const finalHTML = ReactDOMServer.renderToString(<App a="A" />);
assertLog(['App', 'A', 'a', 'B', 'b', 'C', 'c']);
const container = document.createElement('div');
container.innerHTML = finalHTML;
document.body.appendChild(container);
const spanA = container.getElementsByTagName('span')[0];
const spanB = container.getElementsByTagName('span')[2];
const spanC = container.getElementsByTagName('span')[4];
await act(async () => {
const root = ReactDOMClient.hydrateRoot(container, <App a="A" />);
await waitFor(['App', 'Commit']);
TODO_scheduleIdleDOMSchedulerTask(() => {
root.render(<App a="AA" />);
});
await waitFor([
'App',
'A',
]);
dispatchMouseHoverEvent(spanA, null);
dispatchMouseHoverEvent(spanB, spanA);
dispatchClickEvent(spanC);
assertLog([
'C',
'c',
]);
await waitForAll([
'A',
'a',
'B',
'b',
'App',
'AA',
'aa',
'Commit',
]);
});
const spanA2 = container.getElementsByTagName('span')[0];
expect(spanA).toBe(spanA2);
document.body.removeChild(container);
});
it('fires capture event handlers and native events if content is hydratable during discrete event', async () => {
spyOnDev(console, 'error');
function Child({text}) {
Scheduler.log(text);
const ref = React.useRef();
React.useLayoutEffect(() => {
if (!ref.current) {
return;
}
ref.current.onclick = () => {
Scheduler.log('Native Click ' + text);
};
}, [text]);
return (
<span
ref={ref}
onClickCapture={() => {
Scheduler.log('Capture Clicked ' + text);
}}
onClick={e => {
Scheduler.log('Clicked ' + text);
}}>
{text}
</span>
);
}
function App() {
Scheduler.log('App');
return (
<div>
<Activity>
<Child text="A" />
</Activity>
<Activity>
<Child text="B" />
</Activity>
</div>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['App', 'A', 'B']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[1];
ReactDOMClient.hydrateRoot(container, <App />);
assertLog([]);
dispatchClickEvent(span);
assertLog(['App', 'B', 'Capture Clicked B', 'Native Click B', 'Clicked B']);
await waitForAll(['A']);
document.body.removeChild(container);
});
it('does not propagate discrete event if it cannot be synchronously hydrated', async () => {
let triggeredParent = false;
let triggeredChild = false;
let suspend = false;
const promise = new Promise(() => {});
function Child() {
if (suspend) {
throw promise;
}
Scheduler.log('Child');
return (
<span
onClickCapture={e => {
e.stopPropagation();
triggeredChild = true;
}}>
Click me
</span>
);
}
function App() {
const onClick = () => {
triggeredParent = true;
};
Scheduler.log('App');
return (
<div
ref={n => {
if (n) n.onclick = onClick;
}}
onClick={onClick}>
<Activity>
<Child />
</Activity>
</div>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['App', 'Child']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
suspend = true;
ReactDOMClient.hydrateRoot(container, <App />);
assertLog([]);
const span = container.getElementsByTagName('span')[0];
dispatchClickEvent(span);
assertLog(['App']);
dispatchClickEvent(span);
expect(triggeredParent).toBe(false);
expect(triggeredChild).toBe(false);
});
it('can force hydration in response to sync update', async () => {
function Child({text}) {
Scheduler.log(`Child ${text}`);
return <span ref={ref => (spanRef = ref)}>{text}</span>;
}
function App({text}) {
Scheduler.log(`App ${text}`);
return (
<div>
<Activity>
<Child text={text} />
</Activity>
</div>
);
}
let spanRef;
const finalHTML = ReactDOMServer.renderToString(<App text="A" />);
assertLog(['App A', 'Child A']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
const initialSpan = container.getElementsByTagName('span')[0];
const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
await waitForPaint(['App A']);
await act(() => {
ReactDOM.flushSync(() => {
root.render(<App text="B" />);
});
});
assertLog(['App B', 'Child A', 'App B', 'Child B']);
expect(initialSpan).toBe(spanRef);
});
it('can force hydration in response to continuous update', async () => {
function Child({text}) {
Scheduler.log(`Child ${text}`);
return <span ref={ref => (spanRef = ref)}>{text}</span>;
}
function App({text}) {
Scheduler.log(`App ${text}`);
return (
<div>
<Activity>
<Child text={text} />
</Activity>
</div>
);
}
let spanRef;
const finalHTML = ReactDOMServer.renderToString(<App text="A" />);
assertLog(['App A', 'Child A']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
const initialSpan = container.getElementsByTagName('span')[0];
const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
await waitForPaint(['App A']);
await act(() => {
TODO_scheduleContinuousSchedulerTask(() => {
root.render(<App text="B" />);
});
});
assertLog(['App B', 'Child A', 'App B', 'Child B']);
expect(initialSpan).toBe(spanRef);
});
it('can force hydration in response to default update', async () => {
function Child({text}) {
Scheduler.log(`Child ${text}`);
return <span ref={ref => (spanRef = ref)}>{text}</span>;
}
function App({text}) {
Scheduler.log(`App ${text}`);
return (
<div>
<Activity>
<Child text={text} />
</Activity>
</div>
);
}
let spanRef;
const finalHTML = ReactDOMServer.renderToString(<App text="A" />);
assertLog(['App A', 'Child A']);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
const initialSpan = container.getElementsByTagName('span')[0];
const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
await waitForPaint(['App A']);
await act(() => {
root.render(<App text="B" />);
});
assertLog(['App B', 'Child A', 'App B', 'Child B']);
expect(initialSpan).toBe(spanRef);
});
it('regression test: can unwind context on selective hydration interruption', async () => {
const Context = React.createContext('DefaultContext');
function ContextReader(props) {
const value = React.useContext(Context);
Scheduler.log(value);
return null;
}
function Child({text}) {
Scheduler.log(text);
return <span>{text}</span>;
}
const ChildWithBoundary = React.memo(function ({text}) {
return (
<Activity>
<Child text={text} />
</Activity>
);
});
function App({a}) {
Scheduler.log('App');
React.useEffect(() => {
Scheduler.log('Commit');
});
return (
<>
<Context.Provider value="SiblingContext">
<ChildWithBoundary text={a} />
</Context.Provider>
<ContextReader />
</>
);
}
const finalHTML = ReactDOMServer.renderToString(<App a="A" />);
assertLog(['App', 'A', 'DefaultContext']);
const container = document.createElement('div');
container.innerHTML = finalHTML;
document.body.appendChild(container);
const spanA = container.getElementsByTagName('span')[0];
await act(async () => {
const root = ReactDOMClient.hydrateRoot(container, <App a="A" />);
await waitFor(['App', 'DefaultContext', 'Commit']);
TODO_scheduleIdleDOMSchedulerTask(() => {
root.render(<App a="AA" />);
});
await waitFor(['App', 'A']);
dispatchClickEvent(spanA);
assertLog(['A']);
await waitForAll(['App', 'AA', 'DefaultContext', 'Commit']);
});
});
it('regression test: can unwind context on selective hydration interruption for sync updates', async () => {
const Context = React.createContext('DefaultContext');
function ContextReader(props) {
const value = React.useContext(Context);
Scheduler.log(value);
return null;
}
function Child({text}) {
Scheduler.log(text);
return <span>{text}</span>;
}
const ChildWithBoundary = React.memo(function ({text}) {
return (
<Activity>
<Child text={text} />
</Activity>
);
});
function App({a}) {
Scheduler.log('App');
React.useEffect(() => {
Scheduler.log('Commit');
});
return (
<>
<Context.Provider value="SiblingContext">
<ChildWithBoundary text={a} />
</Context.Provider>
<ContextReader />
</>
);
}
const finalHTML = ReactDOMServer.renderToString(<App a="A" />);
assertLog(['App', 'A', 'DefaultContext']);
const container = document.createElement('div');
container.innerHTML = finalHTML;
await act(async () => {
const root = ReactDOMClient.hydrateRoot(container, <App a="A" />);
await waitFor(['App', 'DefaultContext', 'Commit']);
ReactDOM.flushSync(() => {
root.render(<App a="AA" />);
});
assertLog(['App', 'A', 'App', 'AA', 'DefaultContext', 'Commit']);
});
});
it('regression: selective hydration does not contribute to "maximum update limit" count', async () => {
const outsideRef = React.createRef(null);
const insideRef = React.createRef(null);
function Child() {
return (
<Activity>
<div ref={insideRef} />
</Activity>
);
}
let setIsMounted = false;
function App() {
const [isMounted, setState] = React.useState(false);
setIsMounted = setState;
const children = [];
for (let i = 0; i < 100; i++) {
children.push(<Child key={i} isMounted={isMounted} />);
}
return <div ref={outsideRef}>{children}</div>;
}
const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
await act(async () => {
ReactDOMClient.hydrateRoot(container, <App />);
await waitForPaint([]);
expect(outsideRef.current).not.toBe(null);
expect(insideRef.current).toBe(null);
ReactDOM.flushSync(() => {
setIsMounted(true);
});
});
expect(insideRef.current).not.toBe(null);
});
});