Skip to content

Commit b914dd4

Browse files
author
Rishabh Rao
committed
Add tests for ErrorBoundary and ErrorBoundaryFallbackComponent
1 parent 0bf67c6 commit b914dd4

File tree

4 files changed

+503
-1
lines changed

4 files changed

+503
-1
lines changed

src/ErrorBoundary.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class ErrorBoundary extends Component<Props, State> {
3535

3636
if (typeof onError === 'function') {
3737
try {
38+
/* istanbul ignore next: Ignoring ternary; can’t reproduce missing info in test environment. */
3839
onError.call(this, error, info ? info.componentStack : '');
3940
} catch (ignoredError) {}
4041
}
@@ -49,7 +50,10 @@ class ErrorBoundary extends Component<Props, State> {
4950
if (error !== null) {
5051
return (
5152
<FallbackComponent
52-
componentStack={info ? info.componentStack : ''}
53+
componentStack={
54+
// istanbul ignore next: Ignoring ternary; can’t reproduce missing info in test environment.
55+
info ? info.componentStack : ''
56+
}
5357
error={error}
5458
/>
5559
);

src/ErrorBoundary.test.js

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import {mount} from 'enzyme';
2+
import React from 'react';
3+
4+
import ErrorBoundary, {withErrorBoundary} from './ErrorBoundary';
5+
import ErrorBoundaryFallbackComponent from './ErrorBoundaryFallbackComponent';
6+
7+
describe('ErrorBoundary', () => {
8+
let consoleErrorSpy;
9+
let mockError;
10+
11+
let Spell;
12+
let Wand;
13+
14+
beforeAll(() => {
15+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {
16+
// React error boundaries print the error to `console.error` stream even when it’s caught by our
17+
// `ErrorBoundary` component. We suppress `console.error` to keep our test console output clean.
18+
// @see #11098 Allow suppressing error boundary logs from intentionally thrown/caught errors
19+
// https://github.com/facebook/react/issues/11098
20+
});
21+
22+
mockError = new Error(
23+
'You cast an unforgivable curse! You’ve earned a one-way ticket to Azkaban.',
24+
);
25+
Spell = ({incantation}) => {
26+
switch (incantation) {
27+
case 'Avada Kedavra':
28+
case 'Crucio':
29+
case 'Imperio':
30+
throw mockError;
31+
32+
default:
33+
return <p>{`You cast the ${incantation} spell!`}</p>;
34+
}
35+
};
36+
37+
Wand = ({name, incantation}) => (
38+
<div>
39+
<p>{`Casting spell with the ${name} wand`}</p>
40+
<Spell incantation={incantation} />
41+
</div>
42+
);
43+
});
44+
45+
afterAll(() => {
46+
consoleErrorSpy.mockRestore();
47+
});
48+
49+
it('Renders the child component if there is no error', () => {
50+
const wrapper = mount(
51+
<ErrorBoundary>
52+
<Wand name="Harry’s" incantation="Expelliarmus" />
53+
</ErrorBoundary>,
54+
);
55+
56+
const WandWithErrorBoundary = withErrorBoundary(Wand);
57+
const wrapperWithErrorBoundary = mount(
58+
<WandWithErrorBoundary name="Harry’s" incantation="Expelliarmus" />,
59+
);
60+
61+
expect(wrapper.state().error).toBe(null);
62+
expect(wrapper.state().info).toBe(null);
63+
expect(
64+
wrapper.containsMatchingElement(<ErrorBoundaryFallbackComponent />),
65+
).toBe(false);
66+
expect(
67+
wrapper.contains(<Wand name="Harry’s" incantation="Expelliarmus" />),
68+
).toBe(true);
69+
70+
// Note: We use `….instance().state …` instead of `….state() …` here because…
71+
// > ReactWrapper::state() can only be called on the root
72+
expect(
73+
wrapperWithErrorBoundary.find(ErrorBoundary).instance().state.error,
74+
).toBe(null);
75+
expect(
76+
wrapperWithErrorBoundary.find(ErrorBoundary).instance().state.info,
77+
).toBe(null);
78+
expect(
79+
wrapperWithErrorBoundary.containsMatchingElement(
80+
<ErrorBoundaryFallbackComponent />,
81+
),
82+
).toBe(false);
83+
expect(
84+
wrapperWithErrorBoundary.contains(
85+
<Wand name="Harry’s" incantation="Expelliarmus" />,
86+
),
87+
).toBe(true);
88+
});
89+
90+
it('Sets its state to an error state and renders the default fallback component', () => {
91+
const wrapper = mount(
92+
<ErrorBoundary>
93+
<Wand name="Voldemort’s" incantation="Avada Kedavra" />
94+
</ErrorBoundary>,
95+
);
96+
97+
const WandWithErrorBoundary = withErrorBoundary(Wand);
98+
const wrapperWithErrorBoundary = mount(
99+
<WandWithErrorBoundary name="Voldemort’s" incantation="Avada Kedavra" />,
100+
);
101+
102+
expect(wrapper.state().error).toEqual(expect.objectContaining(mockError));
103+
expect(wrapper.state().info).toEqual(
104+
expect.objectContaining({
105+
componentStack: expect.any(String),
106+
}),
107+
);
108+
expect(
109+
wrapper.containsMatchingElement(<ErrorBoundaryFallbackComponent />),
110+
).toBe(true);
111+
112+
expect(
113+
wrapperWithErrorBoundary.find(ErrorBoundary).instance().state.error,
114+
).toEqual(expect.objectContaining(mockError));
115+
expect(
116+
wrapperWithErrorBoundary.find(ErrorBoundary).instance().state.info,
117+
).toEqual(
118+
expect.objectContaining({
119+
componentStack: expect.any(String),
120+
}),
121+
);
122+
expect(
123+
wrapperWithErrorBoundary.containsMatchingElement(
124+
<ErrorBoundaryFallbackComponent />,
125+
),
126+
).toBe(true);
127+
});
128+
129+
it('Sets its state to an error state and renders a custom fallback component', () => {
130+
const MockFallbackComponent = ({error, componentStack}) => (
131+
<div>
132+
<p>
133+
<strong>Wand unable to perform magic!</strong>
134+
Please contact Ollivanders in Diagon Alley for repairs.
135+
</p>
136+
<p>
137+
<strong>Error:</strong> {error.toString()}
138+
</p>
139+
<p>
140+
<strong>Stacktrace:</strong>
141+
<pre>{componentStack}</pre>
142+
</p>
143+
</div>
144+
);
145+
146+
const wrapper = mount(
147+
<ErrorBoundary FallbackComponent={MockFallbackComponent}>
148+
<Wand name="Voldemort’s" incantation="Crucio" />
149+
</ErrorBoundary>,
150+
);
151+
152+
const WandWithErrorBoundary = withErrorBoundary(
153+
Wand,
154+
MockFallbackComponent,
155+
);
156+
const wrapperWithErrorBoundary = mount(
157+
<WandWithErrorBoundary name="Voldemort’s" incantation="Crucio" />,
158+
);
159+
160+
expect(wrapper.state().error).toEqual(expect.objectContaining(mockError));
161+
expect(wrapper.state().info).toEqual(
162+
expect.objectContaining({
163+
componentStack: expect.any(String),
164+
}),
165+
);
166+
expect(
167+
wrapper.containsMatchingElement(<ErrorBoundaryFallbackComponent />),
168+
).toBe(false);
169+
expect(
170+
wrapper.containsMatchingElement(
171+
<MockFallbackComponent
172+
error={mockError} /* componentStack="ignored" */
173+
/>,
174+
),
175+
).toBe(true);
176+
177+
expect(
178+
wrapperWithErrorBoundary.find(ErrorBoundary).instance().state.error,
179+
).toEqual(expect.objectContaining(mockError));
180+
expect(
181+
wrapperWithErrorBoundary.find(ErrorBoundary).instance().state.info,
182+
).toEqual(
183+
expect.objectContaining({
184+
componentStack: expect.any(String),
185+
}),
186+
);
187+
expect(
188+
wrapperWithErrorBoundary.containsMatchingElement(
189+
<ErrorBoundaryFallbackComponent />,
190+
),
191+
).toBe(false);
192+
expect(
193+
wrapperWithErrorBoundary.containsMatchingElement(
194+
<MockFallbackComponent
195+
error={mockError} /* componentStack="ignored" */
196+
/>,
197+
),
198+
).toBe(true);
199+
});
200+
201+
it('Sets its state to an error state and invokes the onError callback prop', () => {
202+
const mockOnError = jest.fn().mockImplementation((
203+
error, // eslint-disable-line no-unused-vars
204+
info, // eslint-disable-line no-unused-vars
205+
) => {});
206+
207+
const mockOnErrorWithErrorBoundary = jest.fn().mockImplementation((
208+
error, // eslint-disable-line no-unused-vars
209+
info, // eslint-disable-line no-unused-vars
210+
) => {});
211+
212+
const wrapper = mount(
213+
<ErrorBoundary onError={mockOnError}>
214+
<Wand name="Voldemort’s" incantation="Imperio" />
215+
</ErrorBoundary>,
216+
);
217+
const WandWithErrorBoundary = withErrorBoundary(
218+
Wand,
219+
ErrorBoundaryFallbackComponent,
220+
mockOnErrorWithErrorBoundary,
221+
);
222+
const wrapperWithErrorBoundary = mount(
223+
<WandWithErrorBoundary name="Voldemort’s" incantation="Imperio" />,
224+
);
225+
226+
expect(wrapper.state().error).toEqual(expect.objectContaining(mockError));
227+
expect(wrapper.state().info).toEqual(
228+
expect.objectContaining({
229+
componentStack: expect.any(String),
230+
}),
231+
);
232+
expect(mockOnError).toHaveBeenCalledWith(mockError, expect.any(String));
233+
expect(
234+
wrapper.containsMatchingElement(
235+
<ErrorBoundaryFallbackComponent
236+
error={mockError} /* componentStack="ignored" */
237+
/>,
238+
),
239+
).toBe(true);
240+
241+
expect(
242+
wrapperWithErrorBoundary.find(ErrorBoundary).instance().state.error,
243+
).toEqual(expect.objectContaining(mockError));
244+
expect(
245+
wrapperWithErrorBoundary.find(ErrorBoundary).instance().state.info,
246+
).toEqual(
247+
expect.objectContaining({
248+
componentStack: expect.any(String),
249+
}),
250+
);
251+
expect(mockOnErrorWithErrorBoundary).toHaveBeenCalledWith(
252+
mockError,
253+
expect.any(String),
254+
);
255+
expect(
256+
wrapperWithErrorBoundary.containsMatchingElement(
257+
<ErrorBoundaryFallbackComponent
258+
error={mockError} /* componentStack="ignored" */
259+
/>,
260+
),
261+
).toBe(true);
262+
});
263+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react';
2+
import {shallow} from 'enzyme';
3+
4+
import ErrorBoundaryFallbackComponent from './ErrorBoundaryFallbackComponent';
5+
6+
describe('ErrorBoundaryFallbackComponent', () => {
7+
let mockError;
8+
9+
beforeAll(() => {
10+
mockError = new Error(
11+
'You cast an unforgivable curse! You’ve earned a one-way ticket to Azkaban.',
12+
);
13+
});
14+
15+
it('Passes a snapshot test', () => {
16+
const wrapper = shallow(
17+
<ErrorBoundaryFallbackComponent error={mockError} componentStack="" />,
18+
);
19+
20+
expect(wrapper).toMatchSnapshot();
21+
});
22+
});

0 commit comments

Comments
 (0)