Skip to content

Commit 944f9e0

Browse files
committed
[New] shallow/mount: add simulateError
1 parent 66f27e9 commit 944f9e0

File tree

7 files changed

+566
-2
lines changed

7 files changed

+566
-2
lines changed

SUMMARY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
* [setState(nextState[, callback])](/docs/api/ShallowWrapper/setState.md)
6666
* [shallow([options])](/docs/api/ShallowWrapper/shallow.md)
6767
* [simulate(event[, data])](/docs/api/ShallowWrapper/simulate.md)
68+
* [simulateError(error)](/docs/api/ShallowWrapper/simulateError.md)
6869
* [slice([begin[, end]])](/docs/api/ShallowWrapper/slice.md)
6970
* [some(selector)](/docs/api/ShallowWrapper/some.md)
7071
* [someWhere(predicate)](/docs/api/ShallowWrapper/someWhere.md)
@@ -121,6 +122,7 @@
121122
* [setProps(nextProps[, callback])](/docs/api/ReactWrapper/setProps.md)
122123
* [setState(nextState[, callback])](/docs/api/ReactWrapper/setState.md)
123124
* [simulate(event[, data])](/docs/api/ReactWrapper/simulate.md)
125+
* [simulateError(error)](/docs/api/ReactWrapper/simulateError.md)
124126
* [slice([begin[, end]])](/docs/api/ReactWrapper/slice.md)
125127
* [some(selector)](/docs/api/ReactWrapper/some.md)
126128
* [someWhere(predicate)](/docs/api/ReactWrapper/someWhere.md)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# `.simulateError(error) => Self`
2+
3+
Simulate a component throwing an error as part of its rendering lifecycle.
4+
5+
This is particularly useful in combination with React 16 error boundaries (ie, the `componentDidCatch` lifecycle method).
6+
7+
8+
#### Arguments
9+
10+
1. `error` (`Any`): The error to throw.
11+
12+
13+
14+
#### Returns
15+
16+
`ReactWrapper`: Returns itself.
17+
18+
19+
20+
#### Example
21+
22+
```jsx
23+
function Something() {
24+
// this is just a placeholder
25+
return null;
26+
}
27+
28+
class ErrorBoundary extends React.Component {
29+
componentDidCatch(error, info) {
30+
const { spy } = this.props;
31+
spy(error, info);
32+
}
33+
34+
render() {
35+
const { children } = this.props;
36+
return (
37+
<React.Fragment>
38+
{children}
39+
</React.Fragment>
40+
);
41+
}
42+
}
43+
ErrorBoundary.propTypes = {
44+
children: PropTypes.node.isRequired,
45+
spy: PropTypes.func.isRequired,
46+
};
47+
48+
const spy = sinon.spy();
49+
const wrapper = mount(<ErrorBoundary spy={spy}><Something /></ErrorBoundary>);
50+
const error = new Error('hi!');
51+
wrapper.find(Something).simulateError(error);
52+
53+
expect(spy).to.have.property('callCount', 1);
54+
expect(spy.args).to.deep.equal([
55+
error,
56+
{
57+
componentStack: `
58+
in Something (created by ErrorBoundary)
59+
in ErrorBoundary (created by WrapperComponent)
60+
in WrapperComponent`,
61+
},
62+
]);
63+
```
64+
65+
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# `.simulateError(error) => Self`
2+
3+
Simulate a component throwing an error as part of its rendering lifecycle.
4+
5+
This is particularly useful in combination with React 16 error boundaries (ie, the `componentDidCatch` lifecycle method).
6+
7+
8+
#### Arguments
9+
10+
1. `error` (`Any`): The error to throw.
11+
12+
13+
14+
#### Returns
15+
16+
`ShallowWrapper`: Returns itself.
17+
18+
19+
20+
#### Example
21+
22+
```jsx
23+
function Something() {
24+
// this is just a placeholder
25+
return null;
26+
}
27+
28+
class ErrorBoundary extends React.Component {
29+
componentDidCatch(error, info) {
30+
const { spy } = this.props;
31+
spy(error, info);
32+
}
33+
34+
render() {
35+
const { children } = this.props;
36+
return (
37+
<React.Fragment>
38+
{children}
39+
</React.Fragment>
40+
);
41+
}
42+
}
43+
ErrorBoundary.propTypes = {
44+
children: PropTypes.node.isRequired,
45+
spy: PropTypes.func.isRequired,
46+
};
47+
48+
const spy = sinon.spy();
49+
const wrapper = shallow(<ErrorBoundary spy={spy}><Something /></ErrorBoundary>);
50+
const error = new Error('hi!');
51+
wrapper.find(Something).simulateError(error);
52+
53+
expect(spy).to.have.property('callCount', 1);
54+
expect(spy.args).to.deep.equal([
55+
error,
56+
{
57+
componentStack: `
58+
in Something (created by ErrorBoundary)
59+
in ErrorBoundary (created by WrapperComponent)
60+
in WrapperComponent`,
61+
},
62+
]);
63+
```
64+
65+

packages/enzyme-test-suite/test/ReactWrapper-spec.jsx

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2641,6 +2641,86 @@ describeWithDOM('mount', () => {
26412641
});
26422642
});
26432643

2644+
describe('.simulateError(error)', () => {
2645+
class Div extends React.Component {
2646+
render() {
2647+
return <div>{this.props.children}</div>;
2648+
}
2649+
}
2650+
2651+
class Spans extends React.Component {
2652+
render() {
2653+
return <div><span /><span /></div>;
2654+
}
2655+
}
2656+
2657+
class Nested extends React.Component {
2658+
render() {
2659+
return <Div><Spans /></Div>;
2660+
}
2661+
}
2662+
2663+
it('throws on host elements', () => {
2664+
const wrapper = mount(<Div />).find('div');
2665+
expect(wrapper.is('div')).to.equal(true);
2666+
expect(() => wrapper.simulateError()).to.throw();
2667+
});
2668+
2669+
it('throws on "not one" node', () => {
2670+
const wrapper = mount(<Spans />);
2671+
2672+
const spans = wrapper.find('span');
2673+
expect(spans).to.have.lengthOf(2);
2674+
expect(() => spans.simulateError()).to.throw();
2675+
2676+
const navs = wrapper.find('nav');
2677+
expect(navs).to.have.lengthOf(0);
2678+
expect(() => navs.simulateError()).to.throw();
2679+
});
2680+
2681+
it('throws when the renderer lacks `simulateError`', () => {
2682+
const wrapper = mount(<Nested />);
2683+
delete wrapper[sym('__renderer__')].simulateError;
2684+
expect(() => wrapper.simulateError()).to.throw();
2685+
try {
2686+
wrapper.simulateError();
2687+
} catch (e) {
2688+
expect(e).not.to.equal(undefined);
2689+
}
2690+
});
2691+
2692+
it('calls through to renderer’s `simulateError`', () => {
2693+
const wrapper = mount(<Nested />).find(Div);
2694+
const stub = sinon.stub().callsFake((_, __, e) => { throw e; });
2695+
wrapper[sym('__renderer__')].simulateError = stub;
2696+
const error = new Error('hi');
2697+
expect(() => wrapper.simulateError(error)).to.throw(error);
2698+
expect(stub).to.have.property('callCount', 1);
2699+
2700+
const [args] = stub.args;
2701+
expect(args).to.have.lengthOf(3);
2702+
const [hierarchy, rootNode, actualError] = args;
2703+
expect(actualError).to.equal(error);
2704+
expect(rootNode).to.eql(wrapper[sym('__root__')].getNodeInternal());
2705+
expect(hierarchy).to.have.lengthOf(2);
2706+
const [divNode, spanNode] = hierarchy;
2707+
expect(divNode).to.contain.keys({
2708+
type: Div,
2709+
nodeType: 'class',
2710+
rendered: {
2711+
type: Spans,
2712+
nodeType: 'class',
2713+
rendered: null,
2714+
},
2715+
});
2716+
expect(spanNode).to.contain.keys({
2717+
type: Spans,
2718+
nodeType: 'class',
2719+
rendered: null,
2720+
});
2721+
});
2722+
});
2723+
26442724
describe('.setState(newState[, callback])', () => {
26452725
it('throws on a non-function callback', () => {
26462726
class Foo extends React.Component {
@@ -4930,6 +5010,118 @@ describeWithDOM('mount', () => {
49305010
});
49315011
});
49325012

5013+
describeIf(is('>= 16'), 'componentDidCatch', () => {
5014+
describe('errors inside an error boundary', () => {
5015+
const errorToThrow = new EvalError('threw an error!');
5016+
// in React 16.0 - 16.2, and some older nodes, the actual error thrown isn't reported.
5017+
const reactError = new Error('An error was thrown inside one of your components, but React doesn\'t know what it was. This is likely due to browser flakiness. React does its best to preserve the "Pause on exceptions" behavior of the DevTools, which requires some DEV-mode only tricks. It\'s possible that these don\'t work in your browser. Try triggering the error in production mode, or switching to a modern browser. If you suspect that this is actually an issue with React, please file an issue.');
5018+
const properErrorMessage = error => error instanceof Error && (
5019+
error.message === errorToThrow.message
5020+
|| error.message === reactError.message
5021+
);
5022+
5023+
const hasFragments = is('>= 16.2');
5024+
const MaybeFragment = hasFragments ? Fragment : 'main';
5025+
5026+
function Thrower({ throws }) {
5027+
if (throws) {
5028+
throw errorToThrow;
5029+
}
5030+
return null;
5031+
}
5032+
5033+
class ErrorBoundary extends React.Component {
5034+
constructor(...args) {
5035+
super(...args);
5036+
this.state = { throws: false };
5037+
}
5038+
5039+
componentDidCatch(error, info) {
5040+
const { spy } = this.props;
5041+
spy(error, info);
5042+
this.setState({ throws: false });
5043+
}
5044+
5045+
render() {
5046+
const { throws } = this.state;
5047+
return (
5048+
<div>
5049+
<MaybeFragment>
5050+
<span>
5051+
<Thrower throws={throws} />
5052+
</span>
5053+
</MaybeFragment>
5054+
</div>
5055+
);
5056+
}
5057+
}
5058+
5059+
describe('Thrower', () => {
5060+
it('does not throw when `throws` is `false`', () => {
5061+
expect(() => mount(<Thrower throws={false} />)).not.to.throw();
5062+
});
5063+
5064+
it('throws when `throws` is `true`', () => {
5065+
expect(() => mount(<Thrower throws />)).to.throw();
5066+
try {
5067+
mount(<Thrower throws />);
5068+
expect(true).to.equal(false, 'this line should not be reached');
5069+
} catch (e) {
5070+
expect(e).to.satisfy(properErrorMessage);
5071+
}
5072+
});
5073+
});
5074+
5075+
it('catches a simulated error', () => {
5076+
const spy = sinon.spy();
5077+
const wrapper = mount(<ErrorBoundary spy={spy} />);
5078+
5079+
expect(spy).to.have.property('callCount', 0);
5080+
5081+
expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw();
5082+
5083+
expect(spy).to.have.property('callCount', 1);
5084+
5085+
expect(spy.args).to.be.an('array').and.have.lengthOf(1);
5086+
const [[actualError, info]] = spy.args;
5087+
expect(actualError).to.equal(errorToThrow);
5088+
expect(info).to.deep.equal({
5089+
componentStack: `
5090+
in Thrower (created by ErrorBoundary)
5091+
in span (created by ErrorBoundary)${hasFragments ? '' : `
5092+
in main (created by ErrorBoundary)`}
5093+
in div (created by ErrorBoundary)
5094+
in ErrorBoundary (created by WrapperComponent)
5095+
in WrapperComponent`,
5096+
});
5097+
});
5098+
5099+
it('catches errors during render', () => {
5100+
const spy = sinon.spy();
5101+
const wrapper = mount(<ErrorBoundary spy={spy} />);
5102+
5103+
expect(spy).to.have.property('callCount', 0);
5104+
5105+
wrapper.setState({ throws: true });
5106+
5107+
expect(spy).to.have.property('callCount', 1);
5108+
5109+
expect(spy.args).to.be.an('array').and.have.lengthOf(1);
5110+
const [[actualError, info]] = spy.args;
5111+
expect(actualError).to.satisfy(properErrorMessage);
5112+
expect(info).to.deep.equal({
5113+
componentStack: `
5114+
in Thrower (created by ErrorBoundary)
5115+
in span (created by ErrorBoundary)${hasFragments ? '' : `
5116+
in main (created by ErrorBoundary)`}
5117+
in div (created by ErrorBoundary)
5118+
in ErrorBoundary (created by WrapperComponent)
5119+
in WrapperComponent`,
5120+
});
5121+
});
5122+
});
5123+
});
5124+
49335125
context('mounting phase', () => {
49345126
it('calls componentWillMount and componentDidMount', () => {
49355127
const spy = sinon.spy();

0 commit comments

Comments
 (0)