Skip to content

Commit bae1180

Browse files
committed
fix: prevent overlay for errors caught by React error boundaries
1 parent 5a39c70 commit bae1180

File tree

2 files changed

+216
-1
lines changed

2 files changed

+216
-1
lines changed

client-src/overlay.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,10 @@ const createOverlay = (options) => {
664664
if (!error && !message) {
665665
return;
666666
}
667-
667+
// if error stack indicates a React error boundary caught the error, do not show overlay.
668+
if (error.stack && error.stack.includes("invokeGuardedCallbackDev")) {
669+
return;
670+
}
668671
handleError(error, message);
669672
});
670673

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
"use strict";
6+
7+
const { createOverlay } = require("../../../client-src/overlay");
8+
9+
describe("createOverlay", () => {
10+
const originalDocument = global.document;
11+
const originalWindow = global.window;
12+
13+
beforeEach(() => {
14+
global.document = {
15+
createElement: jest.fn(() => {return {
16+
style: {},
17+
appendChild: jest.fn(),
18+
addEventListener: jest.fn(),
19+
contentDocument: {
20+
createElement: jest.fn(() => {return { style: {}, appendChild: jest.fn() }}),
21+
body: { appendChild: jest.fn() },
22+
},
23+
}}),
24+
body: { appendChild: jest.fn(), removeChild: jest.fn() },
25+
};
26+
global.window = {
27+
// Keep addEventListener mocked for other potential uses
28+
addEventListener: jest.fn(),
29+
removeEventListener: jest.fn(),
30+
// Mock trustedTypes
31+
trustedTypes: null,
32+
// Mock dispatchEvent
33+
dispatchEvent: jest.fn(),
34+
};
35+
jest.useFakeTimers();
36+
});
37+
38+
afterEach(() => {
39+
global.document = originalDocument;
40+
global.window = originalWindow;
41+
jest.useRealTimers();
42+
jest.clearAllMocks();
43+
});
44+
45+
it("should not show overlay for errors caught by React error boundaries", () => {
46+
const options = { trustedTypesPolicyName: null, catchRuntimeError: true };
47+
const overlay = createOverlay(options);
48+
const showOverlayMock = jest.spyOn(overlay, "send");
49+
50+
const reactError = new Error(
51+
"Error inside React render\n" +
52+
" at Boom (webpack:///./src/index.jsx?:41:11)\n" +
53+
" at renderWithHooks (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:16305:18)\n" +
54+
" at mountIndeterminateComponent (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:20069:13)\n" +
55+
" at beginWork (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:21582:16)\n" +
56+
" at HTMLUnknownElement.callCallback (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:4164:14)\n" +
57+
" at Object.invokeGuardedCallbackDev (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:4213:16)\n" +
58+
" at invokeGuardedCallback (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:4277:31)\n" +
59+
" at beginWork$1 (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:27446:7)\n" +
60+
" at performUnitOfWork (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:26555:12)\n" +
61+
" at workLoopSync (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:26461:5)",
62+
);
63+
reactError._suppressLogging = true;
64+
65+
const errorEvent = new ErrorEvent("error", {
66+
error: reactError,
67+
message: reactError.message,
68+
});
69+
window.dispatchEvent(errorEvent);
70+
71+
expect(showOverlayMock).not.toHaveBeenCalled();
72+
showOverlayMock.mockRestore();
73+
});
74+
75+
it("should show overlay for normal uncaught errors", () => {
76+
const options = { trustedTypesPolicyName: null, catchRuntimeError: true };
77+
const overlay = createOverlay(options);
78+
const showOverlayMock = jest.spyOn(overlay, "send");
79+
80+
const regularError = new Error(
81+
"Error inside React render\n" +
82+
" at Boom (webpack:///./src/index.jsx?:41:11)\n" +
83+
" at renderWithHooks (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:16305:18)\n" +
84+
" at mountIndeterminateComponent (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:20069:13)\n" +
85+
" at beginWork (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:21582:16)\n" +
86+
" at HTMLUnknownElement.callCallback (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:4164:14)\n",
87+
);
88+
89+
const errorEvent = new ErrorEvent("error", {
90+
error: regularError,
91+
message: "Regular test error message",
92+
});
93+
window.dispatchEvent(errorEvent);
94+
95+
expect(showOverlayMock).toHaveBeenCalledWith({
96+
type: "RUNTIME_ERROR",
97+
messages: [
98+
{
99+
message: regularError.message,
100+
stack: expect.anything(),
101+
},
102+
],
103+
});
104+
showOverlayMock.mockRestore();
105+
});
106+
107+
it("should show overlay for normal uncaught errors when catchRuntimeError is a function that return true", () => {
108+
const options = {
109+
trustedTypesPolicyName: null,
110+
catchRuntimeError: () => true,
111+
};
112+
const overlay = createOverlay(options);
113+
const showOverlayMock = jest.spyOn(overlay, "send");
114+
115+
const regularError = new Error("Regular test error");
116+
const errorEvent = new ErrorEvent("error", {
117+
error: regularError,
118+
message: "Regular test error message",
119+
});
120+
window.dispatchEvent(errorEvent);
121+
122+
expect(showOverlayMock).toHaveBeenCalledWith({
123+
type: "RUNTIME_ERROR",
124+
messages: [
125+
{
126+
message: regularError.message,
127+
stack: expect.anything(),
128+
},
129+
],
130+
});
131+
showOverlayMock.mockRestore();
132+
});
133+
134+
it("should not show overlay for normal uncaught errors when catchRuntimeError is a function that return false", () => {
135+
const options = {
136+
trustedTypesPolicyName: null,
137+
catchRuntimeError: () => false,
138+
};
139+
const overlay = createOverlay(options);
140+
const showOverlayMock = jest.spyOn(overlay, "send");
141+
142+
const regularError = new Error("Regular test error");
143+
const errorEvent = new ErrorEvent("error", {
144+
error: regularError,
145+
message: "Regular test error message",
146+
});
147+
window.dispatchEvent(errorEvent);
148+
149+
expect(showOverlayMock).not.toHaveBeenCalled();
150+
showOverlayMock.mockRestore();
151+
});
152+
153+
it("should not show the overlay for errors with stack containing 'invokeGuardedCallbackDev'", () => {
154+
const options = { trustedTypesPolicyName: null, catchRuntimeError: true };
155+
const overlay = createOverlay(options);
156+
const showOverlayMock = jest.spyOn(overlay, "send");
157+
158+
const reactInternalError = new Error("React internal error");
159+
reactInternalError.stack = "invokeGuardedCallbackDev\n at somefile.js";
160+
const errorEvent = new ErrorEvent("error", {
161+
error: reactInternalError,
162+
message: "React internal error",
163+
});
164+
window.dispatchEvent(errorEvent);
165+
166+
expect(showOverlayMock).not.toHaveBeenCalled();
167+
showOverlayMock.mockRestore();
168+
});
169+
170+
it("should show overlay for unhandled rejections", () => {
171+
const options = { trustedTypesPolicyName: null, catchRuntimeError: true };
172+
const overlay = createOverlay(options);
173+
const showOverlayMock = jest.spyOn(overlay, "send");
174+
175+
const rejectionReason = new Error("Promise rejection reason");
176+
const rejectionEvent = new Event("unhandledrejection");
177+
rejectionEvent.reason = rejectionReason;
178+
179+
window.dispatchEvent(rejectionEvent);
180+
181+
expect(showOverlayMock).toHaveBeenCalledWith({
182+
type: "RUNTIME_ERROR",
183+
messages: [
184+
{
185+
message: rejectionReason.message,
186+
stack: expect.anything(),
187+
},
188+
],
189+
});
190+
showOverlayMock.mockRestore();
191+
});
192+
193+
it("should show overlay for unhandled rejections with string reason", () => {
194+
const options = { trustedTypesPolicyName: null, catchRuntimeError: true };
195+
const overlay = createOverlay(options);
196+
const showOverlayMock = jest.spyOn(overlay, "send");
197+
const rejectionEvent = new Event("unhandledrejection");
198+
rejectionEvent.reason = "some reason";
199+
window.dispatchEvent(rejectionEvent);
200+
201+
expect(showOverlayMock).toHaveBeenCalledWith({
202+
type: "RUNTIME_ERROR",
203+
messages: [
204+
{
205+
message: "some reason",
206+
stack: expect.anything(),
207+
},
208+
],
209+
});
210+
showOverlayMock.mockRestore();
211+
});
212+
});

0 commit comments

Comments
 (0)