'use strict';
let React;
let ReactNoop;
let Scheduler;
let act;
let waitForAll;
let waitFor;
let assertLog;
let waitForPaint;
describe('ReactIncrementalScheduling', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
waitForPaint = InternalTestUtils.waitForPaint;
});
it('schedules and flushes deferred work', async () => {
ReactNoop.render(<span prop="1" />);
expect(ReactNoop).toMatchRenderedOutput(null);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="1" />);
});
it('searches for work on other roots once the current root completes', async () => {
ReactNoop.renderToRootWithID(<span prop="a:1" />, 'a');
ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b');
ReactNoop.renderToRootWithID(<span prop="c:1" />, 'c');
await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:1" />);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:1" />);
expect(ReactNoop.getChildrenAsJSX('c')).toEqual(<span prop="c:1" />);
});
it('schedules top-level updates in order of priority', async () => {
ReactNoop.render(<span prop={1} />);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
ReactNoop.batchedUpdates(() => {
ReactNoop.render(<span prop={5} />);
ReactNoop.flushSync(() => {
ReactNoop.render(<span prop={2} />);
ReactNoop.render(<span prop={3} />);
ReactNoop.render(<span prop={4} />);
});
});
expect(ReactNoop).toMatchRenderedOutput(<span prop={4} />);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={4} />);
});
it('schedules top-level updates with same priority in order of insertion', async () => {
ReactNoop.render(<span prop={1} />);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
ReactNoop.render(<span prop={2} />);
ReactNoop.render(<span prop={3} />);
ReactNoop.render(<span prop={4} />);
ReactNoop.render(<span prop={5} />);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={5} />);
});
it('works on deferred roots in the order they were scheduled', async () => {
const {useEffect} = React;
function Text({text}) {
useEffect(() => {
Scheduler.log(text);
}, [text]);
return text;
}
await act(() => {
ReactNoop.renderToRootWithID(<Text text="a:1" />, 'a');
ReactNoop.renderToRootWithID(<Text text="b:1" />, 'b');
ReactNoop.renderToRootWithID(<Text text="c:1" />, 'c');
});
assertLog(['a:1', 'b:1', 'c:1']);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:1');
await act(async () => {
React.startTransition(() => {
ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
});
await waitFor(['c:2']);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
React.startTransition(() => {
ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
});
await waitFor(['b:2']);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:2');
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
await waitFor(['a:2']);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:2');
expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:2');
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
});
});
it('schedules sync updates when inside componentDidMount/Update', async () => {
let instance;
class Foo extends React.Component {
state = {tick: 0};
componentDidMount() {
Scheduler.log(
'componentDidMount (before setState): ' + this.state.tick,
);
this.setState({tick: 1});
Scheduler.log('componentDidMount (after setState): ' + this.state.tick);
}
componentDidUpdate() {
Scheduler.log('componentDidUpdate: ' + this.state.tick);
if (this.state.tick === 2) {
Scheduler.log(
'componentDidUpdate (before setState): ' + this.state.tick,
);
this.setState({tick: 3});
Scheduler.log(
'componentDidUpdate (after setState): ' + this.state.tick,
);
}
}
render() {
Scheduler.log('render: ' + this.state.tick);
instance = this;
return <span prop={this.state.tick} />;
}
}
React.startTransition(() => {
ReactNoop.render(<Foo />);
});
await waitFor(['render: 0']);
expect(ReactNoop.flushNextYield()).toEqual([
'componentDidMount (before setState): 0',
'componentDidMount (after setState): 0',
'render: 1',
'componentDidUpdate: 1',
]);
React.startTransition(() => {
instance.setState({tick: 2});
});
await waitFor(['render: 2']);
expect(ReactNoop.flushNextYield()).toEqual([
'componentDidUpdate: 2',
'componentDidUpdate (before setState): 2',
'componentDidUpdate (after setState): 2',
'render: 3',
'componentDidUpdate: 3',
]);
});
it('can opt-in to async scheduling inside componentDidMount/Update', async () => {
let instance;
class Foo extends React.Component {
state = {tick: 0};
componentDidMount() {
React.startTransition(() => {
Scheduler.log(
'componentDidMount (before setState): ' + this.state.tick,
);
this.setState({tick: 1});
Scheduler.log(
'componentDidMount (after setState): ' + this.state.tick,
);
});
}
componentDidUpdate() {
React.startTransition(() => {
Scheduler.log('componentDidUpdate: ' + this.state.tick);
if (this.state.tick === 2) {
Scheduler.log(
'componentDidUpdate (before setState): ' + this.state.tick,
);
this.setState({tick: 3});
Scheduler.log(
'componentDidUpdate (after setState): ' + this.state.tick,
);
}
});
}
render() {
Scheduler.log('render: ' + this.state.tick);
instance = this;
return <span prop={this.state.tick} />;
}
}
ReactNoop.flushSync(() => {
ReactNoop.render(<Foo />);
});
assertLog([
'render: 0',
'componentDidMount (before setState): 0',
'componentDidMount (after setState): 0',
]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
await waitForAll(['render: 1', 'componentDidUpdate: 1']);
expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
React.startTransition(() => {
instance.setState({tick: 2});
});
await waitForPaint([
'render: 2',
'componentDidUpdate: 2',
'componentDidUpdate (before setState): 2',
'componentDidUpdate (after setState): 2',
]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
await waitForAll(['render: 3', 'componentDidUpdate: 3']);
expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
});
it('performs Task work even after time runs out', async () => {
class Foo extends React.Component {
state = {step: 1};
componentDidMount() {
this.setState({step: 2}, () => {
this.setState({step: 3}, () => {
this.setState({step: 4}, () => {
this.setState({step: 5});
});
});
});
}
render() {
Scheduler.log('Foo');
return <span prop={this.state.step} />;
}
}
React.startTransition(() => {
ReactNoop.render(<Foo />);
});
await waitFor(['Foo']);
expect(ReactNoop).toMatchRenderedOutput(null);
ReactNoop.flushNextYield();
expect(ReactNoop).toMatchRenderedOutput(<span prop={5} />);
});
});