react-native-macos
Version:
A framework for building native macOS apps using React
491 lines (441 loc) • 12.3 kB
JavaScript
/**
* Copyright 2016-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
*/
;
describe('ReactDebugFiberPerf', () => {
let React;
let ReactCoroutine;
let ReactFeatureFlags;
let ReactNoop;
let ReactPortal;
let PropTypes;
let root;
let activeMeasure;
let knownMarks;
let knownMeasures;
function resetFlamechart() {
root = {
children: [],
indent: -1,
markName: null,
label: null,
parent: null,
toString() {
return this.children.map(c => c.toString()).join('\n');
},
};
activeMeasure = root;
knownMarks = new Set();
knownMeasures = new Set();
}
function addComment(comment) {
activeMeasure.children.push(
`${' '.repeat(activeMeasure.indent + 1)}// ${comment}`,
);
}
function getFlameChart() {
// Make sure we unwind the measurement stack every time.
expect(activeMeasure.indent).toBe(-1);
expect(activeMeasure).toBe(root);
// We should always clean them up because browsers
// buffer user timing measurements forever.
expect(knownMarks.size).toBe(0);
expect(knownMeasures.size).toBe(0);
return root.toString();
}
function createUserTimingPolyfill() {
// This is not a true polyfill, but it gives us enough
// to capture measurements in a readable tree-like output.
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API
return {
mark(markName) {
const measure = {
children: [],
indent: activeMeasure.indent + 1,
markName: markName,
// Will be assigned on measure() call:
label: null,
parent: activeMeasure,
toString() {
return (
[
' '.repeat(this.indent) + this.label,
...this.children.map(c => c.toString()),
].join('\n') +
// Extra newline after each root reconciliation
(this.indent === 0 ? '\n' : '')
);
},
};
// Step one level deeper
activeMeasure.children.push(measure);
activeMeasure = measure;
knownMarks.add(markName);
},
// We don't use the overload with three arguments.
measure(label, markName) {
if (markName !== activeMeasure.markName) {
throw new Error('Unexpected measure() call.');
}
// Step one level up
activeMeasure.label = label;
activeMeasure = activeMeasure.parent;
knownMeasures.add(label);
},
clearMarks(markName) {
if (markName === activeMeasure.markName) {
// Step one level up if we're in this measure
activeMeasure = activeMeasure.parent;
activeMeasure.children.length--;
}
knownMarks.delete(markName);
},
clearMeasures(label) {
knownMeasures.delete(label);
},
};
}
beforeEach(() => {
jest.resetModules();
resetFlamechart();
global.performance = createUserTimingPolyfill();
// Import after the polyfill is set up:
React = require('React');
ReactCoroutine = require('ReactCoroutine');
ReactFeatureFlags = require('ReactFeatureFlags');
ReactNoop = require('ReactNoop');
ReactPortal = require('ReactPortal');
ReactFeatureFlags.disableNewFiberFeatures = false;
PropTypes = require('prop-types');
});
afterEach(() => {
delete global.performance;
});
function Parent(props) {
return <div>{props.children}</div>;
}
function Child(props) {
return <div>{props.children}</div>;
}
it('measures a simple reconciliation', () => {
ReactNoop.render(<Parent><Child /></Parent>);
addComment('Mount');
ReactNoop.flush();
ReactNoop.render(<Parent><Child /></Parent>);
addComment('Update');
ReactNoop.flush();
ReactNoop.render(null);
addComment('Unmount');
ReactNoop.flush();
expect(getFlameChart()).toMatchSnapshot();
});
it('skips parents during setState', () => {
class A extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
}
class B extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
}
let a;
let b;
ReactNoop.render(
<Parent>
<Parent>
<Parent>
<A ref={inst => (a = inst)} />
</Parent>
</Parent>
<Parent>
<B ref={inst => (b = inst)} />
</Parent>
</Parent>,
);
ReactNoop.flush();
resetFlamechart();
a.setState({});
b.setState({});
addComment('Should include just A and B, no Parents');
ReactNoop.flush();
expect(getFlameChart()).toMatchSnapshot();
});
it('warns on cascading renders from setState', () => {
class Cascading extends React.Component {
componentDidMount() {
this.setState({});
}
render() {
return <div>{this.props.children}</div>;
}
}
ReactNoop.render(<Parent><Cascading /></Parent>);
addComment('Should print a warning');
ReactNoop.flush();
expect(getFlameChart()).toMatchSnapshot();
});
it('warns on cascading renders from top-level render', () => {
class Cascading extends React.Component {
componentDidMount() {
ReactNoop.renderToRootWithID(<Child />, 'b');
addComment('Scheduling another root from componentDidMount');
ReactNoop.flush();
}
render() {
return <div>{this.props.children}</div>;
}
}
ReactNoop.renderToRootWithID(<Cascading />, 'a');
addComment('Rendering the first root');
ReactNoop.flush();
expect(getFlameChart()).toMatchSnapshot();
});
it('does not treat setState from cWM or cWRP as cascading', () => {
class NotCascading extends React.Component {
componentWillMount() {
this.setState({});
}
componentWillReceiveProps() {
this.setState({});
}
render() {
return <div>{this.props.children}</div>;
}
}
ReactNoop.render(<Parent><NotCascading /></Parent>);
addComment('Should not print a warning');
ReactNoop.flush();
ReactNoop.render(<Parent><NotCascading /></Parent>);
addComment('Should not print a warning');
ReactNoop.flush();
expect(getFlameChart()).toMatchSnapshot();
});
it('captures all lifecycles', () => {
class AllLifecycles extends React.Component {
static childContextTypes = {
foo: PropTypes.any,
};
shouldComponentUpdate() {
return true;
}
getChildContext() {
return {foo: 42};
}
componentWillMount() {}
componentDidMount() {}
componentWillReceiveProps() {}
componentWillUpdate() {}
componentDidUpdate() {}
componentWillUnmount() {}
render() {
return <div />;
}
}
ReactNoop.render(<AllLifecycles />);
addComment('Mount');
ReactNoop.flush();
ReactNoop.render(<AllLifecycles />);
addComment('Update');
ReactNoop.flush();
ReactNoop.render(null);
addComment('Unmount');
ReactNoop.flush();
expect(getFlameChart()).toMatchSnapshot();
});
it('measures deprioritized work', () => {
ReactNoop.performAnimationWork(() => {
ReactNoop.render(
<Parent>
<div hidden={true}>
<Child />
</div>
</Parent>,
);
});
addComment('Flush the parent');
ReactNoop.flushAnimationPri();
addComment('Flush the child');
ReactNoop.flushDeferredPri();
expect(getFlameChart()).toMatchSnapshot();
});
it('measures deferred work in chunks', () => {
class A extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
}
class B extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
}
ReactNoop.render(
<Parent>
<A>
<Child />
</A>
<B>
<Child />
</B>
</Parent>,
);
addComment('Start mounting Parent and A');
ReactNoop.flushDeferredPri(40);
addComment('Mount B just a little (but not enough to memoize)');
ReactNoop.flushDeferredPri(10);
addComment('Complete B and Parent');
ReactNoop.flushDeferredPri();
expect(getFlameChart()).toMatchSnapshot();
});
it('recovers from fatal errors', () => {
function Baddie() {
throw new Error('Game over');
}
ReactNoop.render(<Parent><Baddie /></Parent>);
try {
addComment('Will fatal');
ReactNoop.flush();
} catch (err) {
expect(err.message).toBe('Game over');
}
ReactNoop.render(<Parent><Child /></Parent>);
addComment('Will reconcile from a clean state');
ReactNoop.flush();
expect(getFlameChart()).toMatchSnapshot();
});
it('recovers from caught errors', () => {
function Baddie() {
throw new Error('Game over');
}
function ErrorReport() {
return <div />;
}
class Boundary extends React.Component {
state = {error: null};
unstable_handleError(error) {
this.setState({error});
}
render() {
if (this.state.error) {
return <ErrorReport />;
}
return this.props.children;
}
}
ReactNoop.render(
<Parent>
<Boundary>
<Parent>
<Baddie />
</Parent>
</Boundary>
</Parent>,
);
addComment('Stop on Baddie and restart from Boundary');
ReactNoop.flush();
expect(getFlameChart()).toMatchSnapshot();
});
it('deduplicates lifecycle names during commit to reduce overhead', () => {
class A extends React.Component {
componentDidUpdate() {}
render() {
return <div />;
}
}
class B extends React.Component {
componentDidUpdate(prevProps) {
if (this.props.cascade && !prevProps.cascade) {
this.setState({});
}
}
render() {
return <div />;
}
}
ReactNoop.render(
<Parent>
<A />
<B />
<A />
<B />
</Parent>,
);
ReactNoop.flush();
resetFlamechart();
ReactNoop.render(
<Parent>
<A />
<B />
<A />
<B />
</Parent>,
);
addComment('The commit phase should mention A and B just once');
ReactNoop.flush();
ReactNoop.render(
<Parent>
<A />
<B />
<A />
<B cascade={true} />
</Parent>,
);
addComment("Because of deduplication, we don't know B was cascading,");
addComment('but we should still see the warning for the commit phase.');
ReactNoop.flush();
expect(getFlameChart()).toMatchSnapshot();
});
it('supports coroutines', () => {
function Continuation({isSame}) {
return <span prop={isSame ? 'foo==bar' : 'foo!=bar'} />;
}
function CoChild({bar}) {
return ReactCoroutine.createYield({
props: {
bar: bar,
},
continuation: Continuation,
});
}
function Indirection() {
return [<CoChild key="a" bar={true} />, <CoChild key="b" bar={false} />];
}
function HandleYields(props, yields) {
return yields.map((y, i) => (
<y.continuation key={i} isSame={props.foo === y.props.bar} />
));
}
function CoParent(props) {
return ReactCoroutine.createCoroutine(
props.children,
HandleYields,
props,
);
}
function App() {
return <div><CoParent foo={true}><Indirection /></CoParent></div>;
}
ReactNoop.render(<App />);
ReactNoop.flush();
expect(getFlameChart()).toMatchSnapshot();
});
it('supports portals', () => {
const noopContainer = {children: []};
ReactNoop.render(
<Parent>
{ReactPortal.createPortal(<Child />, noopContainer, null)}
</Parent>,
);
ReactNoop.flush();
expect(getFlameChart()).toMatchSnapshot();
});
});