Skip to content

Commit cefab44

Browse files
committed
Merge #5681 from 4.0 into 4.1
2 parents 6b7faf8 + 3fd5a2b commit cefab44

File tree

4 files changed

+379
-137
lines changed

4 files changed

+379
-137
lines changed

impl/src/main/resources/META-INF/resources/jakarta.faces/faces-uncompressed.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,12 +1465,32 @@ if ( !( (window.faces && window.faces.specversion && window.faces.specversion >=
14651465
sent = true;
14661466
}
14671467

1468-
if (!sent && faces.getProjectStage() === "Development") {
1469-
if (status === "serverError") {
1470-
alert("serverError: " + serverErrorName + SPACE + serverErrorMessage);
1468+
if (!sent) {
1469+
const errorMessage = status + ": "
1470+
+ (serverErrorName ? serverErrorName + " " : "")
1471+
+ data.description
1472+
+ (data.responseCode ? " (HTTP " + data.responseCode + ")" : "")
1473+
+ (data.source ? " [source: " + (data.source.id || data.source) + "]" : "");
1474+
1475+
// Example outputs:
1476+
// - httpError: There was an error communicating with the server, status: 404 (HTTP 404) [source: myButton]
1477+
// - serverError: java.lang.NullPointerException fieldName (HTTP 500) [source: myForm]
1478+
// - emptyResponse: An empty response was received from the server. Check server error logs. [source: myButton]
1479+
1480+
if (faces.getProjectStage() === "Development") {
1481+
alert(errorMessage);
14711482
} else {
1472-
alert(status + ": " + data.description);
1483+
console.error(errorMessage);
1484+
}
1485+
1486+
const warnMessage = "No faces.ajax.addOnError handler registered to handle this error. Register one to customize error handling.";
1487+
1488+
if (window.onerror) {
1489+
const onerrorMessage = errorMessage + " WARNING: " + warnMessage;
1490+
window.onerror(onerrorMessage, "jakarta.faces:faces.js", 0, 0, new Error(onerrorMessage));
14731491
}
1492+
1493+
console.warn(warnMessage);
14741494
}
14751495
};
14761496

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/**
2+
* Tests for the fallback error handling behavior when no error handler is registered (issue #5681).
3+
*
4+
* This is a separate test file because the fallback code path requires that no global
5+
* addOnError listeners are registered. Since errorListeners is module-level state with
6+
* no removal API, these tests need a fresh faces.js instance (provided by a separate
7+
* jest test file which gets its own jsdom environment).
8+
*/
9+
10+
import { loadFacesJs } from "../test-setup";
11+
import { installMockXHR, uninstallMockXHR, lastXHR, createAjaxForm } from "../test-helpers";
12+
13+
beforeAll(() => loadFacesJs());
14+
15+
const ajax = () => faces.ajax as Record<string, Function>;
16+
17+
// ---- Fallback error handling when no error handler registered (issue #5681) ----
18+
19+
describe("faces.ajax: fallback error handling without registered handler", () => {
20+
let form: HTMLFormElement;
21+
let button: HTMLButtonElement;
22+
let savedProjectStageCache: unknown;
23+
24+
beforeEach(() => {
25+
installMockXHR();
26+
({ form, button } = createAjaxForm());
27+
savedProjectStageCache = mojarra.projectStageCache;
28+
});
29+
30+
afterEach(() => {
31+
mojarra.projectStageCache = savedProjectStageCache;
32+
form?.remove();
33+
uninstallMockXHR();
34+
jest.restoreAllMocks();
35+
});
36+
37+
test("logs console.error with detailed message in Production mode", () => {
38+
mojarra.projectStageCache = "Production";
39+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
40+
jest.spyOn(console, "warn").mockImplementation(() => {});
41+
42+
ajax().request(button, null);
43+
lastXHR().respond(404, "Not Found");
44+
45+
expect(errorSpy).toHaveBeenCalledTimes(1);
46+
const msg = errorSpy.mock.calls[0][0] as string;
47+
expect(msg).toContain("httpError");
48+
expect(msg).toContain("(HTTP 404)");
49+
});
50+
51+
test("shows alert in Development mode", () => {
52+
mojarra.projectStageCache = "Development";
53+
const alertSpy = jest.spyOn(window, "alert").mockImplementation(() => {});
54+
jest.spyOn(console, "warn").mockImplementation(() => {});
55+
56+
ajax().request(button, null);
57+
lastXHR().respond(500, "Server Error");
58+
59+
expect(alertSpy).toHaveBeenCalledTimes(1);
60+
const msg = alertSpy.mock.calls[0][0] as string;
61+
expect(msg).toContain("httpError");
62+
expect(msg).toContain("(HTTP 500)");
63+
});
64+
65+
test("error message includes source element id", () => {
66+
mojarra.projectStageCache = "Production";
67+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
68+
jest.spyOn(console, "warn").mockImplementation(() => {});
69+
70+
button.id = "myButton";
71+
ajax().request(button, null);
72+
lastXHR().respond(404, "Not Found");
73+
74+
const msg = errorSpy.mock.calls[0][0] as string;
75+
expect(msg).toContain("[source: myButton]");
76+
});
77+
78+
test("error message includes responseCode", () => {
79+
mojarra.projectStageCache = "Production";
80+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
81+
jest.spyOn(console, "warn").mockImplementation(() => {});
82+
83+
ajax().request(button, null);
84+
lastXHR().respond(503, "Service Unavailable");
85+
86+
const msg = errorSpy.mock.calls[0][0] as string;
87+
expect(msg).toContain("(HTTP 503)");
88+
});
89+
90+
test("always logs console.warn about missing handler", () => {
91+
mojarra.projectStageCache = "Production";
92+
jest.spyOn(console, "error").mockImplementation(() => {});
93+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
94+
95+
ajax().request(button, null);
96+
lastXHR().respond(404, "Not Found");
97+
98+
expect(warnSpy).toHaveBeenCalledTimes(1);
99+
expect(warnSpy.mock.calls[0][0] as string).toContain("No faces.ajax.addOnError handler registered");
100+
});
101+
102+
test("console.warn logged in Development mode too", () => {
103+
mojarra.projectStageCache = "Development";
104+
jest.spyOn(window, "alert").mockImplementation(() => {});
105+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
106+
107+
ajax().request(button, null);
108+
lastXHR().respond(404, "Not Found");
109+
110+
expect(warnSpy).toHaveBeenCalledTimes(1);
111+
expect(warnSpy.mock.calls[0][0] as string).toContain("No faces.ajax.addOnError handler registered");
112+
});
113+
114+
test("no fallback when per-request onerror handler registered", () => {
115+
mojarra.projectStageCache = "Production";
116+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
117+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
118+
const alertSpy = jest.spyOn(window, "alert").mockImplementation(() => {});
119+
120+
ajax().request(button, null, { onerror: () => {} });
121+
lastXHR().respond(404, "Not Found");
122+
123+
expect(errorSpy).not.toHaveBeenCalled();
124+
expect(warnSpy).not.toHaveBeenCalled();
125+
expect(alertSpy).not.toHaveBeenCalled();
126+
});
127+
128+
test("emptyResponse fallback includes status and source", () => {
129+
mojarra.projectStageCache = "Production";
130+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
131+
jest.spyOn(console, "warn").mockImplementation(() => {});
132+
133+
button.id = "emptyBtn";
134+
ajax().request(button, null);
135+
lastXHR().respond(200, "");
136+
137+
const msg = errorSpy.mock.calls[0][0] as string;
138+
expect(msg).toContain("emptyResponse");
139+
expect(msg).toContain("[source: emptyBtn]");
140+
});
141+
142+
test("serverError fallback includes errorName in message", () => {
143+
mojarra.projectStageCache = "Production";
144+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
145+
jest.spyOn(console, "warn").mockImplementation(() => {});
146+
147+
ajax().request(button, null);
148+
const xml = '<?xml version="1.0" encoding="UTF-8"?><partial-response id=""><error><error-name>java.lang.NullPointerException</error-name><error-message><![CDATA[Something broke]]></error-message></error></partial-response>';
149+
lastXHR().respond(200, "", xml);
150+
151+
const msg = errorSpy.mock.calls[0][0] as string;
152+
expect(msg).toContain("serverError");
153+
expect(msg).toContain("java.lang.NullPointerException");
154+
});
155+
156+
test("calls window.onerror in Production mode when handler is registered", () => {
157+
mojarra.projectStageCache = "Production";
158+
jest.spyOn(console, "error").mockImplementation(() => {});
159+
jest.spyOn(console, "warn").mockImplementation(() => {});
160+
const onerrorSpy = jest.fn();
161+
window.onerror = onerrorSpy;
162+
163+
ajax().request(button, null);
164+
lastXHR().respond(404, "Not Found");
165+
166+
expect(onerrorSpy).toHaveBeenCalledTimes(1);
167+
expect(onerrorSpy.mock.calls[0][0]).toContain("httpError");
168+
expect(onerrorSpy.mock.calls[0][0]).toContain("WARNING: No faces.ajax.addOnError handler registered");
169+
expect(onerrorSpy.mock.calls[0][1]).toBe("jakarta.faces:faces.js");
170+
expect(onerrorSpy.mock.calls[0][4]).toBeInstanceOf(Error);
171+
172+
window.onerror = null;
173+
});
174+
175+
test("calls window.onerror in Development mode when handler is registered", () => {
176+
mojarra.projectStageCache = "Development";
177+
jest.spyOn(window, "alert").mockImplementation(() => {});
178+
jest.spyOn(console, "warn").mockImplementation(() => {});
179+
const onerrorSpy = jest.fn();
180+
window.onerror = onerrorSpy;
181+
182+
ajax().request(button, null);
183+
lastXHR().respond(500, "Server Error");
184+
185+
expect(onerrorSpy).toHaveBeenCalledTimes(1);
186+
expect(onerrorSpy.mock.calls[0][0]).toContain("httpError");
187+
expect(onerrorSpy.mock.calls[0][0]).toContain("WARNING: No faces.ajax.addOnError handler registered");
188+
expect(onerrorSpy.mock.calls[0][1]).toBe("jakarta.faces:faces.js");
189+
190+
window.onerror = null;
191+
});
192+
193+
test("does not call window.onerror when no handler is registered", () => {
194+
mojarra.projectStageCache = "Production";
195+
jest.spyOn(console, "error").mockImplementation(() => {});
196+
jest.spyOn(console, "warn").mockImplementation(() => {});
197+
window.onerror = null;
198+
199+
// Should not throw
200+
ajax().request(button, null);
201+
lastXHR().respond(404, "Not Found");
202+
});
203+
204+
// This test registers a global addOnError listener which cannot be removed,
205+
// so it must be the last test in this file.
206+
test("no fallback when global addOnError listener registered", () => {
207+
mojarra.projectStageCache = "Production";
208+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
209+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
210+
211+
ajax().addOnError(() => {});
212+
ajax().request(button, null);
213+
lastXHR().respond(404, "Not Found");
214+
215+
expect(errorSpy).not.toHaveBeenCalled();
216+
expect(warnSpy).not.toHaveBeenCalled();
217+
});
218+
});

0 commit comments

Comments
 (0)