Files
react/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
T
Flarnie Marchan 1e3cd332a0 Remove the 'alwaysUseRequestIdleCallbackPolyfill' feature flag (#12648)
* Remove the 'alwaysUseRequestIdleCallbackPolyfill' feature flag

**what is the change?:**
Removes the feature flag 'alwaysUseRequestIdleCallbackPolyfill', such
that we **always** use the polyfill for requestIdleCallback.

**why make this change?:**
We have been testing this feature flag at 100% for some time internally,
and determined it works better for React than the native implementation.
Looks like RN was overriding the flag to use the native when possible,
but since no RN products are using 'async' mode it should be safe to
switch this flag over for RN as well.

**test plan:**
We have already been testing this internally for some time.

**issue:**
internal task t28128480

* fix mistaken conditional

* Add mocking of rAF, postMessage, and initial test for ReactScheduler

**what is the change?:**
- In all tests where we previously mocked rIC or relied on native
mocking which no longer works, we are now mocking rAF and postMessage.
- Also adds a basic initial test for ReactScheduler.
NOTE -> we do plan to write headless browser tests for ReactScheduler!
This is just an initial test, to verify that it works with the mocked
out browser APIs as expected.

**why make this change?:**
We need to mock out the browser APIs more completely for the new
'ReactScheduler' to work in our tests. Many tests are depending on it,
since it's used at a low level.

By mocking the browser APIs rather than the 'react-scheduler' module, we
enable testing the production bundles. This approach is trading
isolation for accuracy. These tests will be closer to a real use.

**test plan:**
run the tests :)

**issue:**
internal task T28128480
2018-04-23 15:25:46 -07:00

367 lines
10 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 react-scheduler:
// - 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');
});
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',
]);
expect(() => batch.commit()).toThrow('Element type is invalid');
});
});