/**
* 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';
let React = require('react');
let ReactDOM = require('react-dom');
let ReactDOMServer = require('react-dom/server');
let Scheduler = require('scheduler');
describe('ReactDOMRoot', () => {
let container;
beforeEach(() => {
jest.resetModules();
container = document.createElement('div');
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
Scheduler = require('scheduler');
});
if (!__EXPERIMENTAL__) {
it('createRoot is not exposed in stable build', () => {
expect(ReactDOM.unstable_createRoot).toBe(undefined);
});
return;
}
it('renders children', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(
Hi
);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('Hi');
});
it('warns if a callback parameter is provided to render', () => {
const callback = jest.fn();
const root = ReactDOM.unstable_createRoot(container);
expect(() =>
root.render(Hi
, callback),
).toErrorDev(
'render(...): does not support the second callback argument. ' +
'To execute a side effect after rendering, declare it in a component body with useEffect().',
{withoutStack: true},
);
Scheduler.unstable_flushAll();
expect(callback).not.toHaveBeenCalled();
});
it('warns if a callback parameter is provided to unmount', () => {
const callback = jest.fn();
const root = ReactDOM.unstable_createRoot(container);
root.render(Hi
);
expect(() =>
root.unmount(callback),
).toErrorDev(
'unmount(...): does not support a callback argument. ' +
'To execute a side effect after rendering, declare it in a component body with useEffect().',
{withoutStack: true},
);
Scheduler.unstable_flushAll();
expect(callback).not.toHaveBeenCalled();
});
it('unmounts children', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(Hi
);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('Hi');
root.unmount();
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('');
});
it('supports hydration', async () => {
const markup = await new Promise(resolve =>
resolve(
ReactDOMServer.renderToString(
,
),
),
);
// Does not hydrate by default
const container1 = document.createElement('div');
container1.innerHTML = markup;
const root1 = ReactDOM.unstable_createRoot(container1);
root1.render(
,
);
Scheduler.unstable_flushAll();
// Accepts `hydrate` option
const container2 = document.createElement('div');
container2.innerHTML = markup;
const root2 = ReactDOM.unstable_createRoot(container2, {hydrate: true});
root2.render(
,
);
expect(() => Scheduler.unstable_flushAll()).toErrorDev('Extra attributes');
});
it('clears existing children with legacy API', async () => {
container.innerHTML = 'a
b
';
ReactDOM.render(
c
d
,
container,
);
expect(container.textContent).toEqual('cd');
ReactDOM.render(
d
c
,
container,
);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('dc');
});
it('clears existing children', async () => {
container.innerHTML = 'a
b
';
const root = ReactDOM.unstable_createRoot(container);
root.render(
c
d
,
);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('cd');
root.render(
d
c
,
);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('dc');
});
it('throws a good message on invalid containers', () => {
expect(() => {
ReactDOM.unstable_createRoot(Hi
);
}).toThrow('createRoot(...): Target container is not a DOM element.');
});
it('warns when rendering with legacy API into createRoot() container', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(Hi
);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('Hi');
expect(() => {
ReactDOM.render(Bye
, container);
}).toErrorDev(
[
// We care about this warning:
'You are calling ReactDOM.render() on a container that was previously ' +
'passed to ReactDOM.createRoot(). This is not supported. ' +
'Did you mean to call root.render(element)?',
// This is more of a symptom but restructuring the code to avoid it isn't worth it:
'Replacing React-rendered children with a new root component.',
],
{withoutStack: true},
);
Scheduler.unstable_flushAll();
// This works now but we could disallow it:
expect(container.textContent).toEqual('Bye');
});
it('warns when hydrating with legacy API into createRoot() container', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(Hi
);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('Hi');
expect(() => {
ReactDOM.hydrate(Hi
, container);
}).toErrorDev(
[
// We care about this warning:
'You are calling ReactDOM.hydrate() on a container that was previously ' +
'passed to ReactDOM.createRoot(). This is not supported. ' +
'Did you mean to call createRoot(container, {hydrate: true}).render(element)?',
// This is more of a symptom but restructuring the code to avoid it isn't worth it:
'Replacing React-rendered children with a new root component.',
],
{withoutStack: true},
);
});
it('warns when unmounting with legacy API (no previous content)', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(Hi
);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('Hi');
let unmounted = false;
expect(() => {
unmounted = ReactDOM.unmountComponentAtNode(container);
}).toErrorDev(
[
// We care about this warning:
'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
'passed to ReactDOM.createRoot(). This is not supported. Did you mean to call root.unmount()?',
// This is more of a symptom but restructuring the code to avoid it isn't worth it:
"The node you're attempting to unmount was rendered by React and is not a top-level container.",
],
{withoutStack: true},
);
expect(unmounted).toBe(false);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('Hi');
root.unmount();
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('');
});
it('warns when unmounting with legacy API (has previous content)', () => {
// Currently createRoot().render() doesn't clear this.
container.appendChild(document.createElement('div'));
// The rest is the same as test above.
const root = ReactDOM.unstable_createRoot(container);
root.render(Hi
);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('Hi');
let unmounted = false;
expect(() => {
unmounted = ReactDOM.unmountComponentAtNode(container);
}).toErrorDev(
[
'Did you mean to call root.unmount()?',
// This is more of a symptom but restructuring the code to avoid it isn't worth it:
"The node you're attempting to unmount was rendered by React and is not a top-level container.",
],
{withoutStack: true},
);
expect(unmounted).toBe(false);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('Hi');
root.unmount();
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('');
});
it('warns when passing legacy container to createRoot()', () => {
ReactDOM.render(Hi
, container);
expect(() => {
ReactDOM.unstable_createRoot(container);
}).toErrorDev(
'You are calling ReactDOM.createRoot() on a container that was previously ' +
'passed to ReactDOM.render(). This is not supported.',
{withoutStack: true},
);
});
it('warns when creating two roots managing the same container', () => {
ReactDOM.unstable_createRoot(container);
expect(() => {
ReactDOM.unstable_createRoot(container);
}).toErrorDev(
'You are calling ReactDOM.createRoot() on a container that ' +
'has already been passed to createRoot() before. Instead, call ' +
'root.render() on the existing root instead if you want to update it.',
{withoutStack: true},
);
});
it('does not warn when creating second root after first one is unmounted', () => {
const root = ReactDOM.unstable_createRoot(container);
root.unmount();
Scheduler.unstable_flushAll();
ReactDOM.unstable_createRoot(container); // No warning
});
it('warns if creating a root on the document.body', async () => {
expect(() => {
ReactDOM.unstable_createRoot(document.body);
}).toErrorDev(
'createRoot(): Creating roots directly with document.body is ' +
'discouraged, since its children are often manipulated by third-party ' +
'scripts and browser extensions. This may lead to subtle ' +
'reconciliation issues. Try using a container element created ' +
'for your app.',
{withoutStack: true},
);
});
it('warns if updating a root that has had its contents removed', async () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(Hi
);
Scheduler.unstable_flushAll();
container.innerHTML = '';
expect(() => {
root.render(Hi
);
}).toErrorDev(
'render(...): It looks like the React-rendered content of the ' +
'root container was removed without using React. This is not ' +
'supported and will cause errors. Instead, call ' +
"root.unmount() to empty a root's container.",
{withoutStack: true},
);
});
});