mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
In the next major release, we intend to drop support for using the `act` testing helper in production. (It already fires a warning.) The rationale is that, in order for `act` to work, you must either mock the testing environment or add extra logic at runtime. Mocking the testing environment isn't ideal because it requires extra set up for the user. Extra logic at runtime is fine only in development mode — we don't want to slow down the production builds. Since most people only run their tests in development mode, dropping support for production should be fine; if there's demand, we can add it back later using a special testing build that is identical to the production build except for the additional testing logic. One blocker for removing production support is that we currently use `act` to test React itself. We must test React in both development and production modes. So, the solution is to fork `act` into separate public and internal implementations: - *public implementation of `act`* – exposed to users, only works in development mode, uses special runtime logic, does not support partial rendering - *internal implementation of `act`* – private, works in both development and productionm modes, only used by the React Core test suite, uses no special runtime logic, supports partial rendering (i.e. `toFlushAndYieldThrough`) The internal implementation should mostly match the public implementation's behavior, but since it's a private API, it doesn't have to match exactly. It works by mocking the test environment: it uses a mock build of Scheduler to flush rendering tasks, and Jest's mock timers to flush Suspense placeholders. --- In this first commit, I've added the internal forks of `act` and migrated our tests to use them. The public `act` implementation is unaffected for now; I will leave refactoring/clean-up for a later step.
955 lines
26 KiB
JavaScript
955 lines
26 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its 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 {createEventTarget} from 'dom-event-testing-library';
|
|
|
|
let React;
|
|
let ReactDOM;
|
|
let ReactDOMServer;
|
|
let ReactTestUtils;
|
|
let Scheduler;
|
|
let Suspense;
|
|
let act;
|
|
|
|
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);
|
|
}
|
|
|
|
describe('ReactDOMServerSelectiveHydration', () => {
|
|
beforeEach(() => {
|
|
jest.resetModuleRegistry();
|
|
|
|
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
|
ReactFeatureFlags.enableCreateEventHandleAPI = true;
|
|
React = require('react');
|
|
ReactDOM = require('react-dom');
|
|
ReactDOMServer = require('react-dom/server');
|
|
ReactTestUtils = require('react-dom/test-utils');
|
|
act = ReactTestUtils.unstable_concurrentAct;
|
|
Scheduler = require('scheduler');
|
|
Suspense = React.Suspense;
|
|
});
|
|
|
|
// @gate experimental
|
|
it('hydrates the target boundary synchronously during a click', async () => {
|
|
function Child({text}) {
|
|
Scheduler.unstable_yieldValue(text);
|
|
return (
|
|
<span
|
|
onClick={e => {
|
|
e.preventDefault();
|
|
Scheduler.unstable_yieldValue('Clicked ' + text);
|
|
}}>
|
|
{text}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function App() {
|
|
Scheduler.unstable_yieldValue('App');
|
|
return (
|
|
<div>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="A" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="B" />
|
|
</Suspense>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const finalHTML = ReactDOMServer.renderToString(<App />);
|
|
|
|
expect(Scheduler).toHaveYielded(['App', 'A', 'B']);
|
|
|
|
const container = document.createElement('div');
|
|
// We need this to be in the document since we'll dispatch events on it.
|
|
document.body.appendChild(container);
|
|
|
|
container.innerHTML = finalHTML;
|
|
|
|
const span = container.getElementsByTagName('span')[1];
|
|
|
|
const root = ReactDOM.createRoot(container, {hydrate: true});
|
|
root.render(<App />);
|
|
|
|
// Nothing has been hydrated so far.
|
|
expect(Scheduler).toHaveYielded([]);
|
|
|
|
// This should synchronously hydrate the root App and the second suspense
|
|
// boundary.
|
|
const result = dispatchClickEvent(span);
|
|
|
|
// The event should have been canceled because we called preventDefault.
|
|
expect(result).toBe(false);
|
|
|
|
// We rendered App, B and then invoked the event without rendering A.
|
|
expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']);
|
|
|
|
// After continuing the scheduler, we finally hydrate A.
|
|
expect(Scheduler).toFlushAndYield(['A']);
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
// @gate experimental
|
|
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.unstable_yieldValue(text);
|
|
return (
|
|
<span
|
|
onClick={e => {
|
|
e.preventDefault();
|
|
Scheduler.unstable_yieldValue('Clicked ' + text);
|
|
}}>
|
|
{text}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function App() {
|
|
Scheduler.unstable_yieldValue('App');
|
|
return (
|
|
<div>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="A" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="B" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="C" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="D" />
|
|
</Suspense>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const finalHTML = ReactDOMServer.renderToString(<App />);
|
|
|
|
expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
|
|
|
|
const container = document.createElement('div');
|
|
// We need this to be in the document since we'll dispatch events on it.
|
|
document.body.appendChild(container);
|
|
|
|
container.innerHTML = finalHTML;
|
|
|
|
const spanD = container.getElementsByTagName('span')[3];
|
|
|
|
suspend = true;
|
|
|
|
// A and D will be suspended. We'll click on D which should take
|
|
// priority, after we unsuspend.
|
|
const root = ReactDOM.createRoot(container, {hydrate: true});
|
|
root.render(<App />);
|
|
|
|
// Nothing has been hydrated so far.
|
|
expect(Scheduler).toHaveYielded([]);
|
|
|
|
// This click target cannot be hydrated yet because it's suspended.
|
|
const result = dispatchClickEvent(spanD);
|
|
|
|
expect(Scheduler).toHaveYielded(['App']);
|
|
|
|
expect(result).toBe(true);
|
|
|
|
// Continuing rendering will render B next.
|
|
expect(Scheduler).toFlushAndYield(['B', 'C']);
|
|
|
|
suspend = false;
|
|
resolve();
|
|
await promise;
|
|
|
|
// After the click, we should prioritize D and the Click first,
|
|
// and only after that render A and C.
|
|
expect(Scheduler).toFlushAndYield(['D', 'Clicked D', 'A']);
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
// @gate experimental
|
|
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.unstable_yieldValue(text);
|
|
return (
|
|
<span
|
|
onClick={e => {
|
|
e.preventDefault();
|
|
Scheduler.unstable_yieldValue('Clicked ' + text);
|
|
}}>
|
|
{text}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function App() {
|
|
Scheduler.unstable_yieldValue('App');
|
|
return (
|
|
<div>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="A" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="B" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="C" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="D" />
|
|
</Suspense>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const finalHTML = ReactDOMServer.renderToString(<App />);
|
|
|
|
expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
|
|
|
|
const container = document.createElement('div');
|
|
// We need this to be in the document since we'll dispatch events on it.
|
|
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;
|
|
|
|
// A and D will be suspended. We'll click on D which should take
|
|
// priority, after we unsuspend.
|
|
const root = ReactDOM.createRoot(container, {hydrate: true});
|
|
root.render(<App />);
|
|
|
|
// Nothing has been hydrated so far.
|
|
expect(Scheduler).toHaveYielded([]);
|
|
|
|
// This click target cannot be hydrated yet because the first is Suspended.
|
|
dispatchClickEvent(spanA);
|
|
dispatchClickEvent(spanC);
|
|
dispatchClickEvent(spanD);
|
|
|
|
expect(Scheduler).toHaveYielded(['App']);
|
|
|
|
suspend = false;
|
|
resolve();
|
|
await promise;
|
|
|
|
// We should prioritize hydrating A, C and D first since we clicked in
|
|
// them. Only after they're done will we hydrate B.
|
|
expect(Scheduler).toFlushAndYield([
|
|
'A',
|
|
'Clicked A',
|
|
'C',
|
|
'Clicked C',
|
|
'D',
|
|
'Clicked D',
|
|
// B should render last since it wasn't clicked.
|
|
'B',
|
|
]);
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
// @gate experimental
|
|
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.unstable_yieldValue(text);
|
|
if (!isServerRendering) {
|
|
React.useLayoutEffect(() => {
|
|
return setClick(ref.current, () => {
|
|
Scheduler.unstable_yieldValue('Clicked ' + text);
|
|
});
|
|
});
|
|
}
|
|
|
|
return <span ref={ref}>{text}</span>;
|
|
}
|
|
|
|
function App() {
|
|
Scheduler.unstable_yieldValue('App');
|
|
return (
|
|
<div>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="A" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="B" />
|
|
</Suspense>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const finalHTML = ReactDOMServer.renderToString(<App />);
|
|
|
|
expect(Scheduler).toHaveYielded(['App', 'A', 'B']);
|
|
|
|
const container = document.createElement('div');
|
|
// We need this to be in the document since we'll dispatch events on it.
|
|
document.body.appendChild(container);
|
|
|
|
container.innerHTML = finalHTML;
|
|
|
|
isServerRendering = false;
|
|
|
|
const root = ReactDOM.createRoot(container, {hydrate: true});
|
|
|
|
root.render(<App />);
|
|
|
|
// Nothing has been hydrated so far.
|
|
expect(Scheduler).toHaveYielded([]);
|
|
|
|
const span = container.getElementsByTagName('span')[1];
|
|
|
|
const target = createEventTarget(span);
|
|
|
|
// This should synchronously hydrate the root App and the second suspense
|
|
// boundary.
|
|
target.virtualclick();
|
|
|
|
// We rendered App, B and then invoked the event without rendering A.
|
|
expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']);
|
|
|
|
// After continuing the scheduler, we finally hydrate A.
|
|
expect(Scheduler).toFlushAndYield(['A']);
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
// @gate experimental
|
|
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.unstable_yieldValue(text);
|
|
|
|
if (!isServerRendering) {
|
|
React.useLayoutEffect(() => {
|
|
return setClick(ref.current, () => {
|
|
Scheduler.unstable_yieldValue('Clicked ' + text);
|
|
});
|
|
});
|
|
}
|
|
|
|
return <span ref={ref}>{text}</span>;
|
|
}
|
|
|
|
function App() {
|
|
Scheduler.unstable_yieldValue('App');
|
|
return (
|
|
<div>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="A" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="B" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="C" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="D" />
|
|
</Suspense>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const finalHTML = ReactDOMServer.renderToString(<App />);
|
|
|
|
expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
|
|
|
|
const container = document.createElement('div');
|
|
// We need this to be in the document since we'll dispatch events on it.
|
|
document.body.appendChild(container);
|
|
|
|
container.innerHTML = finalHTML;
|
|
|
|
const spanD = container.getElementsByTagName('span')[3];
|
|
|
|
suspend = true;
|
|
isServerRendering = false;
|
|
|
|
// A and D will be suspended. We'll click on D which should take
|
|
// priority, after we unsuspend.
|
|
const root = ReactDOM.createRoot(container, {hydrate: true});
|
|
root.render(<App />);
|
|
|
|
// Nothing has been hydrated so far.
|
|
expect(Scheduler).toHaveYielded([]);
|
|
|
|
const target = createEventTarget(spanD);
|
|
target.virtualclick();
|
|
|
|
expect(Scheduler).toHaveYielded(['App']);
|
|
|
|
// Continuing rendering will render B next.
|
|
expect(Scheduler).toFlushAndYield(['B', 'C']);
|
|
|
|
suspend = false;
|
|
resolve();
|
|
await promise;
|
|
|
|
// After the click, we should prioritize D and the Click first,
|
|
// and only after that render A and C.
|
|
expect(Scheduler).toFlushAndYield(['D', 'Clicked D', 'A']);
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
// @gate experimental
|
|
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.unstable_yieldValue(text);
|
|
|
|
if (!isServerRendering) {
|
|
React.useLayoutEffect(() => {
|
|
return setClick(ref.current, () => {
|
|
Scheduler.unstable_yieldValue('Clicked ' + text);
|
|
});
|
|
});
|
|
}
|
|
return <span ref={ref}>{text}</span>;
|
|
}
|
|
|
|
function App() {
|
|
Scheduler.unstable_yieldValue('App');
|
|
return (
|
|
<div>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="A" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="B" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="C" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="D" />
|
|
</Suspense>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const finalHTML = ReactDOMServer.renderToString(<App />);
|
|
|
|
expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
|
|
|
|
const container = document.createElement('div');
|
|
// We need this to be in the document since we'll dispatch events on it.
|
|
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;
|
|
|
|
// A and D will be suspended. We'll click on D which should take
|
|
// priority, after we unsuspend.
|
|
const root = ReactDOM.createRoot(container, {hydrate: true});
|
|
root.render(<App />);
|
|
|
|
// Nothing has been hydrated so far.
|
|
expect(Scheduler).toHaveYielded([]);
|
|
|
|
// This click target cannot be hydrated yet because the first is Suspended.
|
|
createEventTarget(spanA).virtualclick();
|
|
createEventTarget(spanC).virtualclick();
|
|
createEventTarget(spanD).virtualclick();
|
|
|
|
expect(Scheduler).toHaveYielded(['App']);
|
|
|
|
suspend = false;
|
|
resolve();
|
|
await promise;
|
|
|
|
// We should prioritize hydrating A, C and D first since we clicked in
|
|
// them. Only after they're done will we hydrate B.
|
|
expect(Scheduler).toFlushAndYield([
|
|
'A',
|
|
'Clicked A',
|
|
'C',
|
|
'Clicked C',
|
|
'D',
|
|
'Clicked D',
|
|
// B should render last since it wasn't clicked.
|
|
'B',
|
|
]);
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
// @gate experimental
|
|
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.unstable_yieldValue(text);
|
|
return (
|
|
<span
|
|
onClick={e => {
|
|
e.preventDefault();
|
|
Scheduler.unstable_yieldValue('Clicked ' + text);
|
|
}}
|
|
onMouseEnter={e => {
|
|
e.preventDefault();
|
|
Scheduler.unstable_yieldValue('Hover ' + text);
|
|
}}>
|
|
{text}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function App() {
|
|
Scheduler.unstable_yieldValue('App');
|
|
return (
|
|
<div>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="A" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="B" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="C" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="D" />
|
|
</Suspense>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const finalHTML = ReactDOMServer.renderToString(<App />);
|
|
|
|
expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
|
|
|
|
const container = document.createElement('div');
|
|
// We need this to be in the document since we'll dispatch events on it.
|
|
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;
|
|
|
|
// A and D will be suspended. We'll click on D which should take
|
|
// priority, after we unsuspend.
|
|
const root = ReactDOM.createRoot(container, {hydrate: true});
|
|
root.render(<App />);
|
|
|
|
// Nothing has been hydrated so far.
|
|
expect(Scheduler).toHaveYielded([]);
|
|
|
|
// Click D
|
|
dispatchMouseHoverEvent(spanD, null);
|
|
dispatchClickEvent(spanD);
|
|
// Hover over B and then C.
|
|
dispatchMouseHoverEvent(spanB, spanD);
|
|
dispatchMouseHoverEvent(spanC, spanB);
|
|
|
|
expect(Scheduler).toHaveYielded(['App']);
|
|
|
|
suspend = false;
|
|
resolve();
|
|
await promise;
|
|
|
|
// We should prioritize hydrating D first because we clicked it.
|
|
// Next we should hydrate C since that's the current hover target.
|
|
// To simplify implementation details we hydrate both B and C at
|
|
// the same time since B was already scheduled.
|
|
// This is ok because it will at least not continue for nested
|
|
// boundary. See the next test below.
|
|
expect(Scheduler).toFlushAndYield([
|
|
'D',
|
|
'Clicked D',
|
|
'B', // Ideally this should be later.
|
|
'C',
|
|
'Hover C',
|
|
'A',
|
|
]);
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
// @gate experimental
|
|
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.unstable_yieldValue(text);
|
|
return (
|
|
<span
|
|
onMouseEnter={e => {
|
|
e.preventDefault();
|
|
Scheduler.unstable_yieldValue('Hover ' + text);
|
|
}}>
|
|
{text}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function App() {
|
|
Scheduler.unstable_yieldValue('App');
|
|
return (
|
|
<div>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="A" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<div>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="B" />
|
|
</Suspense>
|
|
</div>
|
|
<Child text="C" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="D" />
|
|
</Suspense>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const finalHTML = ReactDOMServer.renderToString(<App />);
|
|
|
|
expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
|
|
|
|
const container = document.createElement('div');
|
|
// We need this to be in the document since we'll dispatch events on it.
|
|
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;
|
|
|
|
// A and D will be suspended. We'll click on D which should take
|
|
// priority, after we unsuspend.
|
|
const root = ReactDOM.createRoot(container, {hydrate: true});
|
|
root.render(<App />);
|
|
|
|
// Nothing has been hydrated so far.
|
|
expect(Scheduler).toHaveYielded([]);
|
|
|
|
// Hover over B and then C.
|
|
dispatchMouseHoverEvent(spanB, spanD);
|
|
dispatchMouseHoverEvent(spanC, spanB);
|
|
|
|
suspend = false;
|
|
resolve();
|
|
await promise;
|
|
|
|
// We should prioritize hydrating D first because we clicked it.
|
|
// Next we should hydrate C since that's the current hover target.
|
|
// Next it doesn't matter if we hydrate A or B first but as an
|
|
// implementation detail we're currently hydrating B first since
|
|
// we at one point hovered over it and we never deprioritized it.
|
|
expect(Scheduler).toFlushAndYield(['App', 'C', 'Hover C', 'A', 'B', 'D']);
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
// @gate experimental
|
|
it('hydrates the last explicitly hydrated target at higher priority', async () => {
|
|
function Child({text}) {
|
|
Scheduler.unstable_yieldValue(text);
|
|
return <span>{text}</span>;
|
|
}
|
|
|
|
function App() {
|
|
Scheduler.unstable_yieldValue('App');
|
|
return (
|
|
<div>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="A" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="B" />
|
|
</Suspense>
|
|
<Suspense fallback="Loading...">
|
|
<Child text="C" />
|
|
</Suspense>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const finalHTML = ReactDOMServer.renderToString(<App />);
|
|
|
|
expect(Scheduler).toHaveYielded(['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 = ReactDOM.createRoot(container, {hydrate: true});
|
|
root.render(<App />);
|
|
|
|
// Nothing has been hydrated so far.
|
|
expect(Scheduler).toHaveYielded([]);
|
|
|
|
// Increase priority of B and then C.
|
|
ReactDOM.unstable_scheduleHydration(spanB);
|
|
ReactDOM.unstable_scheduleHydration(spanC);
|
|
|
|
// We should prioritize hydrating C first because the last added
|
|
// gets highest priority followed by the next added.
|
|
expect(Scheduler).toFlushAndYield(['App', 'C', 'B', 'A']);
|
|
});
|
|
|
|
// @gate experimental
|
|
it('hydrates before an update even if hydration moves away from it', async () => {
|
|
function Child({text}) {
|
|
Scheduler.unstable_yieldValue(text);
|
|
return <span>{text}</span>;
|
|
}
|
|
const ChildWithBoundary = React.memo(function({text}) {
|
|
return (
|
|
<Suspense fallback="Loading...">
|
|
<Child text={text} />
|
|
<Child text={text.toLowerCase()} />
|
|
</Suspense>
|
|
);
|
|
});
|
|
|
|
function App({a}) {
|
|
Scheduler.unstable_yieldValue('App');
|
|
React.useEffect(() => {
|
|
Scheduler.unstable_yieldValue('Commit');
|
|
});
|
|
return (
|
|
<div>
|
|
<ChildWithBoundary text={a} />
|
|
<ChildWithBoundary text="B" />
|
|
<ChildWithBoundary text="C" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const finalHTML = ReactDOMServer.renderToString(<App a="A" />);
|
|
|
|
expect(Scheduler).toHaveYielded(['App', 'A', 'a', 'B', 'b', 'C', 'c']);
|
|
|
|
const container = document.createElement('div');
|
|
container.innerHTML = finalHTML;
|
|
|
|
// We need this to be in the document since we'll dispatch events on it.
|
|
document.body.appendChild(container);
|
|
|
|
const spanA = container.getElementsByTagName('span')[0];
|
|
const spanB = container.getElementsByTagName('span')[2];
|
|
const spanC = container.getElementsByTagName('span')[4];
|
|
|
|
const root = ReactDOM.createRoot(container, {hydrate: true});
|
|
act(() => {
|
|
root.render(<App a="A" />);
|
|
|
|
// Hydrate the shell.
|
|
expect(Scheduler).toFlushAndYieldThrough(['App', 'Commit']);
|
|
|
|
// Render an update at Idle priority that needs to update A.
|
|
Scheduler.unstable_runWithPriority(
|
|
Scheduler.unstable_IdlePriority,
|
|
() => {
|
|
root.render(<App a="AA" />);
|
|
},
|
|
);
|
|
|
|
// Start rendering. This will force the first boundary to hydrate
|
|
// by scheduling it at one higher pri than Idle.
|
|
expect(Scheduler).toFlushAndYieldThrough([
|
|
// An update was scheduled to force hydrate the boundary, but React will
|
|
// continue rendering at Idle until the next time React yields. This is
|
|
// fine though because it will switch to the hydration level when it
|
|
// re-enters the work loop.
|
|
'App',
|
|
'AA',
|
|
]);
|
|
|
|
// Hover over A which (could) schedule at one higher pri than Idle.
|
|
dispatchMouseHoverEvent(spanA, null);
|
|
|
|
// Before, we're done we now switch to hover over B.
|
|
// This is meant to test that this doesn't cause us to forget that
|
|
// we still have to hydrate A. The first boundary.
|
|
// This also tests that we don't do the -1 down-prioritization of
|
|
// continuous hover events because that would decrease its priority
|
|
// to Idle.
|
|
dispatchMouseHoverEvent(spanB, spanA);
|
|
|
|
// Also click C to prioritize that even higher which resets the
|
|
// priority levels.
|
|
dispatchClickEvent(spanC);
|
|
|
|
expect(Scheduler).toHaveYielded([
|
|
// Hydrate C first since we clicked it.
|
|
'C',
|
|
'c',
|
|
]);
|
|
|
|
expect(Scheduler).toFlushAndYield([
|
|
// Finish hydration of A since we forced it to hydrate.
|
|
'A',
|
|
'a',
|
|
// Also, hydrate B since we hovered over it.
|
|
// It's not important which one comes first. A or B.
|
|
// As long as they both happen before the Idle update.
|
|
'B',
|
|
'b',
|
|
// Begin the Idle update again.
|
|
'App',
|
|
'AA',
|
|
'aa',
|
|
'Commit',
|
|
]);
|
|
});
|
|
|
|
const spanA2 = container.getElementsByTagName('span')[0];
|
|
// This is supposed to have been hydrated, not replaced.
|
|
expect(spanA).toBe(spanA2);
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
});
|