Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New] add componentDidCatch support, and simulateError #1797

Merged
merged 3 commits into from
Sep 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
* [setState(nextState[, callback])](/docs/api/ShallowWrapper/setState.md)
* [shallow([options])](/docs/api/ShallowWrapper/shallow.md)
* [simulate(event[, data])](/docs/api/ShallowWrapper/simulate.md)
* [simulateError(error)](/docs/api/ShallowWrapper/simulateError.md)
* [slice([begin[, end]])](/docs/api/ShallowWrapper/slice.md)
* [some(selector)](/docs/api/ShallowWrapper/some.md)
* [someWhere(predicate)](/docs/api/ShallowWrapper/someWhere.md)
Expand Down Expand Up @@ -121,6 +122,7 @@
* [setProps(nextProps[, callback])](/docs/api/ReactWrapper/setProps.md)
* [setState(nextState[, callback])](/docs/api/ReactWrapper/setState.md)
* [simulate(event[, data])](/docs/api/ReactWrapper/simulate.md)
* [simulateError(error)](/docs/api/ReactWrapper/simulateError.md)
* [slice([begin[, end]])](/docs/api/ReactWrapper/slice.md)
* [some(selector)](/docs/api/ReactWrapper/some.md)
* [someWhere(predicate)](/docs/api/ReactWrapper/someWhere.md)
Expand Down
65 changes: 65 additions & 0 deletions docs/api/ReactWrapper/simulateError.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# `.simulateError(error) => Self`

Simulate a component throwing an error as part of its rendering lifecycle.

This is particularly useful in combination with React 16 error boundaries (ie, the `componentDidCatch` lifecycle method).


#### Arguments

1. `error` (`Any`): The error to throw.



#### Returns

`ReactWrapper`: Returns itself.



#### Example

```jsx
function Something() {
// this is just a placeholder
return null;
}

class ErrorBoundary extends React.Component {
componentDidCatch(error, info) {
const { spy } = this.props;
spy(error, info);
}

render() {
const { children } = this.props;
return (
<React.Fragment>
{children}
</React.Fragment>
);
}
}
ErrorBoundary.propTypes = {
children: PropTypes.node.isRequired,
spy: PropTypes.func.isRequired,
};

const spy = sinon.spy();
const wrapper = mount(<ErrorBoundary spy={spy}><Something /></ErrorBoundary>);
const error = new Error('hi!');
wrapper.find(Something).simulateError(error);

expect(spy).to.have.property('callCount', 1);
expect(spy.args).to.deep.equal([
error,
{
componentStack: `
in Something (created by ErrorBoundary)
in ErrorBoundary (created by WrapperComponent)
in WrapperComponent`,
},
]);
```


65 changes: 65 additions & 0 deletions docs/api/ShallowWrapper/simulateError.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# `.simulateError(error) => Self`

Simulate a component throwing an error as part of its rendering lifecycle.

This is particularly useful in combination with React 16 error boundaries (ie, the `componentDidCatch` lifecycle method).


#### Arguments

1. `error` (`Any`): The error to throw.



#### Returns

`ShallowWrapper`: Returns itself.



#### Example

```jsx
function Something() {
// this is just a placeholder
return null;
}

class ErrorBoundary extends React.Component {
componentDidCatch(error, info) {
const { spy } = this.props;
spy(error, info);
}

render() {
const { children } = this.props;
return (
<React.Fragment>
{children}
</React.Fragment>
);
}
}
ErrorBoundary.propTypes = {
children: PropTypes.node.isRequired,
spy: PropTypes.func.isRequired,
};

const spy = sinon.spy();
const wrapper = shallow(<ErrorBoundary spy={spy}><Something /></ErrorBoundary>);
const error = new Error('hi!');
wrapper.find(Something).simulateError(error);

expect(spy).to.have.property('callCount', 1);
expect(spy.args).to.deep.equal([
error,
{
componentStack: `
in Something (created by ErrorBoundary)
in ErrorBoundary (created by WrapperComponent)
in WrapperComponent`,
},
]);
```


25 changes: 25 additions & 0 deletions packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
createMountWrapper,
propsWithKeysAndRef,
ensureKeyOrUndefined,
simulateError,
} from 'enzyme-adapter-utils';
import { findCurrentFiberUsingSlowPath } from 'react-reconciler/reflection';

Expand Down Expand Up @@ -262,6 +263,19 @@ class ReactSixteenOneAdapter extends EnzymeAdapter {
getNode() {
return instance ? toTree(instance._reactInternalFiber).rendered : null;
},
simulateError(nodeHierarchy, rootNode, error) {
const { instance: catchingInstance } = nodeHierarchy
.find(x => x.instance && x.instance.componentDidCatch) || {};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I have multiple error boundary components in my tree, will this throw on the closest one to the current node?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should throw on the first one it finds as it traverses upwards.


simulateError(
error,
catchingInstance,
rootNode,
nodeHierarchy,
nodeTypeFromType,
adapter.displayNameOfNode,
);
},
simulateEvent(node, event, mock) {
const mappedEvent = mapNativeEventNames(event, eventOptions);
const eventFn = TestUtils.Simulate[mappedEvent];
Expand All @@ -279,6 +293,7 @@ class ReactSixteenOneAdapter extends EnzymeAdapter {
}

createShallowRenderer(/* options */) {
const adapter = this;
const renderer = new ShallowRenderer();
let isDOM = false;
let cachedNode = null;
Expand Down Expand Up @@ -327,6 +342,16 @@ class ReactSixteenOneAdapter extends EnzymeAdapter {
: elementToTree(output),
};
},
simulateError(nodeHierarchy, rootNode, error) {
simulateError(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not check the nodeHierarchy for error boundaries in a shallow renderer? Should this scenario require a mount renderer?

// ErrorBoundary is a component with a componentDidCatch method that renders null
// if an error is encountered.
function MyComponent({children}) {
    return (
        <div>
            <h3>My component is cool!</h3>
            <ErrorBoundary>
                {children}
            </ErrorBoundary>
        </div>
    );
}

const BadChild = () => null;

const wrapper = shallow(
    <MyComponent>
        <BadChild />
    </MyComponent>
);
wrapper.find(BadChild).simulateError(new Error('That was bad'));

expect(wrapper.find('h3')).toExist();

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a shallow render, there can't ever be any other error boundaries, since only the root node is actually rendered.

Copy link

@GreenGremlin GreenGremlin Sep 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but if simulateError is called on an element in shallow render tree, it seems reasonable to render any error boundaries found above that element.

This actually seems to work:

const wrapper = shallow(
    <MyComponent>
        <BadChild />
    </MyComponent>
);
const errorBoundary = wrapper.find('ErrorBoundary').shallow();
errorBoundary.find(BadChild).simulateError(new Error('That was bad'));
expect(wrapper.find('h3')).toExist();

...but it doesn't really feel intuitive. I can see people trying my previous example and being confused when it doesn't work, especially since I did just that :\

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not reasonable or possible, because of how shallow rendering works. In shallow rendering, everything that's not the root node is treated as if it's a div - ie, as if the component implementation is ({ children }) => children. Thus, there's no componentDidCatch anywhere, except the top.

error,
renderer._instance,
cachedNode,
nodeHierarchy.concat(cachedNode),
nodeTypeFromType,
adapter.displayNameOfNode,
);
},
simulateEvent(node, event, ...args) {
const handler = node.props[propFromEvent(event, eventOptions)];
if (handler) {
Expand Down
25 changes: 25 additions & 0 deletions packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
createMountWrapper,
propsWithKeysAndRef,
ensureKeyOrUndefined,
simulateError,
} from 'enzyme-adapter-utils';
import { findCurrentFiberUsingSlowPath } from 'react-reconciler/reflection';

Expand Down Expand Up @@ -264,6 +265,19 @@ class ReactSixteenTwoAdapter extends EnzymeAdapter {
getNode() {
return instance ? toTree(instance._reactInternalFiber).rendered : null;
},
simulateError(nodeHierarchy, rootNode, error) {
const { instance: catchingInstance } = nodeHierarchy
.find(x => x.instance && x.instance.componentDidCatch) || {};

simulateError(
error,
catchingInstance,
rootNode,
nodeHierarchy,
nodeTypeFromType,
adapter.displayNameOfNode,
);
},
simulateEvent(node, event, mock) {
const mappedEvent = mapNativeEventNames(event, eventOptions);
const eventFn = TestUtils.Simulate[mappedEvent];
Expand All @@ -281,6 +295,7 @@ class ReactSixteenTwoAdapter extends EnzymeAdapter {
}

createShallowRenderer(/* options */) {
const adapter = this;
const renderer = new ShallowRenderer();
let isDOM = false;
let cachedNode = null;
Expand Down Expand Up @@ -329,6 +344,16 @@ class ReactSixteenTwoAdapter extends EnzymeAdapter {
: elementToTree(output),
};
},
simulateError(nodeHierarchy, rootNode, error) {
simulateError(
error,
renderer._instance,
cachedNode,
nodeHierarchy.concat(cachedNode),
nodeTypeFromType,
adapter.displayNameOfNode,
);
},
simulateEvent(node, event, ...args) {
const handler = node.props[propFromEvent(event, eventOptions)];
if (handler) {
Expand Down
25 changes: 25 additions & 0 deletions packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
createMountWrapper,
propsWithKeysAndRef,
ensureKeyOrUndefined,
simulateError,
} from 'enzyme-adapter-utils';
import { findCurrentFiberUsingSlowPath } from 'react-reconciler/reflection';

Expand Down Expand Up @@ -283,6 +284,19 @@ class ReactSixteenThreeAdapter extends EnzymeAdapter {
getNode() {
return instance ? toTree(instance._reactInternalFiber).rendered : null;
},
simulateError(nodeHierarchy, rootNode, error) {
const { instance: catchingInstance } = nodeHierarchy
.find(x => x.instance && x.instance.componentDidCatch) || {};

simulateError(
error,
catchingInstance,
rootNode,
nodeHierarchy,
nodeTypeFromType,
adapter.displayNameOfNode,
);
},
simulateEvent(node, event, mock) {
const mappedEvent = mapNativeEventNames(event, eventOptions);
const eventFn = TestUtils.Simulate[mappedEvent];
Expand All @@ -300,6 +314,7 @@ class ReactSixteenThreeAdapter extends EnzymeAdapter {
}

createShallowRenderer(/* options */) {
const adapter = this;
const renderer = new ShallowRenderer();
let isDOM = false;
let cachedNode = null;
Expand Down Expand Up @@ -348,6 +363,16 @@ class ReactSixteenThreeAdapter extends EnzymeAdapter {
: elementToTree(output),
};
},
simulateError(nodeHierarchy, rootNode, error) {
simulateError(
error,
renderer._instance,
cachedNode,
nodeHierarchy.concat(cachedNode),
nodeTypeFromType,
adapter.displayNameOfNode,
);
},
simulateEvent(node, event, ...args) {
const handler = node.props[propFromEvent(event, eventOptions)];
if (handler) {
Expand Down
25 changes: 25 additions & 0 deletions packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
createMountWrapper,
propsWithKeysAndRef,
ensureKeyOrUndefined,
simulateError,
} from 'enzyme-adapter-utils';
import findCurrentFiberUsingSlowPath from './findCurrentFiberUsingSlowPath';
import detectFiberTags from './detectFiberTags';
Expand Down Expand Up @@ -284,6 +285,19 @@ class ReactSixteenAdapter extends EnzymeAdapter {
getNode() {
return instance ? toTree(instance._reactInternalFiber).rendered : null;
},
simulateError(nodeHierarchy, rootNode, error) {
const { instance: catchingInstance } = nodeHierarchy
.find(x => x.instance && x.instance.componentDidCatch) || {};

simulateError(
error,
catchingInstance,
rootNode,
nodeHierarchy,
nodeTypeFromType,
adapter.displayNameOfNode,
);
},
simulateEvent(node, event, mock) {
const mappedEvent = mapNativeEventNames(event, eventOptions);
const eventFn = TestUtils.Simulate[mappedEvent];
Expand All @@ -300,6 +314,7 @@ class ReactSixteenAdapter extends EnzymeAdapter {
}

createShallowRenderer(/* options */) {
const adapter = this;
const renderer = new ShallowRenderer();
let isDOM = false;
let cachedNode = null;
Expand Down Expand Up @@ -348,6 +363,16 @@ class ReactSixteenAdapter extends EnzymeAdapter {
: elementToTree(output),
};
},
simulateError(nodeHierarchy, rootNode, error) {
simulateError(
error,
renderer._instance,
cachedNode,
nodeHierarchy.concat(cachedNode),
nodeTypeFromType,
adapter.displayNameOfNode,
);
},
simulateEvent(node, event, ...args) {
const handler = node.props[propFromEvent(event, eventOptions)];
if (handler) {
Expand Down
Loading