Skip to content

Commit 8c1abc9

Browse files
authored
fix: prevent overlay for errors caught by React error boundaries (#5431)
1 parent 5a39c70 commit 8c1abc9

File tree

2 files changed

+220
-1
lines changed

2 files changed

+220
-1
lines changed

Diff for: 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

Diff for: test/client/clients/ReactErrorBoundary.test.js

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

0 commit comments

Comments
 (0)