react-native
Version:
A framework for building native apps using React
414 lines (359 loc) • 13.5 kB
JavaScript
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/
;
var React;
var ReactNoop;
var ReactFeatureFlags;
describe('ReactIncrementalScheduling', () => {
beforeEach(() => {
jest.resetModules();
React = require('React');
ReactNoop = require('ReactNoop');
ReactFeatureFlags = require('ReactFeatureFlags');
ReactFeatureFlags.disableNewFiberFeatures = false;
});
function span(prop) {
return { type: 'span', children: [], prop };
}
it('schedules and flushes deferred work', () => {
ReactNoop.render(<span prop="1" />);
expect(ReactNoop.getChildren()).toEqual([]);
ReactNoop.flushDeferredPri();
expect(ReactNoop.getChildren()).toEqual([span('1')]);
});
it('schedules and flushes animation work', () => {
ReactNoop.performAnimationWork(() => {
ReactNoop.render(<span prop="1" />);
});
expect(ReactNoop.getChildren()).toEqual([]);
ReactNoop.flushAnimationPri();
expect(ReactNoop.getChildren()).toEqual([span('1')]);
});
it('searches for work on other roots once the current root completes', () => {
ReactNoop.renderToRootWithID(<span prop="a:1" />, 'a');
ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b');
ReactNoop.renderToRootWithID(<span prop="c:1" />, 'c');
ReactNoop.flush();
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:1')]);
});
it('schedules an animation callback when there`\s leftover animation work', () => {
class Foo extends React.Component {
state = { step: 0 };
componentDidMount() {
ReactNoop.performAnimationWork(() => {
this.setState({ step: 2 });
});
this.setState({ step: 1 });
}
render() {
return <span prop={this.state.step} />;
}
}
ReactNoop.render(<Foo />);
// Flush just enough work to mount the component, but not enough to flush
// the animation update.
ReactNoop.flushDeferredPri(25);
expect(ReactNoop.getChildren()).toEqual([span(1)]);
// There's more animation work. A callback should have been scheduled.
ReactNoop.flushAnimationPri();
expect(ReactNoop.getChildren()).toEqual([span(2)]);
});
it('schedules top-level updates in order of priority', () => {
// Initial render.
ReactNoop.render(<span prop={1} />);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span(1)]);
ReactNoop.render(<span prop={5} />);
ReactNoop.performAnimationWork(() => {
ReactNoop.render(<span prop={2} />);
ReactNoop.render(<span prop={3} />);
ReactNoop.render(<span prop={4} />);
});
// The low pri update should be flushed last, even though it was scheduled
// before the animation updates.
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span(5)]);
});
it('schedules top-level updates with same priority in order of insertion', () => {
// Initial render.
ReactNoop.render(<span prop={1} />);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span(1)]);
ReactNoop.render(<span prop={2} />);
ReactNoop.render(<span prop={3} />);
ReactNoop.render(<span prop={4} />);
ReactNoop.render(<span prop={5} />);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span(5)]);
});
it('works on deferred roots in the order they were scheduled', () => {
ReactNoop.renderToRootWithID(<span prop="a:1" />, 'a');
ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b');
ReactNoop.renderToRootWithID(<span prop="c:1" />, 'c');
ReactNoop.flush();
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:1')]);
// Schedule deferred work in the reverse order
ReactNoop.renderToRootWithID(<span prop="c:2" />, 'c');
ReactNoop.renderToRootWithID(<span prop="b:2" />, 'b');
// Ensure it starts in the order it was scheduled
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
// Schedule last bit of work, it will get processed the last
ReactNoop.renderToRootWithID(<span prop="a:2" />, 'a');
// Keep performing work in the order it was scheduled
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
});
it('schedules sync updates when inside componentDidMount/Update', () => {
var instance;
var ops = [];
class Foo extends React.Component {
state = { tick: 0 };
componentDidMount() {
ops.push('componentDidMount (before setState): ' + this.state.tick);
this.setState({ tick: 1 });
// We're in a batch. Update hasn't flushed yet.
ops.push('componentDidMount (after setState): ' + this.state.tick);
}
componentDidUpdate() {
ops.push('componentDidUpdate: ' + this.state.tick);
if (this.state.tick === 2) {
ops.push('componentDidUpdate (before setState): ' + this.state.tick);
this.setState({ tick: 3 });
ops.push('componentDidUpdate (after setState): ' + this.state.tick);
// We're in a batch. Update hasn't flushed yet.
}
}
render() {
ops.push('render: ' + this.state.tick);
instance = this;
return <span prop={this.state.tick} />;
}
}
ReactNoop.render(<Foo />);
ReactNoop.flushDeferredPri(20 + 5);
expect(ops).toEqual([
'render: 0',
'componentDidMount (before setState): 0',
'componentDidMount (after setState): 0',
// If the setState inside componentDidMount were deferred, there would be
// no more ops. Because it has Task priority, we get these ops, too:
'render: 1',
'componentDidUpdate: 1',
]);
ops = [];
instance.setState({ tick: 2 });
ReactNoop.flushDeferredPri(20 + 5);
expect(ops).toEqual([
'render: 2',
'componentDidUpdate: 2',
'componentDidUpdate (before setState): 2',
'componentDidUpdate (after setState): 2',
// If the setState inside componentDidUpdate were deferred, there would be
// no more ops. Because it has Task priority, we get these ops, too:
'render: 3',
'componentDidUpdate: 3',
]);
});
it('can opt-in to deferred/animation scheduling inside componentDidMount/Update', () => {
var instance;
var ops = [];
class Foo extends React.Component {
state = { tick: 0 };
componentDidMount() {
ReactNoop.performAnimationWork(() => {
ops.push('componentDidMount (before setState): ' + this.state.tick);
this.setState({ tick: 1 });
ops.push('componentDidMount (after setState): ' + this.state.tick);
});
}
componentDidUpdate() {
ReactNoop.performAnimationWork(() => {
ops.push('componentDidUpdate: ' + this.state.tick);
if (this.state.tick === 2) {
ops.push('componentDidUpdate (before setState): ' + this.state.tick);
this.setState({ tick: 3 });
ops.push('componentDidUpdate (after setState): ' + this.state.tick);
}
});
}
render() {
ops.push('render: ' + this.state.tick);
instance = this;
return <span prop={this.state.tick} />;
}
}
ReactNoop.render(<Foo />);
ReactNoop.flushDeferredPri(20 + 5);
expect(ops).toEqual([
'render: 0',
'componentDidMount (before setState): 0',
'componentDidMount (after setState): 0',
// Following items shouldn't appear because they are the result of an
// update scheduled with animation priority
// 'render: 1',
// 'componentDidUpdate: 1',
]);
ops = [];
ReactNoop.flushAnimationPri();
expect(ops).toEqual([
'render: 1',
'componentDidUpdate: 1',
]);
ops = [];
instance.setState({ tick: 2 });
ReactNoop.flushDeferredPri(20 + 5);
expect(ops).toEqual([
'render: 2',
'componentDidUpdate: 2',
'componentDidUpdate (before setState): 2',
'componentDidUpdate (after setState): 2',
// Following items shouldn't appear because they are the result of an
// update scheduled with animation priority
// 'render: 3',
// 'componentDidUpdate: 3',
]);
ops = [];
ReactNoop.flushAnimationPri();
expect(ops).toEqual([
'render: 3',
'componentDidUpdate: 3',
]);
});
it('performs Task work even after time runs out', () => {
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() {
return <span prop={this.state.step} />;
}
}
ReactNoop.render(<Foo />);
// This should be just enough to complete all the work, but not enough to
// commit it.
ReactNoop.flushDeferredPri(20);
expect(ReactNoop.getChildren()).toEqual([]);
// Do one more unit of work.
ReactNoop.flushDeferredPri(10);
// The updates should all be flushed with Task priority
expect(ReactNoop.getChildren()).toEqual([span(5)]);
});
it('does not perform animation work after time runs out', () => {
class Foo extends React.Component {
state = { step: 1 };
componentDidMount() {
ReactNoop.performAnimationWork(() => {
this.setState({ step: 2 }, () => {
this.setState({ step: 3 }, () => {
this.setState({ step: 4 }, () => {
this.setState({ step: 5 });
});
});
});
});
}
render() {
return <span prop={this.state.step} />;
}
}
ReactNoop.render(<Foo />);
// This should be just enough to complete all the work, but not enough to
// commit it.
ReactNoop.flushDeferredPri(20);
expect(ReactNoop.getChildren()).toEqual([]);
// Do one more unit of work.
ReactNoop.flushDeferredPri(10);
// None of the updates should be flushed because they only have
// animation priority.
expect(ReactNoop.getChildren()).toEqual([span(1)]);
});
it('can opt-out of batching using unbatchedUpdates', () => {
// syncUpdates gives synchronous priority to updates
ReactNoop.syncUpdates(() => {
// batchedUpdates downgrades sync updates to task priority
ReactNoop.batchedUpdates(() => {
ReactNoop.render(<span prop={0} />);
expect(ReactNoop.getChildren()).toEqual([]);
// Should not have flushed yet because we're still batching
// unbatchedUpdates reverses the effect of batchedUpdates, so sync
// updates are not batched
ReactNoop.unbatchedUpdates(() => {
ReactNoop.render(<span prop={1} />);
expect(ReactNoop.getChildren()).toEqual([span(1)]);
ReactNoop.render(<span prop={2} />);
expect(ReactNoop.getChildren()).toEqual([span(2)]);
});
ReactNoop.render(<span prop={3} />);
expect(ReactNoop.getChildren()).toEqual([span(2)]);
});
// Remaining update is now flushed
expect(ReactNoop.getChildren()).toEqual([span(3)]);
});
});
it('nested updates are always deferred, even inside unbatchedUpdates', () => {
let instance;
let ops = [];
class Foo extends React.Component {
state = { step: 0 };
componentDidUpdate() {
ops.push('componentDidUpdate: ' + this.state.step);
if (this.state.step === 1) {
ReactNoop.unbatchedUpdates(() => {
// This is a nested state update, so it should not be
// flushed synchronously, even though we wrapped it
// in unbatchedUpdates.
this.setState({ step: 2 });
});
expect(ReactNoop.getChildren()).toEqual([span(1)]);
}
}
render() {
ops.push('render: ' + this.state.step);
instance = this;
return <span prop={this.state.step} />;
}
}
ReactNoop.render(<Foo />);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span(0)]);
ReactNoop.syncUpdates(() => {
instance.setState({ step: 1 });
expect(ReactNoop.getChildren()).toEqual([span(2)]);
});
expect(ops).toEqual([
'render: 0',
'render: 1',
'componentDidUpdate: 1',
'render: 2',
'componentDidUpdate: 2',
]);
});
});