Skip to content

Commit 34196a7

Browse files
authored
feat: web-compliant error handling (#1985)
1 parent e6d0d3d commit 34196a7

30 files changed

Lines changed: 3453 additions & 44 deletions

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Contains the source code for the NativeScript's Android Runtime. [NativeScript](
1111
- [Build Prerequisites](#build-prerequisites)
1212
- [How to build](#how-to-build)
1313
- [How to run tests](#how-to-run-tests)
14+
- [Documentation](#documentation)
1415
- [Misc](#misc)
1516
- [Get Help](#get-help)
1617

@@ -124,6 +125,10 @@ npx ns debug android --start
124125
We love PRs! Check out the [contributing guidelines](CONTRIBUTING.md). If you want to contribute, but you are not sure where to start - look for [issues labeled `help wanted`](https://github.com/NativeScript/android-runtime/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22).
125126

126127

128+
## Documentation
129+
130+
Runtime feature documentation lives in the [docs](docs/README.md) folder — see [Error handling](docs/error-handling.md) for the global error events, Java exception round-tripping and `interop.escapeException`.
131+
127132
## Misc
128133

129134
* [Implementing additional Chrome DevTools protocol Domains](docs/extending-inspector.md)

docs/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Runtime documentation
2+
3+
- [Error handling](error-handling.md) — global `error`/`unhandledrejection` events, `reportError`, catching Java exceptions in JS (`error.nativeException`), forwarding JS throws to Java callers (`interop.escapeException`), JS stacks on Java exceptions (`com.tns.JavaScriptStackTrace`), configuration flags, and crash-reporter integration.
4+
- [Implementing additional Chrome DevTools protocol Domains](extending-inspector.md)

docs/error-handling.md

Lines changed: 245 additions & 0 deletions
Large diffs are not rendered by default.

test-app/app/src/main/assets/app/mainpage.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ require('./tests/testURLImpl.js');
7474
require('./tests/testURLSearchParamsImpl.js');
7575
require('./tests/testPerformanceNow');
7676
require('./tests/testQueueMicrotask');
77+
require('./tests/testErrorEvents');
78+
require('./tests/testUnhandledRejections');
79+
require('./tests/testEscapeException');
80+
require('./tests/testUncaughtErrorPolicy');
7781
require("./tests/testConcurrentAccess");
7882

7983
require("./tests/testESModules.mjs");
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
describe("WHATWG error events", function () {
2+
// Many tests exercise the global error path, which ends in the
3+
// __onUncaughtError / __onDiscardedError hooks when a listener does not
4+
// preventDefault(). Install spies for every test and restore the previous
5+
// hooks in afterEach. Also track listeners added on the global target so
6+
// they never leak into other suites (the internal EventTarget backing the
7+
// global is process-wide).
8+
var previousUncaughtHook;
9+
var previousDiscardedHook;
10+
var uncaught;
11+
var discarded;
12+
var addedGlobalListeners;
13+
14+
beforeEach(function () {
15+
previousUncaughtHook = global.__onUncaughtError;
16+
previousDiscardedHook = global.__onDiscardedError;
17+
uncaught = [];
18+
discarded = [];
19+
global.__onUncaughtError = function (error) {
20+
uncaught.push(error);
21+
};
22+
global.__onDiscardedError = function (error) {
23+
discarded.push(error);
24+
};
25+
addedGlobalListeners = [];
26+
});
27+
28+
afterEach(function () {
29+
global.__onUncaughtError = previousUncaughtHook;
30+
global.__onDiscardedError = previousDiscardedHook;
31+
for (var i = 0; i < addedGlobalListeners.length; i++) {
32+
var l = addedGlobalListeners[i];
33+
global.removeEventListener(l.type, l.handler);
34+
}
35+
addedGlobalListeners = [];
36+
});
37+
38+
function onGlobal(type, handler) {
39+
global.addEventListener(type, handler);
40+
addedGlobalListeners.push({ type: type, handler: handler });
41+
}
42+
43+
// Wait a couple of quiet looper turns before asserting a NON-event.
44+
function afterQuietTurns(cb) {
45+
setTimeout(function () {
46+
setTimeout(cb, 25);
47+
}, 25);
48+
}
49+
50+
it("reportError fires an 'error' listener with an ErrorEvent carrying error and message", function (done) {
51+
var err = new Error("x");
52+
var received = null;
53+
onGlobal("error", function (e) {
54+
received = e;
55+
e.preventDefault();
56+
});
57+
58+
global.reportError(err);
59+
60+
expect(received).not.toBeNull();
61+
expect(received instanceof ErrorEvent).toBe(true);
62+
expect(received.type).toBe("error");
63+
expect(received.error).toBe(err);
64+
expect(received.message).toBe("x");
65+
// preventDefault() in the listener must suppress the __onUncaughtError hook.
66+
afterQuietTurns(function () {
67+
expect(uncaught.length).toBe(0);
68+
done();
69+
});
70+
});
71+
72+
it("reportError without preventDefault still invokes __onUncaughtError (back-compat)", function () {
73+
var err = new Error("back-compat");
74+
var received = null;
75+
onGlobal("error", function (e) {
76+
received = e;
77+
});
78+
79+
global.reportError(err);
80+
81+
expect(received).not.toBeNull();
82+
expect(received.error).toBe(err);
83+
expect(uncaught.length).toBe(1);
84+
expect(uncaught[0]).toBe(err);
85+
});
86+
87+
it("reportError throws TypeError when called with no arguments", function () {
88+
expect(function () {
89+
global.reportError();
90+
}).toThrowError(TypeError);
91+
});
92+
93+
it("a discarded Java exception dispatches an 'error' event before __onDiscardedError", function () {
94+
var received = null;
95+
onGlobal("error", function (e) {
96+
received = e;
97+
});
98+
99+
var test = new com.tns.tests.DiscardedExceptionTest();
100+
test.reportSupressedException();
101+
102+
expect(received).not.toBeNull();
103+
expect(received instanceof ErrorEvent).toBe(true);
104+
expect(received.error).not.toBeNull();
105+
expect(received.error.message).toBe("Exception to suppress");
106+
// Unprevented, so the existing hook still fires (back-compat).
107+
expect(discarded.length).toBe(1);
108+
expect(discarded[0]).toBe(received.error);
109+
});
110+
111+
it("preventDefault() on the 'error' event suppresses __onDiscardedError", function () {
112+
var received = null;
113+
onGlobal("error", function (e) {
114+
received = e;
115+
e.preventDefault();
116+
});
117+
118+
var test = new com.tns.tests.DiscardedExceptionTest();
119+
test.reportSupressedException();
120+
121+
expect(received).not.toBeNull();
122+
expect(discarded.length).toBe(0);
123+
});
124+
125+
describe("constructors and EventTarget semantics", function () {
126+
it("Event is spec-sane and cancelable via preventDefault", function () {
127+
var e = new Event("x", { cancelable: true });
128+
expect(e.type).toBe("x");
129+
expect(e.cancelable).toBe(true);
130+
expect(e.bubbles).toBe(false);
131+
expect(e.defaultPrevented).toBe(false);
132+
e.preventDefault();
133+
expect(e.defaultPrevented).toBe(true);
134+
});
135+
136+
it("a non-cancelable Event ignores preventDefault", function () {
137+
var e = new Event("x");
138+
e.preventDefault();
139+
expect(e.defaultPrevented).toBe(false);
140+
});
141+
142+
it("ErrorEvent exposes message/error/filename/lineno/colno", function () {
143+
var err = new Error("boom");
144+
var e = new ErrorEvent("error", { message: "m", error: err });
145+
expect(e instanceof Event).toBe(true);
146+
expect(e.message).toBe("m");
147+
expect(e.error).toBe(err);
148+
expect(e.filename).toBe("");
149+
expect(e.lineno).toBe(0);
150+
expect(e.colno).toBe(0);
151+
});
152+
153+
it("PromiseRejectionEvent exposes promise/reason", function () {
154+
var p = Promise.reject(1);
155+
p.catch(function () {});
156+
var r = { some: "reason" };
157+
var e = new PromiseRejectionEvent("unhandledrejection", { promise: p, reason: r });
158+
expect(e instanceof Event).toBe(true);
159+
expect(e.promise).toBe(p);
160+
expect(e.reason).toBe(r);
161+
});
162+
163+
it("dispatchEvent returns !defaultPrevented", function () {
164+
var target = new EventTarget();
165+
target.addEventListener("t", function (e) { e.preventDefault(); });
166+
expect(target.dispatchEvent(new Event("t", { cancelable: true }))).toBe(false);
167+
168+
var target2 = new EventTarget();
169+
target2.addEventListener("t", function () {});
170+
expect(target2.dispatchEvent(new Event("t", { cancelable: true }))).toBe(true);
171+
});
172+
173+
it("once:true listener fires exactly once", function () {
174+
var target = new EventTarget();
175+
var count = 0;
176+
target.addEventListener("t", function () { count++; }, { once: true });
177+
target.dispatchEvent(new Event("t"));
178+
target.dispatchEvent(new Event("t"));
179+
expect(count).toBe(1);
180+
});
181+
182+
it("removeEventListener stops future dispatches", function () {
183+
var target = new EventTarget();
184+
var count = 0;
185+
var handler = function () { count++; };
186+
target.addEventListener("t", handler);
187+
target.dispatchEvent(new Event("t"));
188+
target.removeEventListener("t", handler);
189+
target.dispatchEvent(new Event("t"));
190+
expect(count).toBe(1);
191+
});
192+
193+
it("listeners run in registration order", function () {
194+
var target = new EventTarget();
195+
var order = [];
196+
target.addEventListener("t", function () { order.push(1); });
197+
target.addEventListener("t", function () { order.push(2); });
198+
target.addEventListener("t", function () { order.push(3); });
199+
target.dispatchEvent(new Event("t"));
200+
expect(order).toEqual([1, 2, 3]);
201+
});
202+
203+
it("stopImmediatePropagation stops remaining listeners", function () {
204+
var target = new EventTarget();
205+
var order = [];
206+
target.addEventListener("t", function (e) { order.push(1); e.stopImmediatePropagation(); });
207+
target.addEventListener("t", function () { order.push(2); });
208+
target.dispatchEvent(new Event("t"));
209+
expect(order).toEqual([1]);
210+
});
211+
212+
it("a throwing listener does not stop later listeners", function () {
213+
var target = new EventTarget();
214+
var order = [];
215+
target.addEventListener("t", function () { order.push(1); throw new Error("listener boom"); });
216+
target.addEventListener("t", function () { order.push(2); });
217+
target.dispatchEvent(new Event("t"));
218+
expect(order).toEqual([1, 2]);
219+
});
220+
});
221+
222+
it("reportError still fires listeners after globalThis.dispatchEvent is overwritten", function (done) {
223+
var err = new Error("resilient");
224+
var received = null;
225+
onGlobal("error", function (e) {
226+
received = e;
227+
e.preventDefault();
228+
});
229+
230+
var originalDispatch = globalThis.dispatchEvent;
231+
globalThis.dispatchEvent = function () { return true; };
232+
try {
233+
global.reportError(err);
234+
} finally {
235+
globalThis.dispatchEvent = originalDispatch;
236+
}
237+
238+
expect(received).not.toBeNull();
239+
expect(received.error).toBe(err);
240+
afterQuietTurns(function () {
241+
expect(uncaught.length).toBe(0);
242+
done();
243+
});
244+
});
245+
});

0 commit comments

Comments
 (0)