2016-06-02 02:17:10 +08:00
|
|
|
/**
|
2017-09-25 04:48:13 +08:00
|
|
|
* Copyright (c) 2013-present, Facebook, Inc.
|
2016-06-02 02:17:10 +08:00
|
|
|
*
|
2017-09-25 04:48:13 +08:00
|
|
|
* This source code is licensed under the MIT license found in the
|
|
|
|
|
* LICENSE file in the root directory of this source tree.
|
2016-06-02 02:17:10 +08:00
|
|
|
*
|
|
|
|
|
* @emails react-core
|
2018-01-03 02:42:18 +08:00
|
|
|
* @jest-environment node
|
2016-06-02 02:17:10 +08:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
Add stack unwinding phase for handling errors (#12201)
* Add stack unwinding phase for handling errors
A rewrite of error handling, with semantics that more closely match
stack unwinding.
Errors that are thrown during the render phase unwind to the nearest
error boundary, like before. But rather than synchronously unmount the
children before retrying, we restart the failed subtree within the same
render phase. The failed children are still unmounted (as if all their
keys changed) but without an extra commit.
Commit phase errors are different. They work by scheduling an error on
the update queue of the error boundary. When we enter the render phase,
the error is popped off the queue. The rest of the algorithm is
the same.
This approach is designed to work for throwing non-errors, too, though
that feature is not implemented yet.
* Add experimental getDerivedStateFromCatch lifecycle
Fires during the render phase, so you can recover from an error within the same
pass. This aligns error boundaries more closely with try-catch semantics.
Let's keep this behind a feature flag until a future release. For now, the
recommendation is to keep using componentDidCatch. Eventually, the advice will
be to use getDerivedStateFromCatch for handling errors and componentDidCatch
only for logging.
* Reconcile twice to remount failed children, instead of using a boolean
* Handle effect immediately after its thrown
This way we don't have to store the thrown values on the effect list.
* ReactFiberIncompleteWork -> ReactFiberUnwindWork
* Remove startTime
* Remove TypeOfException
We don't need it yet. We'll reconsider once we add another exception type.
* Move replay to outer catch block
This moves it out of the hot path.
2018-02-24 09:38:42 +08:00
|
|
|
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
|
|
|
|
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
2017-12-01 08:01:45 +08:00
|
|
|
const React = require('react');
|
|
|
|
|
const ReactTestRenderer = require('react-test-renderer');
|
|
|
|
|
const prettyFormat = require('pretty-format');
|
2016-06-02 02:17:10 +08:00
|
|
|
|
2018-07-07 07:04:45 +08:00
|
|
|
// Isolate noop renderer
|
|
|
|
|
jest.resetModules();
|
|
|
|
|
const ReactNoop = require('react-noop-renderer');
|
|
|
|
|
|
2017-02-10 04:55:00 +08:00
|
|
|
// Kind of hacky, but we nullify all the instances to test the tree structure
|
|
|
|
|
// with jasmine's deep equality function, and test the instances separate. We
|
|
|
|
|
// also delete children props because testing them is more annoying and not
|
|
|
|
|
// really important to verify.
|
2017-09-15 05:15:43 +08:00
|
|
|
function cleanNodeOrArray(node) {
|
2017-02-10 04:55:00 +08:00
|
|
|
if (!node) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2017-09-15 05:15:43 +08:00
|
|
|
if (Array.isArray(node)) {
|
|
|
|
|
node.forEach(cleanNodeOrArray);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2017-02-10 04:55:00 +08:00
|
|
|
if (node && node.instance) {
|
|
|
|
|
node.instance = null;
|
|
|
|
|
}
|
|
|
|
|
if (node && node.props && node.props.children) {
|
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
2017-12-01 08:01:45 +08:00
|
|
|
const {children, ...props} = node.props;
|
2017-02-10 04:55:00 +08:00
|
|
|
node.props = props;
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(node.rendered)) {
|
2017-09-15 05:15:43 +08:00
|
|
|
node.rendered.forEach(cleanNodeOrArray);
|
2017-02-10 04:55:00 +08:00
|
|
|
} else if (typeof node.rendered === 'object') {
|
2017-09-15 05:15:43 +08:00
|
|
|
cleanNodeOrArray(node.rendered);
|
2017-02-10 04:55:00 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-07 06:18:42 +08:00
|
|
|
describe('ReactTestRenderer', () => {
|
|
|
|
|
it('renders a simple component', () => {
|
2016-06-02 02:17:10 +08:00
|
|
|
function Link() {
|
|
|
|
|
return <a role="link" />;
|
|
|
|
|
}
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(<Link />);
|
2016-06-02 02:17:10 +08:00
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'a',
|
2017-03-14 08:05:18 +08:00
|
|
|
props: {role: 'link'},
|
2016-06-02 02:17:10 +08:00
|
|
|
children: null,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2016-09-07 06:18:42 +08:00
|
|
|
it('renders a top-level empty component', () => {
|
2016-08-19 04:02:09 +08:00
|
|
|
function Empty() {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(<Empty />);
|
2016-08-19 04:02:09 +08:00
|
|
|
expect(renderer.toJSON()).toEqual(null);
|
|
|
|
|
});
|
|
|
|
|
|
2016-09-07 06:18:42 +08:00
|
|
|
it('exposes a type flag', () => {
|
2016-06-14 13:50:28 +08:00
|
|
|
function Link() {
|
|
|
|
|
return <a role="link" />;
|
|
|
|
|
}
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(<Link />);
|
|
|
|
|
const object = renderer.toJSON();
|
2016-06-14 13:50:28 +08:00
|
|
|
expect(object.$$typeof).toBe(Symbol.for('react.test.json'));
|
|
|
|
|
|
|
|
|
|
// $$typeof should not be enumerable.
|
2017-12-01 08:01:45 +08:00
|
|
|
for (const key in object) {
|
2017-04-27 03:30:38 +08:00
|
|
|
if (object.hasOwnProperty(key)) {
|
2016-06-14 13:50:28 +08:00
|
|
|
expect(key).not.toBe('$$typeof');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2017-01-12 03:19:32 +08:00
|
|
|
it('can render a composite component', () => {
|
|
|
|
|
class Component extends React.Component {
|
|
|
|
|
render() {
|
|
|
|
|
return (
|
|
|
|
|
<div className="purple">
|
|
|
|
|
<Child />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const Child = () => {
|
2017-01-12 03:19:32 +08:00
|
|
|
return <moo />;
|
|
|
|
|
};
|
|
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(<Component />);
|
2017-01-12 03:19:32 +08:00
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'div',
|
2017-03-14 08:05:18 +08:00
|
|
|
props: {className: 'purple'},
|
|
|
|
|
children: [{type: 'moo', props: {}, children: null}],
|
2017-01-12 03:19:32 +08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2016-09-07 06:18:42 +08:00
|
|
|
it('renders some basics with an update', () => {
|
2017-12-01 08:01:45 +08:00
|
|
|
let renders = 0;
|
2016-07-21 15:23:42 +08:00
|
|
|
|
|
|
|
|
class Component extends React.Component {
|
|
|
|
|
state = {x: 3};
|
|
|
|
|
|
|
|
|
|
render() {
|
2016-06-02 02:17:10 +08:00
|
|
|
renders++;
|
|
|
|
|
return (
|
|
|
|
|
<div className="purple">
|
|
|
|
|
{this.state.x}
|
|
|
|
|
<Child />
|
|
|
|
|
<Null />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2016-07-21 15:23:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
componentDidMount() {
|
2016-06-02 02:17:10 +08:00
|
|
|
this.setState({x: 7});
|
2016-07-21 15:23:42 +08:00
|
|
|
}
|
|
|
|
|
}
|
2016-06-02 02:17:10 +08:00
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const Child = () => {
|
2016-11-18 06:16:44 +08:00
|
|
|
renders++;
|
|
|
|
|
return <moo />;
|
|
|
|
|
};
|
|
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const Null = () => {
|
2016-11-18 06:16:44 +08:00
|
|
|
renders++;
|
|
|
|
|
return null;
|
|
|
|
|
};
|
2016-06-02 02:17:10 +08:00
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(<Component />);
|
2016-06-02 02:17:10 +08:00
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'div',
|
2017-03-14 08:05:18 +08:00
|
|
|
props: {className: 'purple'},
|
2017-06-15 05:10:33 +08:00
|
|
|
children: ['7', {type: 'moo', props: {}, children: null}],
|
2016-06-02 02:17:10 +08:00
|
|
|
});
|
|
|
|
|
expect(renders).toBe(6);
|
|
|
|
|
});
|
|
|
|
|
|
2016-09-07 06:18:42 +08:00
|
|
|
it('exposes the instance', () => {
|
2016-06-02 02:17:10 +08:00
|
|
|
class Mouse extends React.Component {
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.state = {mouse: 'mouse'};
|
|
|
|
|
}
|
|
|
|
|
handleMoose() {
|
|
|
|
|
this.setState({mouse: 'moose'});
|
|
|
|
|
}
|
|
|
|
|
render() {
|
|
|
|
|
return <div>{this.state.mouse}</div>;
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(<Mouse />);
|
2016-06-02 02:17:10 +08:00
|
|
|
|
|
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'div',
|
|
|
|
|
props: {},
|
|
|
|
|
children: ['mouse'],
|
|
|
|
|
});
|
|
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const mouse = renderer.getInstance();
|
2016-06-02 02:17:10 +08:00
|
|
|
mouse.handleMoose();
|
|
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'div',
|
|
|
|
|
children: ['moose'],
|
2017-01-12 03:19:32 +08:00
|
|
|
props: {},
|
2016-06-02 02:17:10 +08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2016-09-07 06:18:42 +08:00
|
|
|
it('updates types', () => {
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(<div>mouse</div>);
|
2016-07-13 13:35:31 +08:00
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'div',
|
|
|
|
|
props: {},
|
|
|
|
|
children: ['mouse'],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
renderer.update(<span>mice</span>);
|
|
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'span',
|
|
|
|
|
props: {},
|
|
|
|
|
children: ['mice'],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2016-09-07 06:18:42 +08:00
|
|
|
it('updates children', () => {
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(
|
2016-07-13 13:35:31 +08:00
|
|
|
<div>
|
|
|
|
|
<span key="a">A</span>
|
|
|
|
|
<span key="b">B</span>
|
|
|
|
|
<span key="c">C</span>
|
2017-03-14 08:05:18 +08:00
|
|
|
</div>,
|
2016-07-13 13:35:31 +08:00
|
|
|
);
|
|
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'div',
|
|
|
|
|
props: {},
|
|
|
|
|
children: [
|
|
|
|
|
{type: 'span', props: {}, children: ['A']},
|
|
|
|
|
{type: 'span', props: {}, children: ['B']},
|
|
|
|
|
{type: 'span', props: {}, children: ['C']},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
renderer.update(
|
|
|
|
|
<div>
|
|
|
|
|
<span key="d">D</span>
|
|
|
|
|
<span key="c">C</span>
|
|
|
|
|
<span key="b">B</span>
|
2017-03-14 08:05:18 +08:00
|
|
|
</div>,
|
2016-07-13 13:35:31 +08:00
|
|
|
);
|
|
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'div',
|
|
|
|
|
props: {},
|
|
|
|
|
children: [
|
|
|
|
|
{type: 'span', props: {}, children: ['D']},
|
|
|
|
|
{type: 'span', props: {}, children: ['C']},
|
|
|
|
|
{type: 'span', props: {}, children: ['B']},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2016-09-07 06:18:42 +08:00
|
|
|
it('does the full lifecycle', () => {
|
2017-12-01 08:01:45 +08:00
|
|
|
const log = [];
|
2016-07-13 13:35:31 +08:00
|
|
|
class Log extends React.Component {
|
|
|
|
|
render() {
|
|
|
|
|
log.push('render ' + this.props.name);
|
|
|
|
|
return <div />;
|
|
|
|
|
}
|
|
|
|
|
componentDidMount() {
|
|
|
|
|
log.push('mount ' + this.props.name);
|
|
|
|
|
}
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
|
log.push('unmount ' + this.props.name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(<Log key="foo" name="Foo" />);
|
2016-07-13 13:35:31 +08:00
|
|
|
renderer.update(<Log key="bar" name="Bar" />);
|
|
|
|
|
renderer.unmount();
|
|
|
|
|
|
|
|
|
|
expect(log).toEqual([
|
|
|
|
|
'render Foo',
|
|
|
|
|
'mount Foo',
|
|
|
|
|
'render Bar',
|
2016-12-15 03:14:50 +08:00
|
|
|
'unmount Foo',
|
2016-07-13 13:35:31 +08:00
|
|
|
'mount Bar',
|
|
|
|
|
'unmount Bar',
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
2016-09-07 06:18:42 +08:00
|
|
|
it('gives a ref to native components', () => {
|
2017-12-01 08:01:45 +08:00
|
|
|
const log = [];
|
2017-03-14 08:05:18 +08:00
|
|
|
ReactTestRenderer.create(<div ref={r => log.push(r)} />);
|
2016-07-13 13:35:31 +08:00
|
|
|
expect(log).toEqual([null]);
|
|
|
|
|
});
|
|
|
|
|
|
2016-09-07 06:18:42 +08:00
|
|
|
it('warns correctly for refs on SFCs', () => {
|
2016-09-06 16:49:20 +08:00
|
|
|
function Bar() {
|
2016-09-08 11:03:56 +08:00
|
|
|
return <div>Hello, world</div>;
|
2016-09-06 16:49:20 +08:00
|
|
|
}
|
|
|
|
|
class Foo extends React.Component {
|
|
|
|
|
render() {
|
2016-09-08 11:03:56 +08:00
|
|
|
return <Bar ref="foo" />;
|
2016-09-06 16:49:20 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
class Baz extends React.Component {
|
|
|
|
|
render() {
|
2016-09-08 11:03:56 +08:00
|
|
|
return <div ref="baz" />;
|
2016-09-06 16:49:20 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ReactTestRenderer.create(<Baz />);
|
2018-01-04 05:55:37 +08:00
|
|
|
expect(() => ReactTestRenderer.create(<Foo />)).toWarnDev(
|
|
|
|
|
'Warning: Stateless function components cannot be given refs. Attempts ' +
|
|
|
|
|
'to access this ref will fail.\n\nCheck the render method of `Foo`.\n' +
|
|
|
|
|
' in Bar (at **)\n' +
|
|
|
|
|
' in Foo (at **)',
|
|
|
|
|
);
|
2016-09-06 16:49:20 +08:00
|
|
|
});
|
|
|
|
|
|
2016-09-13 22:09:20 +08:00
|
|
|
it('allows an optional createNodeMock function', () => {
|
2017-12-01 08:01:45 +08:00
|
|
|
const mockDivInstance = {appendChild: () => {}};
|
|
|
|
|
const mockInputInstance = {focus: () => {}};
|
|
|
|
|
const mockListItemInstance = {click: () => {}};
|
|
|
|
|
const mockAnchorInstance = {hover: () => {}};
|
|
|
|
|
const log = [];
|
2016-09-13 22:09:20 +08:00
|
|
|
class Foo extends React.Component {
|
|
|
|
|
componentDidMount() {
|
|
|
|
|
log.push(this.refs.bar);
|
|
|
|
|
}
|
|
|
|
|
render() {
|
2017-03-14 08:05:18 +08:00
|
|
|
return <a ref="bar">Hello, world</a>;
|
2016-09-13 22:09:20 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function createNodeMock(element) {
|
|
|
|
|
switch (element.type) {
|
|
|
|
|
case 'div':
|
|
|
|
|
return mockDivInstance;
|
|
|
|
|
case 'input':
|
|
|
|
|
return mockInputInstance;
|
|
|
|
|
case 'li':
|
|
|
|
|
return mockListItemInstance;
|
|
|
|
|
case 'a':
|
|
|
|
|
return mockAnchorInstance;
|
|
|
|
|
default:
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-03-14 08:05:18 +08:00
|
|
|
ReactTestRenderer.create(<div ref={r => log.push(r)} />, {createNodeMock});
|
|
|
|
|
ReactTestRenderer.create(<input ref={r => log.push(r)} />, {
|
|
|
|
|
createNodeMock,
|
|
|
|
|
});
|
2016-09-13 22:09:20 +08:00
|
|
|
ReactTestRenderer.create(
|
|
|
|
|
<div>
|
|
|
|
|
<span>
|
|
|
|
|
<ul>
|
2017-03-14 08:05:18 +08:00
|
|
|
<li ref={r => log.push(r)} />
|
2016-09-13 22:09:20 +08:00
|
|
|
</ul>
|
|
|
|
|
<ul>
|
2017-03-14 08:05:18 +08:00
|
|
|
<li ref={r => log.push(r)} />
|
|
|
|
|
<li ref={r => log.push(r)} />
|
2016-09-13 22:09:20 +08:00
|
|
|
</ul>
|
|
|
|
|
</span>
|
|
|
|
|
</div>,
|
|
|
|
|
{createNodeMock, foobar: true},
|
|
|
|
|
);
|
2017-03-14 08:05:18 +08:00
|
|
|
ReactTestRenderer.create(<Foo />, {createNodeMock});
|
|
|
|
|
ReactTestRenderer.create(<div ref={r => log.push(r)} />);
|
|
|
|
|
ReactTestRenderer.create(<div ref={r => log.push(r)} />, {});
|
2016-09-13 22:09:20 +08:00
|
|
|
expect(log).toEqual([
|
|
|
|
|
mockDivInstance,
|
|
|
|
|
mockInputInstance,
|
|
|
|
|
mockListItemInstance,
|
|
|
|
|
mockListItemInstance,
|
|
|
|
|
mockListItemInstance,
|
|
|
|
|
mockAnchorInstance,
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
2016-11-11 22:35:18 +08:00
|
|
|
it('supports unmounting when using refs', () => {
|
|
|
|
|
class Foo extends React.Component {
|
|
|
|
|
render() {
|
|
|
|
|
return <div ref="foo" />;
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-03-14 08:05:18 +08:00
|
|
|
const inst = ReactTestRenderer.create(<Foo />, {
|
|
|
|
|
createNodeMock: () => 'foo',
|
|
|
|
|
});
|
2016-11-11 22:35:18 +08:00
|
|
|
expect(() => inst.unmount()).not.toThrow();
|
|
|
|
|
});
|
|
|
|
|
|
2016-12-14 23:57:21 +08:00
|
|
|
it('supports unmounting inner instances', () => {
|
|
|
|
|
let count = 0;
|
|
|
|
|
class Foo extends React.Component {
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
|
count++;
|
|
|
|
|
}
|
|
|
|
|
render() {
|
|
|
|
|
return <div />;
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-11-08 02:09:33 +08:00
|
|
|
const inst = ReactTestRenderer.create(
|
|
|
|
|
<div>
|
|
|
|
|
<Foo />
|
|
|
|
|
</div>,
|
|
|
|
|
{
|
|
|
|
|
createNodeMock: () => 'foo',
|
|
|
|
|
},
|
|
|
|
|
);
|
2016-12-14 23:57:21 +08:00
|
|
|
expect(() => inst.unmount()).not.toThrow();
|
|
|
|
|
expect(count).toEqual(1);
|
|
|
|
|
});
|
|
|
|
|
|
2016-11-11 22:35:18 +08:00
|
|
|
it('supports updates when using refs', () => {
|
|
|
|
|
const log = [];
|
|
|
|
|
const createNodeMock = element => {
|
|
|
|
|
log.push(element.type);
|
|
|
|
|
return element.type;
|
|
|
|
|
};
|
|
|
|
|
class Foo extends React.Component {
|
|
|
|
|
render() {
|
2017-03-14 08:05:18 +08:00
|
|
|
return this.props.useDiv ? <div ref="foo" /> : <span ref="foo" />;
|
2016-11-11 22:35:18 +08:00
|
|
|
}
|
|
|
|
|
}
|
2017-03-14 08:05:18 +08:00
|
|
|
const inst = ReactTestRenderer.create(<Foo useDiv={true} />, {
|
|
|
|
|
createNodeMock,
|
|
|
|
|
});
|
2016-11-11 22:35:18 +08:00
|
|
|
inst.update(<Foo useDiv={false} />);
|
2017-01-12 03:19:32 +08:00
|
|
|
expect(log).toEqual(['div', 'span']);
|
2016-11-11 22:35:18 +08:00
|
|
|
});
|
|
|
|
|
|
2016-09-07 06:18:42 +08:00
|
|
|
it('supports error boundaries', () => {
|
2017-12-01 08:01:45 +08:00
|
|
|
const log = [];
|
2016-08-26 06:35:55 +08:00
|
|
|
class Angry extends React.Component {
|
|
|
|
|
render() {
|
2016-08-26 09:11:20 +08:00
|
|
|
log.push('Angry render');
|
2016-08-26 06:35:55 +08:00
|
|
|
throw new Error('Please, do not render me.');
|
|
|
|
|
}
|
2018-03-17 02:18:50 +08:00
|
|
|
|
2016-08-26 09:11:20 +08:00
|
|
|
componentDidMount() {
|
|
|
|
|
log.push('Angry componentDidMount');
|
|
|
|
|
}
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
|
log.push('Angry componentWillUnmount');
|
|
|
|
|
}
|
2016-08-26 06:35:55 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Boundary extends React.Component {
|
|
|
|
|
constructor(props) {
|
|
|
|
|
super(props);
|
|
|
|
|
this.state = {error: false};
|
|
|
|
|
}
|
|
|
|
|
render() {
|
2016-08-26 09:11:20 +08:00
|
|
|
log.push('Boundary render');
|
2016-08-26 06:35:55 +08:00
|
|
|
if (!this.state.error) {
|
2016-08-26 09:11:20 +08:00
|
|
|
return (
|
2017-11-08 02:09:33 +08:00
|
|
|
<div>
|
|
|
|
|
<button onClick={this.onClick}>ClickMe</button>
|
|
|
|
|
<Angry />
|
|
|
|
|
</div>
|
2016-08-26 09:11:20 +08:00
|
|
|
);
|
2016-08-26 06:35:55 +08:00
|
|
|
} else {
|
2016-08-26 09:11:20 +08:00
|
|
|
return <div>Happy Birthday!</div>;
|
2016-08-26 06:35:55 +08:00
|
|
|
}
|
|
|
|
|
}
|
2016-08-26 09:11:20 +08:00
|
|
|
componentDidMount() {
|
|
|
|
|
log.push('Boundary componentDidMount');
|
|
|
|
|
}
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
|
log.push('Boundary componentWillUnmount');
|
|
|
|
|
}
|
2016-08-26 06:35:55 +08:00
|
|
|
onClick() {
|
|
|
|
|
/* do nothing */
|
|
|
|
|
}
|
2017-07-18 05:38:37 +08:00
|
|
|
componentDidCatch() {
|
Add stack unwinding phase for handling errors (#12201)
* Add stack unwinding phase for handling errors
A rewrite of error handling, with semantics that more closely match
stack unwinding.
Errors that are thrown during the render phase unwind to the nearest
error boundary, like before. But rather than synchronously unmount the
children before retrying, we restart the failed subtree within the same
render phase. The failed children are still unmounted (as if all their
keys changed) but without an extra commit.
Commit phase errors are different. They work by scheduling an error on
the update queue of the error boundary. When we enter the render phase,
the error is popped off the queue. The rest of the algorithm is
the same.
This approach is designed to work for throwing non-errors, too, though
that feature is not implemented yet.
* Add experimental getDerivedStateFromCatch lifecycle
Fires during the render phase, so you can recover from an error within the same
pass. This aligns error boundaries more closely with try-catch semantics.
Let's keep this behind a feature flag until a future release. For now, the
recommendation is to keep using componentDidCatch. Eventually, the advice will
be to use getDerivedStateFromCatch for handling errors and componentDidCatch
only for logging.
* Reconcile twice to remount failed children, instead of using a boolean
* Handle effect immediately after its thrown
This way we don't have to store the thrown values on the effect list.
* ReactFiberIncompleteWork -> ReactFiberUnwindWork
* Remove startTime
* Remove TypeOfException
We don't need it yet. We'll reconsider once we add another exception type.
* Move replay to outer catch block
This moves it out of the hot path.
2018-02-24 09:38:42 +08:00
|
|
|
log.push('Boundary componentDidCatch');
|
2016-08-26 06:35:55 +08:00
|
|
|
this.setState({error: true});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(<Boundary />);
|
2016-08-26 06:35:55 +08:00
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'div',
|
|
|
|
|
props: {},
|
|
|
|
|
children: ['Happy Birthday!'],
|
|
|
|
|
});
|
2017-06-15 05:10:33 +08:00
|
|
|
expect(log).toEqual([
|
|
|
|
|
'Boundary render',
|
|
|
|
|
'Angry render',
|
|
|
|
|
'Boundary componentDidMount',
|
Add stack unwinding phase for handling errors (#12201)
* Add stack unwinding phase for handling errors
A rewrite of error handling, with semantics that more closely match
stack unwinding.
Errors that are thrown during the render phase unwind to the nearest
error boundary, like before. But rather than synchronously unmount the
children before retrying, we restart the failed subtree within the same
render phase. The failed children are still unmounted (as if all their
keys changed) but without an extra commit.
Commit phase errors are different. They work by scheduling an error on
the update queue of the error boundary. When we enter the render phase,
the error is popped off the queue. The rest of the algorithm is
the same.
This approach is designed to work for throwing non-errors, too, though
that feature is not implemented yet.
* Add experimental getDerivedStateFromCatch lifecycle
Fires during the render phase, so you can recover from an error within the same
pass. This aligns error boundaries more closely with try-catch semantics.
Let's keep this behind a feature flag until a future release. For now, the
recommendation is to keep using componentDidCatch. Eventually, the advice will
be to use getDerivedStateFromCatch for handling errors and componentDidCatch
only for logging.
* Reconcile twice to remount failed children, instead of using a boolean
* Handle effect immediately after its thrown
This way we don't have to store the thrown values on the effect list.
* ReactFiberIncompleteWork -> ReactFiberUnwindWork
* Remove startTime
* Remove TypeOfException
We don't need it yet. We'll reconsider once we add another exception type.
* Move replay to outer catch block
This moves it out of the hot path.
2018-02-24 09:38:42 +08:00
|
|
|
'Boundary componentDidCatch',
|
2017-06-15 05:10:33 +08:00
|
|
|
'Boundary render',
|
|
|
|
|
]);
|
2016-08-26 06:35:55 +08:00
|
|
|
});
|
|
|
|
|
|
2017-01-12 03:19:32 +08:00
|
|
|
it('can update text nodes', () => {
|
|
|
|
|
class Component extends React.Component {
|
|
|
|
|
render() {
|
2017-11-08 02:09:33 +08:00
|
|
|
return <div>{this.props.children}</div>;
|
2017-01-12 03:19:32 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(<Component>Hi</Component>);
|
2017-01-12 03:19:32 +08:00
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'div',
|
|
|
|
|
children: ['Hi'],
|
|
|
|
|
props: {},
|
|
|
|
|
});
|
|
|
|
|
renderer.update(<Component>{['Hi', 'Bye']}</Component>);
|
|
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'div',
|
|
|
|
|
children: ['Hi', 'Bye'],
|
|
|
|
|
props: {},
|
|
|
|
|
});
|
|
|
|
|
renderer.update(<Component>Bye</Component>);
|
|
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'div',
|
|
|
|
|
children: ['Bye'],
|
|
|
|
|
props: {},
|
|
|
|
|
});
|
|
|
|
|
renderer.update(<Component>{42}</Component>);
|
|
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'div',
|
2017-06-15 05:10:33 +08:00
|
|
|
children: ['42'],
|
2017-01-12 03:19:32 +08:00
|
|
|
props: {},
|
|
|
|
|
});
|
2017-11-08 02:09:33 +08:00
|
|
|
renderer.update(
|
|
|
|
|
<Component>
|
|
|
|
|
<div />
|
|
|
|
|
</Component>,
|
|
|
|
|
);
|
2017-01-12 03:19:32 +08:00
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'div',
|
2017-03-14 08:05:18 +08:00
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
type: 'div',
|
|
|
|
|
children: null,
|
|
|
|
|
props: {},
|
|
|
|
|
},
|
|
|
|
|
],
|
2017-01-12 03:19:32 +08:00
|
|
|
props: {},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2017-02-10 04:55:00 +08:00
|
|
|
it('toTree() renders simple components returning host components', () => {
|
2017-12-01 08:01:45 +08:00
|
|
|
const Qoo = () => <span className="Qoo">Hello World!</span>;
|
2017-02-10 04:55:00 +08:00
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(<Qoo />);
|
|
|
|
|
const tree = renderer.toTree();
|
2017-02-10 04:55:00 +08:00
|
|
|
|
2017-09-15 05:15:43 +08:00
|
|
|
cleanNodeOrArray(tree);
|
2017-02-10 04:55:00 +08:00
|
|
|
|
2017-03-14 08:05:18 +08:00
|
|
|
expect(prettyFormat(tree)).toEqual(
|
|
|
|
|
prettyFormat({
|
|
|
|
|
nodeType: 'component',
|
|
|
|
|
type: Qoo,
|
|
|
|
|
props: {},
|
2017-02-10 04:55:00 +08:00
|
|
|
instance: null,
|
2017-03-14 08:05:18 +08:00
|
|
|
rendered: {
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
type: 'span',
|
|
|
|
|
props: {className: 'Qoo'},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: ['Hello World!'],
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
);
|
2017-02-10 04:55:00 +08:00
|
|
|
});
|
|
|
|
|
|
2018-01-28 07:03:27 +08:00
|
|
|
it('toTree() handles nested Fragments', () => {
|
|
|
|
|
const Foo = () => (
|
|
|
|
|
<React.Fragment>
|
|
|
|
|
<React.Fragment>foo</React.Fragment>
|
|
|
|
|
</React.Fragment>
|
|
|
|
|
);
|
|
|
|
|
const renderer = ReactTestRenderer.create(<Foo />);
|
|
|
|
|
const tree = renderer.toTree();
|
|
|
|
|
|
|
|
|
|
cleanNodeOrArray(tree);
|
|
|
|
|
|
|
|
|
|
expect(prettyFormat(tree)).toEqual(
|
|
|
|
|
prettyFormat({
|
|
|
|
|
nodeType: 'component',
|
|
|
|
|
type: Foo,
|
|
|
|
|
instance: null,
|
|
|
|
|
props: {},
|
|
|
|
|
rendered: 'foo',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2017-02-10 04:55:00 +08:00
|
|
|
it('toTree() handles null rendering components', () => {
|
|
|
|
|
class Foo extends React.Component {
|
|
|
|
|
render() {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(<Foo />);
|
|
|
|
|
const tree = renderer.toTree();
|
2017-02-10 04:55:00 +08:00
|
|
|
|
|
|
|
|
expect(tree.instance).toBeInstanceOf(Foo);
|
|
|
|
|
|
2017-09-15 05:15:43 +08:00
|
|
|
cleanNodeOrArray(tree);
|
2017-02-10 04:55:00 +08:00
|
|
|
|
|
|
|
|
expect(tree).toEqual({
|
|
|
|
|
type: Foo,
|
|
|
|
|
nodeType: 'component',
|
2017-03-14 08:05:18 +08:00
|
|
|
props: {},
|
2017-02-10 04:55:00 +08:00
|
|
|
instance: null,
|
|
|
|
|
rendered: null,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2017-09-15 05:15:43 +08:00
|
|
|
it('toTree() handles simple components that return arrays', () => {
|
|
|
|
|
const Foo = ({children}) => children;
|
|
|
|
|
|
|
|
|
|
const renderer = ReactTestRenderer.create(
|
|
|
|
|
<Foo>
|
|
|
|
|
<div>One</div>
|
|
|
|
|
<div>Two</div>
|
|
|
|
|
</Foo>,
|
|
|
|
|
);
|
|
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const tree = renderer.toTree();
|
2017-09-15 05:15:43 +08:00
|
|
|
|
|
|
|
|
cleanNodeOrArray(tree);
|
|
|
|
|
|
|
|
|
|
expect(prettyFormat(tree)).toEqual(
|
|
|
|
|
prettyFormat({
|
|
|
|
|
type: Foo,
|
|
|
|
|
nodeType: 'component',
|
|
|
|
|
props: {},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: [
|
|
|
|
|
{
|
|
|
|
|
instance: null,
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {},
|
|
|
|
|
rendered: ['One'],
|
|
|
|
|
type: 'div',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
instance: null,
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {},
|
|
|
|
|
rendered: ['Two'],
|
|
|
|
|
type: 'div',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2018-02-06 00:55:48 +08:00
|
|
|
it('toTree() handles complicated tree of arrays', () => {
|
2017-09-15 05:15:43 +08:00
|
|
|
class Foo extends React.Component {
|
|
|
|
|
render() {
|
|
|
|
|
return this.props.children;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const renderer = ReactTestRenderer.create(
|
|
|
|
|
<div>
|
|
|
|
|
<Foo>
|
|
|
|
|
<div>One</div>
|
|
|
|
|
<div>Two</div>
|
|
|
|
|
<Foo>
|
|
|
|
|
<div>Three</div>
|
|
|
|
|
</Foo>
|
|
|
|
|
</Foo>
|
|
|
|
|
<div>Four</div>
|
|
|
|
|
</div>,
|
|
|
|
|
);
|
|
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const tree = renderer.toTree();
|
2017-09-15 05:15:43 +08:00
|
|
|
|
|
|
|
|
cleanNodeOrArray(tree);
|
|
|
|
|
|
|
|
|
|
expect(prettyFormat(tree)).toEqual(
|
|
|
|
|
prettyFormat({
|
|
|
|
|
type: 'div',
|
|
|
|
|
instance: null,
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {},
|
|
|
|
|
rendered: [
|
|
|
|
|
{
|
|
|
|
|
type: Foo,
|
|
|
|
|
nodeType: 'component',
|
|
|
|
|
props: {},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: [
|
|
|
|
|
{
|
|
|
|
|
type: 'div',
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: ['One'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'div',
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: ['Two'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: Foo,
|
|
|
|
|
nodeType: 'component',
|
|
|
|
|
props: {},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: {
|
|
|
|
|
type: 'div',
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: ['Three'],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'div',
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: ['Four'],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2018-02-06 00:55:48 +08:00
|
|
|
it('toTree() handles complicated tree of fragments', () => {
|
|
|
|
|
const renderer = ReactTestRenderer.create(
|
|
|
|
|
<React.Fragment>
|
|
|
|
|
<React.Fragment>
|
|
|
|
|
<div>One</div>
|
|
|
|
|
<div>Two</div>
|
|
|
|
|
<React.Fragment>
|
|
|
|
|
<div>Three</div>
|
|
|
|
|
</React.Fragment>
|
|
|
|
|
</React.Fragment>
|
|
|
|
|
<div>Four</div>
|
|
|
|
|
</React.Fragment>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const tree = renderer.toTree();
|
|
|
|
|
|
|
|
|
|
cleanNodeOrArray(tree);
|
|
|
|
|
|
|
|
|
|
expect(prettyFormat(tree)).toEqual(
|
|
|
|
|
prettyFormat([
|
|
|
|
|
{
|
|
|
|
|
type: 'div',
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: ['One'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'div',
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: ['Two'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'div',
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: ['Three'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'div',
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: ['Four'],
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2017-05-31 03:15:48 +08:00
|
|
|
it('root instance and createNodeMock ref return the same value', () => {
|
2017-12-01 08:01:45 +08:00
|
|
|
const createNodeMock = ref => ({node: ref});
|
|
|
|
|
let refInst = null;
|
|
|
|
|
const renderer = ReactTestRenderer.create(
|
2017-05-31 03:15:48 +08:00
|
|
|
<div ref={ref => (refInst = ref)} />,
|
|
|
|
|
{createNodeMock},
|
|
|
|
|
);
|
2017-12-01 08:01:45 +08:00
|
|
|
const root = renderer.getInstance();
|
2017-05-31 03:15:48 +08:00
|
|
|
expect(root).toEqual(refInst);
|
|
|
|
|
});
|
|
|
|
|
|
2017-02-10 04:55:00 +08:00
|
|
|
it('toTree() renders complicated trees of composites and hosts', () => {
|
|
|
|
|
// SFC returning host. no children props.
|
2017-12-01 08:01:45 +08:00
|
|
|
const Qoo = () => <span className="Qoo">Hello World!</span>;
|
2017-02-10 04:55:00 +08:00
|
|
|
|
|
|
|
|
// SFC returning host. passes through children.
|
2017-12-01 08:01:45 +08:00
|
|
|
const Foo = ({className, children}) => (
|
2017-02-10 04:55:00 +08:00
|
|
|
<div className={'Foo ' + className}>
|
|
|
|
|
<span className="Foo2">Literal</span>
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// class composite returning composite. passes through children.
|
|
|
|
|
class Bar extends React.Component {
|
|
|
|
|
render() {
|
2017-03-14 08:05:18 +08:00
|
|
|
const {special, children} = this.props;
|
2017-11-08 02:09:33 +08:00
|
|
|
return <Foo className={special ? 'special' : 'normal'}>{children}</Foo>;
|
2017-02-10 04:55:00 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// class composite return composite. no children props.
|
|
|
|
|
class Bam extends React.Component {
|
|
|
|
|
render() {
|
|
|
|
|
return (
|
|
|
|
|
<Bar special={true}>
|
|
|
|
|
<Qoo />
|
|
|
|
|
</Bar>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(<Bam />);
|
|
|
|
|
const tree = renderer.toTree();
|
2017-02-10 04:55:00 +08:00
|
|
|
|
|
|
|
|
// we test for the presence of instances before nulling them out
|
|
|
|
|
expect(tree.instance).toBeInstanceOf(Bam);
|
|
|
|
|
expect(tree.rendered.instance).toBeInstanceOf(Bar);
|
|
|
|
|
|
2017-09-15 05:15:43 +08:00
|
|
|
cleanNodeOrArray(tree);
|
2017-02-10 04:55:00 +08:00
|
|
|
|
2017-03-14 08:05:18 +08:00
|
|
|
expect(prettyFormat(tree)).toEqual(
|
|
|
|
|
prettyFormat({
|
|
|
|
|
type: Bam,
|
2017-02-10 04:55:00 +08:00
|
|
|
nodeType: 'component',
|
2017-03-14 08:05:18 +08:00
|
|
|
props: {},
|
2017-02-10 04:55:00 +08:00
|
|
|
instance: null,
|
|
|
|
|
rendered: {
|
2017-03-14 08:05:18 +08:00
|
|
|
type: Bar,
|
2017-02-10 04:55:00 +08:00
|
|
|
nodeType: 'component',
|
2017-03-14 08:05:18 +08:00
|
|
|
props: {special: true},
|
2017-02-10 04:55:00 +08:00
|
|
|
instance: null,
|
|
|
|
|
rendered: {
|
2017-03-14 08:05:18 +08:00
|
|
|
type: Foo,
|
|
|
|
|
nodeType: 'component',
|
|
|
|
|
props: {className: 'special'},
|
2017-02-10 04:55:00 +08:00
|
|
|
instance: null,
|
2017-03-14 08:05:18 +08:00
|
|
|
rendered: {
|
|
|
|
|
type: 'div',
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {className: 'Foo special'},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: [
|
|
|
|
|
{
|
2017-02-10 04:55:00 +08:00
|
|
|
type: 'span',
|
|
|
|
|
nodeType: 'host',
|
2017-03-14 08:05:18 +08:00
|
|
|
props: {className: 'Foo2'},
|
2017-02-10 04:55:00 +08:00
|
|
|
instance: null,
|
2017-03-14 08:05:18 +08:00
|
|
|
rendered: ['Literal'],
|
2017-02-10 04:55:00 +08:00
|
|
|
},
|
2017-03-14 08:05:18 +08:00
|
|
|
{
|
|
|
|
|
type: Qoo,
|
|
|
|
|
nodeType: 'component',
|
|
|
|
|
props: {},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: {
|
|
|
|
|
type: 'span',
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {className: 'Qoo'},
|
|
|
|
|
instance: null,
|
|
|
|
|
rendered: ['Hello World!'],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
2017-02-10 04:55:00 +08:00
|
|
|
},
|
|
|
|
|
},
|
2017-03-14 08:05:18 +08:00
|
|
|
}),
|
|
|
|
|
);
|
2017-02-10 04:55:00 +08:00
|
|
|
});
|
|
|
|
|
|
2017-06-15 05:10:33 +08:00
|
|
|
it('can update text nodes when rendered as root', () => {
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create(['Hello', 'world']);
|
2017-06-15 05:10:33 +08:00
|
|
|
expect(renderer.toJSON()).toEqual(['Hello', 'world']);
|
|
|
|
|
renderer.update(42);
|
|
|
|
|
expect(renderer.toJSON()).toEqual('42');
|
|
|
|
|
renderer.update([42, 'world']);
|
|
|
|
|
expect(renderer.toJSON()).toEqual(['42', 'world']);
|
|
|
|
|
});
|
2017-01-12 03:19:32 +08:00
|
|
|
|
2017-06-15 05:10:33 +08:00
|
|
|
it('can render and update root fragments', () => {
|
2017-12-01 08:01:45 +08:00
|
|
|
const Component = props => props.children;
|
2017-01-12 03:19:32 +08:00
|
|
|
|
2017-12-01 08:01:45 +08:00
|
|
|
const renderer = ReactTestRenderer.create([
|
2017-06-15 05:10:33 +08:00
|
|
|
<Component key="a">Hi</Component>,
|
|
|
|
|
<Component key="b">Bye</Component>,
|
|
|
|
|
]);
|
|
|
|
|
expect(renderer.toJSON()).toEqual(['Hi', 'Bye']);
|
|
|
|
|
renderer.update(<div />);
|
|
|
|
|
expect(renderer.toJSON()).toEqual({
|
|
|
|
|
type: 'div',
|
|
|
|
|
children: null,
|
|
|
|
|
props: {},
|
|
|
|
|
});
|
|
|
|
|
renderer.update([<div key="a">goodbye</div>, 'world']);
|
|
|
|
|
expect(renderer.toJSON()).toEqual([
|
|
|
|
|
{
|
2017-01-12 03:19:32 +08:00
|
|
|
type: 'div',
|
2017-06-15 05:10:33 +08:00
|
|
|
children: ['goodbye'],
|
2017-01-12 03:19:32 +08:00
|
|
|
props: {},
|
2017-06-15 05:10:33 +08:00
|
|
|
},
|
|
|
|
|
'world',
|
|
|
|
|
]);
|
|
|
|
|
});
|
2018-02-17 03:27:20 +08:00
|
|
|
|
|
|
|
|
it('supports context providers and consumers', () => {
|
|
|
|
|
const {Consumer, Provider} = React.createContext('a');
|
|
|
|
|
|
|
|
|
|
function Child(props) {
|
|
|
|
|
return props.value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
|
return (
|
|
|
|
|
<Provider value="b">
|
|
|
|
|
<Consumer>{value => <Child value={value} />}</Consumer>
|
|
|
|
|
</Provider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const renderer = ReactTestRenderer.create(<App />);
|
|
|
|
|
const child = renderer.root.findByType(Child);
|
|
|
|
|
expect(child.children).toEqual(['b']);
|
|
|
|
|
expect(prettyFormat(renderer.toTree())).toEqual(
|
|
|
|
|
prettyFormat({
|
|
|
|
|
instance: null,
|
|
|
|
|
nodeType: 'component',
|
|
|
|
|
props: {},
|
|
|
|
|
rendered: {
|
|
|
|
|
instance: null,
|
|
|
|
|
nodeType: 'component',
|
|
|
|
|
props: {
|
|
|
|
|
value: 'b',
|
|
|
|
|
},
|
|
|
|
|
rendered: 'b',
|
|
|
|
|
type: Child,
|
|
|
|
|
},
|
|
|
|
|
type: App,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('supports modes', () => {
|
|
|
|
|
function Child(props) {
|
|
|
|
|
return props.value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function App(props) {
|
|
|
|
|
return (
|
|
|
|
|
<React.StrictMode>
|
|
|
|
|
<Child value={props.value} />
|
|
|
|
|
</React.StrictMode>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const renderer = ReactTestRenderer.create(<App value="a" />);
|
|
|
|
|
const child = renderer.root.findByType(Child);
|
|
|
|
|
expect(child.children).toEqual(['a']);
|
|
|
|
|
expect(prettyFormat(renderer.toTree())).toEqual(
|
|
|
|
|
prettyFormat({
|
|
|
|
|
instance: null,
|
|
|
|
|
nodeType: 'component',
|
|
|
|
|
props: {
|
|
|
|
|
value: 'a',
|
|
|
|
|
},
|
|
|
|
|
rendered: {
|
|
|
|
|
instance: null,
|
|
|
|
|
nodeType: 'component',
|
|
|
|
|
props: {
|
|
|
|
|
value: 'a',
|
|
|
|
|
},
|
|
|
|
|
rendered: 'a',
|
|
|
|
|
type: Child,
|
|
|
|
|
},
|
|
|
|
|
type: App,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
2018-03-17 02:18:50 +08:00
|
|
|
|
|
|
|
|
it('supports forwardRef', () => {
|
|
|
|
|
const InnerRefed = React.forwardRef((props, ref) => (
|
|
|
|
|
<div>
|
|
|
|
|
<span ref={ref} />
|
|
|
|
|
</div>
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
class App extends React.Component {
|
|
|
|
|
render() {
|
|
|
|
|
return <InnerRefed ref={r => (this.ref = r)} />;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const renderer = ReactTestRenderer.create(<App />);
|
|
|
|
|
const tree = renderer.toTree();
|
|
|
|
|
cleanNodeOrArray(tree);
|
|
|
|
|
|
|
|
|
|
expect(prettyFormat(tree)).toEqual(
|
|
|
|
|
prettyFormat({
|
|
|
|
|
instance: null,
|
|
|
|
|
nodeType: 'component',
|
|
|
|
|
props: {},
|
|
|
|
|
rendered: {
|
|
|
|
|
instance: null,
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {},
|
|
|
|
|
rendered: [
|
|
|
|
|
{
|
|
|
|
|
instance: null,
|
|
|
|
|
nodeType: 'host',
|
|
|
|
|
props: {},
|
|
|
|
|
rendered: [],
|
|
|
|
|
type: 'span',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
type: 'div',
|
|
|
|
|
},
|
|
|
|
|
type: App,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
2018-07-07 07:04:45 +08:00
|
|
|
|
|
|
|
|
it('can concurrently render context with a "primary" renderer', () => {
|
|
|
|
|
const Context = React.createContext(null);
|
|
|
|
|
const Indirection = React.Fragment;
|
|
|
|
|
const App = () => (
|
|
|
|
|
<Context.Provider>
|
|
|
|
|
<Indirection>
|
|
|
|
|
<Context.Consumer>{() => null}</Context.Consumer>
|
|
|
|
|
</Indirection>
|
|
|
|
|
</Context.Provider>
|
|
|
|
|
);
|
|
|
|
|
ReactNoop.render(<App />);
|
|
|
|
|
ReactNoop.flush();
|
|
|
|
|
ReactTestRenderer.create(<App />);
|
|
|
|
|
});
|
2016-06-02 02:17:10 +08:00
|
|
|
});
|