Fix unhandled promise rejection handler after a `.then`

If we have a promise `p1` that is rejected, and invoke `then` on it,
we create a new promise `p2` (as it should be, per the spec) and mark
`p1` as handled. However, if we call `.catch` on `p2`, we are _not_
marking `p1` as handled correctly since its status is "pending" and
not "rejected". This patch fixes it and adds some tests.

Fixes #1461
This commit is contained in:
andrea.bergia 2024-04-30 12:50:53 +02:00 коммит произвёл Greg Brail
Родитель 05c033d812
Коммит 15abaadb78
2 изменённых файлов: 67 добавлений и 4 удалений

Просмотреть файл

@ -306,15 +306,19 @@ public class NativePromise extends ScriptableObject {
cx.enqueueMicrotask(() -> fulfillReaction.invoke(cx, scope, result));
} else {
assert (state == State.REJECTED);
if (!handled) {
cx.getUnhandledPromiseTracker().promiseHandled(this);
}
markHandled(cx);
cx.enqueueMicrotask(() -> rejectReaction.invoke(cx, scope, result));
}
handled = true;
return capability.promise;
}
private void markHandled(Context cx) {
if (!handled) {
cx.getUnhandledPromiseTracker().promiseHandled(this);
handled = true;
}
}
// Promise.prototype.catch
private static Object doCatch(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
Object arg = (args.length > 0 ? args[0] : Undefined.instance);
@ -446,6 +450,9 @@ public class NativePromise extends ScriptableObject {
for (Reaction r : reactions) {
cx.enqueueMicrotask(() -> r.invoke(cx, scope, reason));
}
if (!reactions.isEmpty()) {
markHandled(cx);
}
return Undefined.instance;
}

Просмотреть файл

@ -64,6 +64,46 @@ public class UnhandledPromiseTest {
assertTrue(Context.toBoolean(scope.get("caught", scope)));
}
@Test
public void rejectionHandledAfterThen() {
scope.put("caught", scope, Boolean.FALSE);
exec(
"new Promise((resolve, reject) => { reject(); })."
+ "then(() => {})."
+ "catch((e) => { caught = true; });\n");
assertTrue(cx.getUnhandledPromiseTracker().enumerate().isEmpty());
AtomicBoolean handled = new AtomicBoolean();
// We should actually never see anything in the tracker here
cx.getUnhandledPromiseTracker()
.process(
(Object o) -> {
assertFalse(handled.get());
handled.set(true);
});
assertFalse(handled.get());
assertTrue(Context.toBoolean(scope.get("caught", scope)));
}
@Test
public void thenRejectsButCatchHandles() {
scope.put("caught", scope, Boolean.FALSE);
exec(
"new Promise((resolve, reject) => { resolve(); })."
+ "then((e) => { return Promise.reject(); })."
+ "catch((e) => { caught = true; });\n");
assertTrue(cx.getUnhandledPromiseTracker().enumerate().isEmpty());
AtomicBoolean handled = new AtomicBoolean();
// We should actually never see anything in the tracker here
cx.getUnhandledPromiseTracker()
.process(
(Object o) -> {
assertFalse(handled.get());
handled.set(true);
});
assertFalse(handled.get());
assertTrue(Context.toBoolean(scope.get("caught", scope)));
}
@Test
public void rejectionObject() {
exec("new Promise((resolve, reject) => { reject('rejected'); });");
@ -115,6 +155,22 @@ public class UnhandledPromiseTest {
assertTrue(handled.get());
}
@Test
public void thenReturnsRejectedPromise() {
exec(
"new Promise((resolve, reject) => { resolve() }).then(() => { return Promise.reject('rejected'); });");
assertEquals(1, cx.getUnhandledPromiseTracker().enumerate().size());
AtomicBoolean handled = new AtomicBoolean();
cx.getUnhandledPromiseTracker()
.process(
(Object o) -> {
assertFalse(handled.get());
handled.set(true);
assertEquals("rejected", o);
});
assertTrue(handled.get());
}
private void exec(String script) {
cx.evaluateString(scope, "load('./testsrc/assert.js'); " + script, "test.js", 0, null);
}