diff --git a/dom/base/DOMCursor.h b/dom/base/DOMCursor.h index 08bd987354b..6c469439924 100644 --- a/dom/base/DOMCursor.h +++ b/dom/base/DOMCursor.h @@ -43,6 +43,11 @@ protected: private: DOMCursor() MOZ_DELETE; + // Calling Then() on DOMCursor is a mistake, since the DOMCursor object + // should not have a .then() method from JS' point of view. + already_AddRefed + Then(JSContext* aCx, AnyCallback* aResolveCallback, + AnyCallback* aRejectCallback, ErrorResult& aRv) MOZ_DELETE; nsCOMPtr mCallback; bool mFinished; diff --git a/dom/base/DOMRequest.cpp b/dom/base/DOMRequest.cpp index a6c21cb4770..3afc8334bee 100644 --- a/dom/base/DOMRequest.cpp +++ b/dom/base/DOMRequest.cpp @@ -10,11 +10,16 @@ #include "nsThreadUtils.h" #include "DOMCursor.h" #include "nsIDOMEvent.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ScriptSettings.h" +using mozilla::dom::AnyCallback; using mozilla::dom::DOMError; using mozilla::dom::DOMRequest; using mozilla::dom::DOMRequestService; using mozilla::dom::DOMCursor; +using mozilla::dom::Promise; using mozilla::AutoSafeJSContext; DOMRequest::DOMRequest(nsPIDOMWindow* aWindow) @@ -25,16 +30,24 @@ DOMRequest::DOMRequest(nsPIDOMWindow* aWindow) { } +DOMRequest::~DOMRequest() +{ + mResult.setUndefined(); + mozilla::DropJSObjects(this); +} + NS_IMPL_CYCLE_COLLECTION_CLASS(DOMRequest) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DOMRequest, DOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mError) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DOMRequest, DOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_UNLINK(mError) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise) tmp->mResult = JSVAL_VOID; NS_IMPL_CYCLE_COLLECTION_UNLINK_END @@ -107,6 +120,10 @@ DOMRequest::FireSuccess(JS::Handle aResult) mResult = aResult; FireEvent(NS_LITERAL_STRING("success"), false, false); + + if (mPromise) { + mPromise->MaybeResolve(mResult); + } } void @@ -120,6 +137,10 @@ DOMRequest::FireError(const nsAString& aError) mError = new DOMError(GetOwner(), aError); FireEvent(NS_LITERAL_STRING("error"), true, true); + + if (mPromise) { + mPromise->MaybeRejectBrokenly(mError); + } } void @@ -133,6 +154,10 @@ DOMRequest::FireError(nsresult aError) mError = new DOMError(GetOwner(), aError); FireEvent(NS_LITERAL_STRING("error"), true, true); + + if (mPromise) { + mPromise->MaybeRejectBrokenly(mError); + } } void @@ -147,6 +172,10 @@ DOMRequest::FireDetailedError(DOMError* aError) mError = aError; FireEvent(NS_LITERAL_STRING("error"), true, true); + + if (mPromise) { + mPromise->MaybeRejectBrokenly(mError); + } } void @@ -175,6 +204,31 @@ DOMRequest::RootResultVal() mozilla::HoldJSObjects(this); } +already_AddRefed +DOMRequest::Then(JSContext* aCx, AnyCallback* aResolveCallback, + AnyCallback* aRejectCallback, mozilla::ErrorResult& aRv) +{ + if (!mPromise) { + mPromise = Promise::Create(DOMEventTargetHelper::GetParentObject(), aRv); + if (aRv.Failed()) { + return nullptr; + } + if (mDone) { + // Since we create mPromise lazily, it's possible that the DOMRequest object + // has already fired its success/error event. In that case we should + // manually resolve/reject mPromise here. mPromise will take care of + // calling the callbacks on |promise| as needed. + if (mError) { + mPromise->MaybeRejectBrokenly(mError); + } else { + mPromise->MaybeResolve(mResult); + } + } + } + + return mPromise->Then(aCx, aResolveCallback, aRejectCallback, aRv); +} + NS_IMPL_ISUPPORTS(DOMRequestService, nsIDOMRequestService) NS_IMETHODIMP @@ -253,7 +307,7 @@ public: const JS::Value& aResult) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); - AutoSafeJSContext cx; + mozilla::ThreadsafeAutoSafeJSContext cx; nsRefPtr asyncTask = new FireSuccessAsyncTask(cx, aRequest, aResult); if (NS_FAILED(NS_DispatchToMainThread(asyncTask))) { NS_WARNING("Failed to dispatch to main thread!"); diff --git a/dom/base/DOMRequest.h b/dom/base/DOMRequest.h index cd19dc02adc..56236c15677 100644 --- a/dom/base/DOMRequest.h +++ b/dom/base/DOMRequest.h @@ -16,14 +16,21 @@ #include "nsCOMPtr.h" namespace mozilla { + +class ErrorResult; + namespace dom { +class AnyCallback; +class Promise; + class DOMRequest : public DOMEventTargetHelper, public nsIDOMDOMRequest { protected: JS::Heap mResult; nsRefPtr mError; + nsRefPtr mPromise; bool mDone; public: @@ -67,6 +74,9 @@ public: IMPL_EVENT_HANDLER(success) IMPL_EVENT_HANDLER(error) + already_AddRefed + Then(JSContext* aCx, AnyCallback* aResolveCallback, + AnyCallback* aRejectCallback, mozilla::ErrorResult& aRv); void FireSuccess(JS::Handle aResult); void FireError(const nsAString& aError); @@ -76,11 +86,7 @@ public: explicit DOMRequest(nsPIDOMWindow* aWindow); protected: - virtual ~DOMRequest() - { - mResult = JSVAL_VOID; - mozilla::DropJSObjects(this); - } + virtual ~DOMRequest(); void FireEvent(const nsAString& aType, bool aBubble, bool aCancelable); diff --git a/dom/base/test/test_domcursor.html b/dom/base/test/test_domcursor.html index bb975a67f37..d33581f124e 100644 --- a/dom/base/test/test_domcursor.html +++ b/dom/base/test/test_domcursor.html @@ -51,6 +51,7 @@ var tests = [ ok("readyState" in req, "cursor has readyState"); ok("done" in req, "cursor has finished"); ok("continue" in req, "cursor has continue"); + ok(!("then" in req), "cursor should not have a then method"); is(req.readyState, "pending", "readyState is pending"); is(req.result, undefined, "result is undefined"); diff --git a/dom/base/test/test_domrequest.html b/dom/base/test/test_domrequest.html index 0620ad52b1e..85636ba5297 100644 --- a/dom/base/test/test_domrequest.html +++ b/dom/base/test/test_domrequest.html @@ -17,59 +17,212 @@ var reqserv = SpecialPowers.getDOMRequestService(); ok("createRequest" in reqserv, "appears to be a service"); -// create a request -var req = reqserv.createRequest(window); -ok("result" in req, "request has result"); -ok("error" in req, "request has error"); -ok("onsuccess" in req, "request has onsuccess"); -ok("onerror" in req, "request has onerror"); -ok("readyState" in req, "request has readyState"); +function testBasics() { + // create a request + var req = reqserv.createRequest(window); + ok("result" in req, "request has result"); + ok("error" in req, "request has error"); + ok("onsuccess" in req, "request has onsuccess"); + ok("onerror" in req, "request has onerror"); + ok("readyState" in req, "request has readyState"); + ok("then" in req, "request has then"); -is(req.readyState, "pending", "readyState is pending"); -is(req.result, undefined, "result is undefined"); -is(req.onsuccess, null, "onsuccess is null"); -is(req.onerror, null, "onerror is null"); + is(req.readyState, "pending", "readyState is pending"); + is(req.result, undefined, "result is undefined"); + is(req.onsuccess, null, "onsuccess is null"); + is(req.onerror, null, "onerror is null"); -// fire success -var ev = null; -req.onsuccess = function(e) { - ev = e; + runTest(); } -reqserv.fireSuccess(req, "my result"); -ok(ev, "got success event"); -is(ev.type, "success", "correct type during success"); -is(ev.target, req, "correct target during success"); -is(req.readyState, "done", "correct readyState after success"); -is(req.error, null, "correct error after success"); -is(req.result, "my result", "correct result after success"); -// fire error -req = reqserv.createRequest(window); -ev = null; -req.onerror = function(e) { - ev = e; +function testSuccess() { + // fire success + var req = reqserv.createRequest(window); + var ev = null; + req.onsuccess = function(e) { + ev = e; + } + var result = null; + var promise = req.then(function(r) { + is(r, "my result", "correct result when resolving the promise"); + result = r; + runTest(); + }, function(e) { + ok(false, "promise should not be rejected"); + runTest(); + }); + ok(promise instanceof Promise, "then() should return a Promise"); + reqserv.fireSuccess(req, "my result"); + ok(ev, "got success event"); + is(ev.type, "success", "correct type during success"); + is(ev.target, req, "correct target during success"); + is(req.readyState, "done", "correct readyState after success"); + is(req.error, null, "correct error after success"); + is(req.result, "my result", "correct result after success"); + is(result, null, "Promise should not be resolved synchronously"); } -reqserv.fireError(req, "OhMyError"); -ok(ev, "got error event"); -is(ev.type, "error", "correct type during error"); -is(ev.target, req, "correct target during error"); -is(req.readyState, "done", "correct readyState after error"); -is(req.error.name, "OhMyError", "correct error after error"); -is(req.result, undefined, "correct result after error"); -// fire detailed error -req = reqserv.createRequest(window); -ev = null; -req.onerror = function(e) { - ev = e; -}; -reqserv.fireDetailedError(req, new DOMError("detailedError")); -ok(ev, "got error event"); -is(ev.type, "error", "correct type during error"); -is(ev.target, req, "correct target during error"); -is(req.readyState, "done", "correct readyState after error"); -is(req.error.name, "detailedError", "correct error after error"); -is(req.result, undefined, "correct result after error"); +function testError() { + // fire error + var req = reqserv.createRequest(window); + var ev = null; + req.onerror = function(e) { + ev = e; + } + var error = null; + var promise = req.then(function(r) { + ok(false, "promise should not be resolved"); + runTest(); + }, function(e) { + ok(e instanceof DOMError, "got error rejection"); + ok(e === req.error, "got correct error when rejecting the promise"); + error = e; + runTest(); + }); + ok(promise instanceof Promise, "then() should return a Promise"); + reqserv.fireError(req, "OhMyError"); + ok(ev, "got error event"); + is(ev.type, "error", "correct type during error"); + is(ev.target, req, "correct target during error"); + is(req.readyState, "done", "correct readyState after error"); + is(req.error.name, "OhMyError", "correct error after error"); + is(req.result, undefined, "correct result after error"); + is(error, null, "Promise should not be rejected synchronously"); +} + +function testDetailedError() { + // fire detailed error + var req = reqserv.createRequest(window); + var ev = null; + req.onerror = function(e) { + ev = e; + }; + var error = null; + var promise = req.then(function(r) { + ok(false, "promise should not be resolved"); + runTest(); + }, function(e) { + ok(e instanceof DOMError, "got error rejection"); + ok(e === req.error, "got correct error when rejecting the promise"); + error = e; + runTest(); + }); + ok(promise instanceof Promise, "then() should return a Promise"); + reqserv.fireDetailedError(req, new DOMError("detailedError")); + ok(ev, "got error event"); + is(ev.type, "error", "correct type during error"); + is(ev.target, req, "correct target during error"); + is(req.readyState, "done", "correct readyState after error"); + is(req.error.name, "detailedError", "correct error after error"); + is(req.result, undefined, "correct result after error"); + is(error, null, "Promise should not be rejected synchronously"); +} + +function testThenAfterSuccess() { + // fire success + var req = reqserv.createRequest(window); + var ev = null; + req.onsuccess = function(e) { + ev = e; + } + reqserv.fireSuccess(req, "my result"); + ok(ev, "got success event"); + is(ev.type, "success", "correct type during success"); + is(ev.target, req, "correct target during success"); + is(req.readyState, "done", "correct readyState after success"); + is(req.error, null, "correct error after success"); + is(req.result, "my result", "correct result after success"); + var result = null; + var promise = req.then(function(r) { + is(r, "my result", "correct result when resolving the promise"); + result = r; + runTest(); + }, function(e) { + ok(false, "promise should not be rejected"); + runTest(); + }); + ok(promise instanceof Promise, "then() should return a Promise"); + is(result, null, "Promise should not be resolved synchronously"); +} + +function testThenAfterError() { + // fire error + var req = reqserv.createRequest(window); + var ev = null; + req.onerror = function(e) { + ev = e; + } + reqserv.fireError(req, "OhMyError"); + ok(ev, "got error event"); + is(ev.type, "error", "correct type during error"); + is(ev.target, req, "correct target during error"); + is(req.readyState, "done", "correct readyState after error"); + is(req.error.name, "OhMyError", "correct error after error"); + is(req.result, undefined, "correct result after error"); + var error = null; + var promise = req.then(function(r) { + ok(false, "promise should not be resolved"); + runTest(); + }, function(e) { + ok(e instanceof DOMError, "got error rejection"); + ok(e === req.error, "got correct error when rejecting the promise"); + error = e; + runTest(); + }); + ok(promise instanceof Promise, "then() should return a Promise"); + is(error, null, "Promise should not be rejected synchronously"); +} + +function testDetailedError() { + // fire detailed error + var req = reqserv.createRequest(window); + var ev = null; + req.onerror = function(e) { + ev = e; + }; + var error = null; + var promise = req.then(function(r) { + ok(false, "promise should not be resolved"); + runTest(); + }, function(e) { + ok(e instanceof DOMError, "got error rejection"); + ok(e === req.error, "got correct error when rejecting the promise"); + error = e; + runTest(); + }); + ok(promise instanceof Promise, "then() should return a Promise"); + reqserv.fireDetailedError(req, new DOMError("detailedError")); + ok(ev, "got error event"); + is(ev.type, "error", "correct type during error"); + is(ev.target, req, "correct target during error"); + is(req.readyState, "done", "correct readyState after error"); + is(req.error.name, "detailedError", "correct error after error"); + is(req.result, undefined, "correct result after error"); + is(error, null, "Promise should not be rejected synchronously"); +} + +var tests = [ + testBasics, + testSuccess, + testError, + testDetailedError, + testThenAfterSuccess, + testThenAfterError, +]; + +function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); +runTest(); + diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index dcd93b3835e..ed41c3d9e9a 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -352,6 +352,10 @@ DOMInterfaces = { 'headerFile': 'mozilla/dom/DOMRect.h', }, +'DOMRequest': { + 'implicitJSContext': [ 'then' ], +}, + 'DOMSettableTokenList': { 'nativeType': 'nsDOMSettableTokenList', }, diff --git a/dom/bindings/ToJSValue.h b/dom/bindings/ToJSValue.h index 1f365ddde36..c300307f59f 100644 --- a/dom/bindings/ToJSValue.h +++ b/dom/bindings/ToJSValue.h @@ -236,6 +236,24 @@ ToJSValue(JSContext* aCx, JS::Handle aArgument, return MaybeWrapValue(aCx, aValue); } +// Accept existing JS values on the Heap (which may not be same-compartment with us +inline bool +ToJSValue(JSContext* aCx, const JS::Heap& aArgument, + JS::MutableHandle aValue) +{ + aValue.set(aArgument); + return MaybeWrapValue(aCx, aValue); +} + +// Accept existing rooted JS values (which may not be same-compartment with us +inline bool +ToJSValue(JSContext* aCx, const JS::Rooted& aArgument, + JS::MutableHandle aValue) +{ + aValue.set(aArgument); + return MaybeWrapValue(aCx, aValue); +} + // Accept nsresult, for use in rejections, and create an XPCOM // exception object representing that nsresult. bool diff --git a/dom/telephony/test/marionette/head.js b/dom/telephony/test/marionette/head.js index ea7ea2933ef..059b3a2542f 100644 --- a/dom/telephony/test/marionette/head.js +++ b/dom/telephony/test/marionette/head.js @@ -1316,17 +1316,8 @@ function sendMMI(aMmi) { let deferred = Promise.defer(); telephony.dial(aMmi) - .then(request => { - ok(request instanceof DOMRequest, - "request is instanceof " + request.constructor); - - request.addEventListener("success", function(event) { - deferred.resolve(request.result); - }); - - request.addEventListener("error", function(event) { - deferred.reject(request.error); - }); + .then(result => { + deferred.resolve(result); }, cause => { deferred.reject(cause); }); diff --git a/dom/webidl/DOMCursor.webidl b/dom/webidl/DOMCursor.webidl index b18a75539bd..3ad36a76fa1 100644 --- a/dom/webidl/DOMCursor.webidl +++ b/dom/webidl/DOMCursor.webidl @@ -3,8 +3,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -interface DOMCursor : DOMRequest { +interface DOMCursor : EventTarget { readonly attribute boolean done; [Throws] void continue(); }; + +DOMCursor implements DOMRequestShared; diff --git a/dom/webidl/DOMRequest.webidl b/dom/webidl/DOMRequest.webidl index a93704bd221..03944690456 100644 --- a/dom/webidl/DOMRequest.webidl +++ b/dom/webidl/DOMRequest.webidl @@ -5,7 +5,8 @@ enum DOMRequestReadyState { "pending", "done" }; -interface DOMRequest : EventTarget { +[NoInterfaceObject] +interface DOMRequestShared { readonly attribute DOMRequestReadyState readyState; readonly attribute any result; @@ -14,3 +15,13 @@ interface DOMRequest : EventTarget { attribute EventHandler onsuccess; attribute EventHandler onerror; }; + +interface DOMRequest : EventTarget { + // The [TreatNonCallableAsNull] annotation is required since then() should do + // nothing instead of throwing errors when non-callable arguments are passed. + [NewObject, Throws] + Promise then([TreatNonCallableAsNull] optional AnyCallback? fulfillCallback = null, + [TreatNonCallableAsNull] optional AnyCallback? rejectCallback = null); +}; + +DOMRequest implements DOMRequestShared;