Bug 1298414 - Properly handle resolve/reject callbacks on xray'd promises. r?efaust,f?bz
Xray'd promises store wrapped versions of the resolve/reject functions. Before this patch, resolving/rejecting them via the JSAPI ResolvePromise/RejectPromise functions wouldn't work, because they assume that the functions are unwrapped.
@bz: I tried to find a way to test this, but, because I know little about how to create tests that would be able to invoke the JSAPI functions *and* create Xray'd objects, I wasn't able to. Maybe you can give me a hint for how to go about this?
MozReview-Commit-ID: BFXIS0ouOTB
--- a/dom/promise/tests/test_promise_xrays.html
+++ b/dom/promise/tests/test_promise_xrays.html
@@ -224,29 +224,55 @@ function testResolve3() {
is(arg, 5, "Promise.resolve with chrome Promise should work");
},
function(e) {
ok(false, "Promise.resolve with chrome Promise should not fail");
}
).then(nextTest);
}
+function testResolve4() {
+ var p = new win.Promise((res, rej) => {});
+ Components.utils.getJSTestingFunctions().resolvePromise(p, 42);
+ p.then(
+ function(arg) {
+ is(arg, 42, "TestingFunctions resolvePromise with chrome Promise should work");
+ },
+ function(e) {
+ ok(false, "TestingFunctions resolvePromise with chrome Promise should not fail");
+ }
+ ).then(nextTest);
+}
+
function testReject1() {
var p = win.Promise.reject(5);
ok(p instanceof win.Promise, "Promise.reject should return a promise");
p.then(
function(arg) {
ok(false, "Promise should be rejected");
},
function(e) {
is(e, 5, "Should get correct Promise.reject value");
}
).then(nextTest);
}
+function testReject2() {
+ var p = new win.Promise((res, rej) => {});
+ Components.utils.getJSTestingFunctions().rejectPromise(p, 42);
+ p.then(
+ function(arg) {
+ ok(false, "TestingFunctions rejectPromise with chrome Promise should trigger catch handler");
+ },
+ function(e) {
+ is(e, 42, "TestingFunctions rejectPromise with chrome Promise should work");
+ }
+ ).then(nextTest);
+}
+
function testThen1() {
var p = win.Promise.resolve(5);
var q = p.then((x) => x*x);
ok(q instanceof win.Promise,
"Promise.then should return a promise from the right global");
q.then(
function(arg) {
is(arg, 25, "Promise.then should work");
@@ -300,17 +326,19 @@ var tests = [
testAll1,
testAll2,
testAll3,
testAll4,
testAll5,
testResolve1,
testResolve2,
testResolve3,
+ testResolve4,
testReject1,
+ testReject2,
testThen1,
testThen2,
testCatch1,
];
function nextTest() {
if (tests.length == 0) {
SimpleTest.finish();
--- a/js/src/builtin/Promise.cpp
+++ b/js/src/builtin/Promise.cpp
@@ -928,46 +928,64 @@ PromiseConstructor(JSContext* cx, unsign
// Step 11.
args.rval().setObject(*promise);
if (needsWrapping)
return cx->compartment()->wrap(cx, args.rval());
return true;
}
+static MOZ_MUST_USE bool
+GetRejectFunction(JSContext* cx, PromiseObject* promise, MutableHandleValue rejectFunVal)
+{
+ JSObject* resolveObj = &promise->getFixedSlot(PROMISE_RESOLVE_FUNCTION_SLOT).toObject();
+ // For xray'd promises, the resolve function will be a wrapper. We can
+ // safely unwrap it because all we want is to get the reject function.
+ bool needsWrapping = false;
+ if (IsWrapper(resolveObj)) {
+ resolveObj = UncheckedUnwrap(resolveObj);
+ needsWrapping = true;
+ }
+ JSFunction* resolve = &resolveObj->as<JSFunction>();
+ rejectFunVal.set(resolve->getExtendedSlot(ResolutionFunctionSlot_OtherFunction));
+ if (needsWrapping) {
+ if (!cx->compartment()->wrap(cx, rejectFunVal))
+ return false;
+ }
+ return true;
+}
bool
PromiseObject::resolve(JSContext* cx, HandleValue resolutionValue)
{
if (state() != JS::PromiseState::Pending)
return true;
RootedValue funVal(cx, this->getReservedSlot(PROMISE_RESOLVE_FUNCTION_SLOT));
- // TODO: ensure that this holds for xray'd promises. (It probably doesn't)
- MOZ_ASSERT(funVal.toObject().is<JSFunction>());
+ MOZ_ASSERT(IsCallable(funVal));
FixedInvokeArgs<1> args(cx);
args[0].set(resolutionValue);
RootedValue dummy(cx);
return Call(cx, funVal, UndefinedHandleValue, args, &dummy);
}
bool
PromiseObject::reject(JSContext* cx, HandleValue rejectionValue)
{
if (state() != JS::PromiseState::Pending)
return true;
- RootedValue resolveVal(cx, this->getReservedSlot(PROMISE_RESOLVE_FUNCTION_SLOT));
- RootedFunction resolve(cx, &resolveVal.toObject().as<JSFunction>());
- RootedValue funVal(cx, resolve->getExtendedSlot(ResolutionFunctionSlot_OtherFunction));
- MOZ_ASSERT(funVal.toObject().is<JSFunction>());
+ RootedValue funVal(cx);
+ if (!GetRejectFunction(cx, this, &funVal))
+ return false;
+ MOZ_ASSERT(IsCallable(funVal));
FixedInvokeArgs<1> args(cx);
args[0].set(rejectionValue);
RootedValue dummy(cx);
return Call(cx, funVal, UndefinedHandleValue, args, &dummy);
}
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -1426,16 +1426,70 @@ SettlePromiseNow(JSContext* cx, unsigned
promise->setFixedSlot(PROMISE_FLAGS_SLOT,
Int32Value(flags | PROMISE_FLAG_RESOLVED | PROMISE_FLAG_FULFILLED));
promise->setFixedSlot(PROMISE_REACTIONS_OR_RESULT_SLOT, UndefinedValue());
JS::dbg::onPromiseSettled(cx, promise);
return true;
}
+static bool
+ResolvePromise(JSContext* cx, unsigned argc, Value* vp)
+{
+ CallArgs args = CallArgsFromVp(argc, vp);
+ if (!args.requireAtLeast(cx, "resolvePromise", 2))
+ return false;
+ if (!args[0].isObject() || !UncheckedUnwrap(&args[0].toObject())->is<PromiseObject>()) {
+ JS_ReportError(cx, "first argument must be a maybe-wrapped Promise object");
+ return false;
+ }
+
+ RootedObject promise(cx, &args[0].toObject());
+ RootedValue resolution(cx, args[1]);
+ mozilla::Maybe<AutoCompartment> ac;
+ if (IsWrapper(promise)) {
+ promise = UncheckedUnwrap(promise);
+ ac.emplace(cx, promise);
+ if (!cx->compartment()->wrap(cx, &resolution))
+ return false;
+ }
+
+ bool result = JS::ResolvePromise(cx, promise, resolution);
+ if (result)
+ args.rval().setUndefined();
+ return result;
+}
+
+static bool
+RejectPromise(JSContext* cx, unsigned argc, Value* vp)
+{
+ CallArgs args = CallArgsFromVp(argc, vp);
+ if (!args.requireAtLeast(cx, "rejectPromise", 2))
+ return false;
+ if (!args[0].isObject() || !UncheckedUnwrap(&args[0].toObject())->is<PromiseObject>()) {
+ JS_ReportError(cx, "first argument must be a maybe-wrapped Promise object");
+ return false;
+ }
+
+ RootedObject promise(cx, &args[0].toObject());
+ RootedValue reason(cx, args[1]);
+ mozilla::Maybe<AutoCompartment> ac;
+ if (IsWrapper(promise)) {
+ promise = UncheckedUnwrap(promise);
+ ac.emplace(cx, promise);
+ if (!cx->compartment()->wrap(cx, &reason))
+ return false;
+ }
+
+ bool result = JS::RejectPromise(cx, promise, reason);
+ if (result)
+ args.rval().setUndefined();
+ return result;
+}
+
#else
static const js::Class FakePromiseClass = {
"Promise", JSCLASS_IS_ANONYMOUS
};
static bool
MakeFakePromise(JSContext* cx, unsigned argc, Value* vp)
@@ -3653,16 +3707,22 @@ static const JSFunctionSpecWithHelp Test
#ifdef SPIDERMONKEY_PROMISE
JS_FN_HELP("settlePromiseNow", SettlePromiseNow, 1, 0,
"settlePromiseNow(promise)",
" 'Settle' a 'promise' immediately. This just marks the promise as resolved\n"
" with a value of `undefined` and causes the firing of any onPromiseSettled\n"
" hooks set on Debugger instances that are observing the given promise's\n"
" global as a debuggee."),
+ JS_FN_HELP("resolvePromise", ResolvePromise, 2, 0,
+"resolvePromise(promise, resolution)",
+" Resolve a Promise by calling the JSAPI function JS::ResolvePromise."),
+ JS_FN_HELP("rejectPromise", RejectPromise, 2, 0,
+"rejectPromise(promise, reason)",
+" Reject a Promise by calling the JSAPI function JS::RejectPromise."),
#else
JS_FN_HELP("makeFakePromise", MakeFakePromise, 0, 0,
"makeFakePromise()",
" Create an object whose [[Class]] name is 'Promise' and call\n"
" JS::dbg::onNewPromise on it before returning it. It doesn't actually have\n"
" any of the other behavior associated with promises."),
JS_FN_HELP("settleFakePromise", SettleFakePromise, 1, 0,