mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
b92f947af1
* Git moved packages/react-scheduler -> packages/schedule * Global find+replace 'react-scheduler' -> 'schedule' * Global find+replace 'ReactScheduler' -> 'Scheduler' * Renamed remaining files "ReactScheduler" -> "Schedule" * Add thank-you note to schedule package README * Replaced schedule package versions 0.1.0-alpha-1 -> 0.2.0 * Patched our local fixtures to work around Yarn install issue * Removed some fixture hacks
378 lines
11 KiB
JavaScript
378 lines
11 KiB
JavaScript
/**
|
|
* Copyright (c) 2013-present, Facebook, Inc.
|
|
*
|
|
* 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 AsyncMode = React.unstable_AsyncMode;
|
|
|
|
describe('ReactDOMRoot', () => {
|
|
let container;
|
|
|
|
let advanceCurrentTime;
|
|
|
|
beforeEach(() => {
|
|
container = document.createElement('div');
|
|
// TODO pull this into helper method, reduce repetition.
|
|
// mock the browser APIs which are used in schedule:
|
|
// - requestAnimationFrame should pass the DOMHighResTimeStamp argument
|
|
// - calling 'window.postMessage' should actually fire postmessage handlers
|
|
// - must allow artificially changing time returned by Date.now
|
|
// Performance.now is not supported in the test environment
|
|
const originalDateNow = Date.now;
|
|
let advancedTime = null;
|
|
global.Date.now = function() {
|
|
if (advancedTime) {
|
|
return originalDateNow() + advancedTime;
|
|
}
|
|
return originalDateNow();
|
|
};
|
|
advanceCurrentTime = function(amount) {
|
|
advancedTime = amount;
|
|
};
|
|
global.requestAnimationFrame = function(cb) {
|
|
return setTimeout(() => {
|
|
cb(Date.now());
|
|
});
|
|
};
|
|
const originalAddEventListener = global.addEventListener;
|
|
let postMessageCallback;
|
|
global.addEventListener = function(eventName, callback, useCapture) {
|
|
if (eventName === 'message') {
|
|
postMessageCallback = callback;
|
|
} else {
|
|
originalAddEventListener(eventName, callback, useCapture);
|
|
}
|
|
};
|
|
global.postMessage = function(messageKey, targetOrigin) {
|
|
const postMessageEvent = {source: window, data: messageKey};
|
|
if (postMessageCallback) {
|
|
postMessageCallback(postMessageEvent);
|
|
}
|
|
};
|
|
|
|
jest.resetModules();
|
|
React = require('react');
|
|
ReactDOM = require('react-dom');
|
|
ReactDOMServer = require('react-dom/server');
|
|
AsyncMode = React.unstable_AsyncMode;
|
|
});
|
|
|
|
it('renders children', () => {
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
root.render(<div>Hi</div>);
|
|
jest.runAllTimers();
|
|
expect(container.textContent).toEqual('Hi');
|
|
});
|
|
|
|
it('unmounts children', () => {
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
root.render(<div>Hi</div>);
|
|
jest.runAllTimers();
|
|
expect(container.textContent).toEqual('Hi');
|
|
root.unmount();
|
|
jest.runAllTimers();
|
|
expect(container.textContent).toEqual('');
|
|
});
|
|
|
|
it('`root.render` returns a thenable work object', () => {
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
const work = root.render(<AsyncMode>Hi</AsyncMode>);
|
|
let ops = [];
|
|
work.then(() => {
|
|
ops.push('inside callback: ' + container.textContent);
|
|
});
|
|
ops.push('before committing: ' + container.textContent);
|
|
jest.runAllTimers();
|
|
ops.push('after committing: ' + container.textContent);
|
|
expect(ops).toEqual([
|
|
'before committing: ',
|
|
// `then` callback should fire during commit phase
|
|
'inside callback: Hi',
|
|
'after committing: Hi',
|
|
]);
|
|
});
|
|
|
|
it('resolves `work.then` callback synchronously if the work already committed', () => {
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
const work = root.render(<AsyncMode>Hi</AsyncMode>);
|
|
jest.runAllTimers();
|
|
let ops = [];
|
|
work.then(() => {
|
|
ops.push('inside callback');
|
|
});
|
|
expect(ops).toEqual(['inside callback']);
|
|
});
|
|
|
|
it('supports hydration', async () => {
|
|
const markup = await new Promise(resolve =>
|
|
resolve(
|
|
ReactDOMServer.renderToString(
|
|
<div>
|
|
<span className="extra" />
|
|
</div>,
|
|
),
|
|
),
|
|
);
|
|
|
|
// Does not hydrate by default
|
|
const container1 = document.createElement('div');
|
|
container1.innerHTML = markup;
|
|
const root1 = ReactDOM.unstable_createRoot(container1);
|
|
root1.render(
|
|
<div>
|
|
<span />
|
|
</div>,
|
|
);
|
|
jest.runAllTimers();
|
|
|
|
// Accepts `hydrate` option
|
|
const container2 = document.createElement('div');
|
|
container2.innerHTML = markup;
|
|
const root2 = ReactDOM.unstable_createRoot(container2, {hydrate: true});
|
|
root2.render(
|
|
<div>
|
|
<span />
|
|
</div>,
|
|
);
|
|
expect(jest.runAllTimers).toWarnDev('Extra attributes', {
|
|
withoutStack: true,
|
|
});
|
|
});
|
|
|
|
it('does not clear existing children', async () => {
|
|
container.innerHTML = '<div>a</div><div>b</div>';
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
root.render(
|
|
<div>
|
|
<span>c</span>
|
|
<span>d</span>
|
|
</div>,
|
|
);
|
|
jest.runAllTimers();
|
|
expect(container.textContent).toEqual('abcd');
|
|
root.render(
|
|
<div>
|
|
<span>d</span>
|
|
<span>c</span>
|
|
</div>,
|
|
);
|
|
jest.runAllTimers();
|
|
expect(container.textContent).toEqual('abdc');
|
|
});
|
|
|
|
it('can defer a commit by batching it', () => {
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
const batch = root.createBatch();
|
|
batch.render(<div>Hi</div>);
|
|
// Hasn't committed yet
|
|
expect(container.textContent).toEqual('');
|
|
// Commit
|
|
batch.commit();
|
|
expect(container.textContent).toEqual('Hi');
|
|
});
|
|
|
|
it('applies setState in componentDidMount synchronously in a batch', done => {
|
|
class App extends React.Component {
|
|
state = {mounted: false};
|
|
componentDidMount() {
|
|
this.setState({
|
|
mounted: true,
|
|
});
|
|
}
|
|
render() {
|
|
return this.state.mounted ? 'Hi' : 'Bye';
|
|
}
|
|
}
|
|
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
const batch = root.createBatch();
|
|
batch.render(
|
|
<AsyncMode>
|
|
<App />
|
|
</AsyncMode>,
|
|
);
|
|
|
|
jest.runAllTimers();
|
|
|
|
// Hasn't updated yet
|
|
expect(container.textContent).toEqual('');
|
|
|
|
let ops = [];
|
|
batch.then(() => {
|
|
// Still hasn't updated
|
|
ops.push(container.textContent);
|
|
|
|
// Should synchronously commit
|
|
batch.commit();
|
|
ops.push(container.textContent);
|
|
|
|
expect(ops).toEqual(['', 'Hi']);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('does not restart a completed batch when committing if there were no intervening updates', () => {
|
|
let ops = [];
|
|
function Foo(props) {
|
|
ops.push('Foo');
|
|
return props.children;
|
|
}
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
const batch = root.createBatch();
|
|
batch.render(<Foo>Hi</Foo>);
|
|
// Flush all async work.
|
|
jest.runAllTimers();
|
|
// Root should complete without committing.
|
|
expect(ops).toEqual(['Foo']);
|
|
expect(container.textContent).toEqual('');
|
|
|
|
ops = [];
|
|
|
|
// Commit. Shouldn't re-render Foo.
|
|
batch.commit();
|
|
expect(ops).toEqual([]);
|
|
expect(container.textContent).toEqual('Hi');
|
|
});
|
|
|
|
it('can wait for a batch to finish', () => {
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
const batch = root.createBatch();
|
|
batch.render(<AsyncMode>Foo</AsyncMode>);
|
|
|
|
jest.runAllTimers();
|
|
|
|
// Hasn't updated yet
|
|
expect(container.textContent).toEqual('');
|
|
|
|
let ops = [];
|
|
batch.then(() => {
|
|
// Still hasn't updated
|
|
ops.push(container.textContent);
|
|
// Should synchronously commit
|
|
batch.commit();
|
|
ops.push(container.textContent);
|
|
});
|
|
|
|
expect(ops).toEqual(['', 'Foo']);
|
|
});
|
|
|
|
it('`batch.render` returns a thenable work object', () => {
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
const batch = root.createBatch();
|
|
const work = batch.render('Hi');
|
|
let ops = [];
|
|
work.then(() => {
|
|
ops.push('inside callback: ' + container.textContent);
|
|
});
|
|
ops.push('before committing: ' + container.textContent);
|
|
batch.commit();
|
|
ops.push('after committing: ' + container.textContent);
|
|
expect(ops).toEqual([
|
|
'before committing: ',
|
|
// `then` callback should fire during commit phase
|
|
'inside callback: Hi',
|
|
'after committing: Hi',
|
|
]);
|
|
});
|
|
|
|
it('can commit an empty batch', () => {
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
root.render(<AsyncMode>1</AsyncMode>);
|
|
|
|
advanceCurrentTime(2000);
|
|
// This batch has a later expiration time than the earlier update.
|
|
const batch = root.createBatch();
|
|
|
|
// This should not flush the earlier update.
|
|
batch.commit();
|
|
expect(container.textContent).toEqual('');
|
|
|
|
jest.runAllTimers();
|
|
expect(container.textContent).toEqual('1');
|
|
});
|
|
|
|
it('two batches created simultaneously are committed separately', () => {
|
|
// (In other words, they have distinct expiration times)
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
const batch1 = root.createBatch();
|
|
batch1.render(1);
|
|
const batch2 = root.createBatch();
|
|
batch2.render(2);
|
|
|
|
expect(container.textContent).toEqual('');
|
|
|
|
batch1.commit();
|
|
expect(container.textContent).toEqual('1');
|
|
|
|
batch2.commit();
|
|
expect(container.textContent).toEqual('2');
|
|
});
|
|
|
|
it('commits an earlier batch without committing a later batch', () => {
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
const batch1 = root.createBatch();
|
|
batch1.render(1);
|
|
|
|
// This batch has a later expiration time
|
|
advanceCurrentTime(2000);
|
|
const batch2 = root.createBatch();
|
|
batch2.render(2);
|
|
|
|
expect(container.textContent).toEqual('');
|
|
|
|
batch1.commit();
|
|
expect(container.textContent).toEqual('1');
|
|
|
|
batch2.commit();
|
|
expect(container.textContent).toEqual('2');
|
|
});
|
|
|
|
it('commits a later batch without committing an earlier batch', () => {
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
const batch1 = root.createBatch();
|
|
batch1.render(1);
|
|
|
|
// This batch has a later expiration time
|
|
advanceCurrentTime(2000);
|
|
const batch2 = root.createBatch();
|
|
batch2.render(2);
|
|
|
|
expect(container.textContent).toEqual('');
|
|
|
|
batch2.commit();
|
|
expect(container.textContent).toEqual('2');
|
|
|
|
batch1.commit();
|
|
jest.runAllTimers();
|
|
expect(container.textContent).toEqual('1');
|
|
});
|
|
|
|
it('handles fatal errors triggered by batch.commit()', () => {
|
|
const root = ReactDOM.unstable_createRoot(container);
|
|
const batch = root.createBatch();
|
|
const InvalidType = undefined;
|
|
expect(() => batch.render(<InvalidType />)).toWarnDev(
|
|
['React.createElement: type is invalid'],
|
|
{withoutStack: true},
|
|
);
|
|
expect(() => batch.commit()).toThrow('Element type is invalid');
|
|
});
|
|
|
|
it('throws a good message on invalid containers', () => {
|
|
expect(() => {
|
|
ReactDOM.unstable_createRoot(<div>Hi</div>);
|
|
}).toThrow(
|
|
'unstable_createRoot(...): Target container is not a DOM element.',
|
|
);
|
|
});
|
|
});
|