summaryrefslogtreecommitdiffstats
path: root/dom/promise
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /dom/promise
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/promise')
-rw-r--r--dom/promise/Promise-inl.h341
-rw-r--r--dom/promise/Promise.cpp1109
-rw-r--r--dom/promise/Promise.h453
-rw-r--r--dom/promise/PromiseDebugging.cpp296
-rw-r--r--dom/promise/PromiseDebugging.h84
-rw-r--r--dom/promise/PromiseNativeHandler.cpp63
-rw-r--r--dom/promise/PromiseNativeHandler.h66
-rw-r--r--dom/promise/PromiseWorkerProxy.h219
-rw-r--r--dom/promise/gtest/NativeThenHandler.cpp160
-rw-r--r--dom/promise/gtest/ThenWithCycleCollectedArgsJS.cpp158
-rw-r--r--dom/promise/gtest/moz.build14
-rw-r--r--dom/promise/moz.build42
-rw-r--r--dom/promise/tests/chrome.ini15
-rw-r--r--dom/promise/tests/file_promise_and_timeout_ordering.js18
-rw-r--r--dom/promise/tests/file_promise_argument_tests.js175
-rw-r--r--dom/promise/tests/file_promise_job_with_bind_from_discarded_iframe.html14
-rw-r--r--dom/promise/tests/file_promise_retval_tests.js56
-rw-r--r--dom/promise/tests/file_promise_xrays.html34
-rw-r--r--dom/promise/tests/mochitest.ini28
-rw-r--r--dom/promise/tests/promise_uncatchable_exception.js11
-rw-r--r--dom/promise/tests/test_bug883683.html41
-rw-r--r--dom/promise/tests/test_on_new_promise.html45
-rw-r--r--dom/promise/tests/test_on_promise_settled.html53
-rw-r--r--dom/promise/tests/test_on_promise_settled_duplicates.html58
-rw-r--r--dom/promise/tests/test_promise.html844
-rw-r--r--dom/promise/tests/test_promise_and_timeout_ordering.html16
-rw-r--r--dom/promise/tests/test_promise_and_timeout_ordering_workers.html14
-rw-r--r--dom/promise/tests/test_promise_argument.html49
-rw-r--r--dom/promise/tests/test_promise_argument_xrays.html90
-rw-r--r--dom/promise/tests/test_promise_callback_retval.html53
-rw-r--r--dom/promise/tests/test_promise_job_with_bind_from_discarded_iframe.html63
-rw-r--r--dom/promise/tests/test_promise_retval.html51
-rw-r--r--dom/promise/tests/test_promise_retval_xrays.html94
-rw-r--r--dom/promise/tests/test_promise_uncatchable_exception.html35
-rw-r--r--dom/promise/tests/test_promise_utils.html313
-rw-r--r--dom/promise/tests/test_promise_xrays.html365
-rw-r--r--dom/promise/tests/test_resolve.html66
-rw-r--r--dom/promise/tests/test_resolver_return_value.html40
-rw-r--r--dom/promise/tests/test_species_getter.html25
-rw-r--r--dom/promise/tests/test_thenable_vs_promise_ordering.html29
-rw-r--r--dom/promise/tests/test_webassembly_compile.html446
-rw-r--r--dom/promise/tests/test_webassembly_compile_sample.wasmbin0 -> 16053 bytes
-rw-r--r--dom/promise/tests/test_webassembly_compile_worker.js55
-rw-r--r--dom/promise/tests/test_webassembly_compile_worker_terminate.js13
-rw-r--r--dom/promise/tests/unit/test_monitor_uncaught.js322
-rw-r--r--dom/promise/tests/unit/test_promise_job_across_sandbox.js221
-rw-r--r--dom/promise/tests/unit/test_promise_unhandled_rejection.js139
-rw-r--r--dom/promise/tests/unit/xpcshell.ini6
48 files changed, 6902 insertions, 0 deletions
diff --git a/dom/promise/Promise-inl.h b/dom/promise/Promise-inl.h
new file mode 100644
index 0000000000..a7a6ebd0fd
--- /dev/null
+++ b/dom/promise/Promise-inl.h
@@ -0,0 +1,341 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+#ifndef mozilla_dom_Promise_inl_h
+#define mozilla_dom_Promise_inl_h
+
+#include <type_traits>
+#include <utility>
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "nsCycleCollectionParticipant.h"
+
+namespace mozilla::dom {
+
+class PromiseNativeThenHandlerBase : public PromiseNativeHandler {
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(PromiseNativeThenHandlerBase)
+
+ PromiseNativeThenHandlerBase(Promise* aPromise) : mPromise(aPromise) {}
+
+ virtual bool HasResolvedCallback() = 0;
+ virtual bool HasRejectedCallback() = 0;
+
+ MOZ_CAN_RUN_SCRIPT void ResolvedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ MOZ_CAN_RUN_SCRIPT void RejectedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ protected:
+ virtual ~PromiseNativeThenHandlerBase() = default;
+
+ MOZ_CAN_RUN_SCRIPT virtual already_AddRefed<Promise> CallResolveCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv) = 0;
+ MOZ_CAN_RUN_SCRIPT virtual already_AddRefed<Promise> CallRejectCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv) = 0;
+
+ virtual void Traverse(nsCycleCollectionTraversalCallback&) = 0;
+ virtual void Unlink() = 0;
+ virtual void Trace(const TraceCallbacks& aCallbacks, void* aClosure) = 0;
+
+ RefPtr<Promise> mPromise;
+};
+
+namespace {
+
+template <typename T, bool = IsRefcounted<std::remove_pointer_t<T>>::value,
+ bool = (std::is_convertible_v<T, nsISupports*> ||
+ std::is_convertible_v<T*, nsISupports*>)>
+struct StorageTypeHelper {
+ using Type = T;
+};
+
+template <typename T>
+struct StorageTypeHelper<T, true, true> {
+ using Type = nsCOMPtr<T>;
+};
+
+template <typename T>
+struct StorageTypeHelper<nsCOMPtr<T>, true, true> {
+ using Type = nsCOMPtr<T>;
+};
+
+template <typename T>
+struct StorageTypeHelper<T*, true, false> {
+ using Type = RefPtr<T>;
+};
+
+template <typename T>
+struct StorageTypeHelper<JS::Handle<T>, false, false> {
+ using Type = JS::Heap<T>;
+};
+
+template <template <typename> class SmartPtr, typename T>
+struct StorageTypeHelper<SmartPtr<T>, true, false>
+ : std::enable_if<std::is_convertible_v<SmartPtr<T>, T*>, RefPtr<T>> {
+ using Type = typename StorageTypeHelper::enable_if::type;
+};
+
+template <typename T>
+using StorageType = typename StorageTypeHelper<std::decay_t<T>>::Type;
+
+// Helpers to choose the correct argument type based on the storage type. Smart
+// pointers are converted to the corresponding raw pointer type. Everything else
+// is passed by move reference.
+//
+// Note: We can't just use std::forward for this because the input type may be a
+// raw pointer which does not match the argument type, and while the
+// spec-compliant behavior there should still give us the expected results, MSVC
+// considers it an illegal use of std::forward.
+template <template <typename> class SmartPtr, typename T>
+decltype(std::declval<SmartPtr<T>>().get()) ArgType(SmartPtr<T>& aVal) {
+ return aVal.get();
+}
+
+template <typename T>
+T&& ArgType(T& aVal) {
+ return std::move(aVal);
+}
+
+using ::ImplCycleCollectionUnlink;
+
+template <typename ResolveCallback, typename RejectCallback, typename ArgsTuple,
+ typename JSArgsTuple>
+class NativeThenHandler;
+
+template <typename ResolveCallback, typename RejectCallback, typename... Args,
+ typename... JSArgs>
+class NativeThenHandler<ResolveCallback, RejectCallback, std::tuple<Args...>,
+ std::tuple<JSArgs...>>
+ final : public PromiseNativeThenHandlerBase {
+ public:
+ /**
+ * @param aPromise A promise that will be settled by the result of the
+ * callbacks. Any thrown value to ErrorResult passed to those callbacks will
+ * be used to reject the promise, otherwise the promise will be resolved with
+ * the return value.
+ * @param aOnResolve A resolve callback
+ * @param aOnReject A reject callback
+ * @param aArgs The custom arguments to be passed to the both callbacks. The
+ * handler class will grab them to make them live long enough and to allow
+ * cycle collection.
+ * @param aJSArgs The JS arguments to be passed to the both callbacks, after
+ * native arguments. The handler will also grab them and allow garbage
+ * collection.
+ *
+ * XXX(krosylight): ideally there should be two signatures, with or without a
+ * promise parameter. Unfortunately doing so confuses the compiler and errors
+ * out, because nothing prevents promise from being ResolveCallback.
+ */
+ NativeThenHandler(Promise* aPromise, Maybe<ResolveCallback>&& aOnResolve,
+ Maybe<RejectCallback>&& aOnReject,
+ std::tuple<std::remove_reference_t<Args>...>&& aArgs,
+ std::tuple<std::remove_reference_t<JSArgs>...>&& aJSArgs)
+ : PromiseNativeThenHandlerBase(aPromise),
+ mOnResolve(std::forward<Maybe<ResolveCallback>>(aOnResolve)),
+ mOnReject(std::forward<Maybe<RejectCallback>>(aOnReject)),
+ mArgs(std::forward<decltype(aArgs)>(aArgs)),
+ mJSArgs(std::forward<decltype(aJSArgs)>(aJSArgs)) {
+ if constexpr (std::tuple_size<decltype(mJSArgs)>::value > 0) {
+ mozilla::HoldJSObjects(this);
+ }
+ }
+
+ protected:
+ ~NativeThenHandler() override {
+ if constexpr (std::tuple_size<decltype(mJSArgs)>::value > 0) {
+ mozilla::DropJSObjects(this);
+ }
+ }
+
+ void Traverse(nsCycleCollectionTraversalCallback& cb) override {
+ auto* tmp = this;
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mArgs)
+ }
+
+ void Unlink() override {
+ auto* tmp = this;
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mArgs)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mJSArgs)
+ }
+
+ void Trace(const TraceCallbacks& aCallbacks, void* aClosure) override {
+ std::apply(
+ [&aCallbacks, aClosure](auto&&... aArgs) {
+ (aCallbacks.Trace(&aArgs, "mJSArgs[]", aClosure), ...);
+ },
+ mJSArgs);
+ }
+
+ bool HasResolvedCallback() override { return mOnResolve.isSome(); }
+ bool HasRejectedCallback() override { return mOnReject.isSome(); }
+
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CallResolveCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv) override {
+ return CallCallback(aCx, *mOnResolve, aValue, aRv);
+ }
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CallRejectCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv) override {
+ return CallCallback(aCx, *mOnReject, aValue, aRv);
+ }
+
+ // mJSArgs are marked with Trace() above, so they can be safely converted to
+ // Handles. But we should not circumvent the read barrier, so call
+ // exposeToActiveJS explicitly.
+ template <typename T>
+ static JS::Handle<T> GetJSArgHandleForCall(JS::Heap<T>& aArg) {
+ aArg.exposeToActiveJS();
+ return JS::Handle<T>::fromMarkedLocation(aArg.address());
+ }
+
+ template <typename TCallback, size_t... Indices, size_t... JSIndices>
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CallCallback(
+ JSContext* aCx, const TCallback& aHandler, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv, std::index_sequence<Indices...>,
+ std::index_sequence<JSIndices...>) {
+ return aHandler(aCx, aValue, aRv, ArgType(std::get<Indices>(mArgs))...,
+ GetJSArgHandleForCall(std::get<JSIndices>(mJSArgs))...);
+ }
+
+ template <typename TCallback>
+ MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CallCallback(
+ JSContext* aCx, const TCallback& aHandler, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ return CallCallback(aCx, aHandler, aValue, aRv,
+ std::index_sequence_for<Args...>{},
+ std::index_sequence_for<JSArgs...>{});
+ }
+
+ Maybe<ResolveCallback> mOnResolve;
+ Maybe<RejectCallback> mOnReject;
+
+ std::tuple<StorageType<Args>...> mArgs;
+ std::tuple<StorageType<JSArgs>...> mJSArgs;
+};
+
+} // anonymous namespace
+
+template <typename ResolveCallback, typename RejectCallback, typename... Args,
+ typename... JSArgs>
+Result<RefPtr<Promise>, nsresult>
+Promise::ThenCatchWithCycleCollectedArgsJSImpl(
+ Maybe<ResolveCallback>&& aOnResolve, Maybe<RejectCallback>&& aOnReject,
+ std::tuple<Args...>&& aArgs, std::tuple<JSArgs...>&& aJSArgs) {
+ using HandlerType =
+ NativeThenHandler<ResolveCallback, RejectCallback, std::tuple<Args...>,
+ std::tuple<JSArgs...>>;
+
+ ErrorResult rv;
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), rv);
+ if (rv.Failed()) {
+ return Err(rv.StealNSResult());
+ }
+
+ auto* handler = new (fallible)
+ HandlerType(promise, std::forward<Maybe<ResolveCallback>>(aOnResolve),
+ std::forward<Maybe<RejectCallback>>(aOnReject),
+ std::forward<std::tuple<Args...>>(aArgs),
+ std::forward<std::tuple<JSArgs...>>(aJSArgs));
+
+ if (!handler) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ AppendNativeHandler(handler);
+ return std::move(promise);
+}
+
+template <typename ResolveCallback, typename RejectCallback, typename... Args>
+Promise::ThenResult<ResolveCallback, Args...>
+Promise::ThenCatchWithCycleCollectedArgsImpl(
+ Maybe<ResolveCallback>&& aOnResolve, Maybe<RejectCallback>&& aOnReject,
+ Args&&... aArgs) {
+ return ThenCatchWithCycleCollectedArgsJSImpl(
+ std::forward<Maybe<ResolveCallback>>(aOnResolve),
+ std::forward<Maybe<RejectCallback>>(aOnReject),
+ std::make_tuple(std::forward<Args>(aArgs)...), std::make_tuple());
+}
+
+template <typename ResolveCallback, typename RejectCallback, typename... Args>
+Promise::ThenResult<ResolveCallback, Args...>
+Promise::ThenCatchWithCycleCollectedArgs(ResolveCallback&& aOnResolve,
+ RejectCallback&& aOnReject,
+ Args&&... aArgs) {
+ return ThenCatchWithCycleCollectedArgsImpl(Some(aOnResolve), Some(aOnReject),
+ std::forward<Args>(aArgs)...);
+}
+
+template <typename Callback, typename... Args>
+Promise::ThenResult<Callback, Args...> Promise::ThenWithCycleCollectedArgs(
+ Callback&& aOnResolve, Args&&... aArgs) {
+ return ThenCatchWithCycleCollectedArgsImpl(Some(aOnResolve),
+ Maybe<Callback>(Nothing()),
+ std::forward<Args>(aArgs)...);
+}
+
+template <typename Callback, typename... Args>
+Promise::ThenResult<Callback, Args...> Promise::CatchWithCycleCollectedArgs(
+ Callback&& aOnReject, Args&&... aArgs) {
+ return ThenCatchWithCycleCollectedArgsImpl(Maybe<Callback>(Nothing()),
+ Some(aOnReject),
+ std::forward<Args>(aArgs)...);
+}
+
+template <typename ResolveCallback, typename RejectCallback, typename ArgsTuple,
+ typename JSArgsTuple>
+Result<RefPtr<Promise>, nsresult> Promise::ThenCatchWithCycleCollectedArgsJS(
+ ResolveCallback&& aOnResolve, RejectCallback&& aOnReject, ArgsTuple&& aArgs,
+ JSArgsTuple&& aJSArgs) {
+ return ThenCatchWithCycleCollectedArgsJSImpl(
+ Some(aOnResolve), Some(aOnReject), std::forward<ArgsTuple>(aArgs),
+ std::forward<JSArgsTuple>(aJSArgs));
+}
+
+template <typename Callback, typename ArgsTuple, typename JSArgsTuple>
+Result<RefPtr<Promise>, nsresult> Promise::ThenWithCycleCollectedArgsJS(
+ Callback&& aOnResolve, ArgsTuple&& aArgs, JSArgsTuple&& aJSArgs) {
+ return ThenCatchWithCycleCollectedArgsJSImpl(
+ Some(aOnResolve), Maybe<Callback>(Nothing()),
+ std::forward<ArgsTuple>(aArgs), std::forward<JSArgsTuple>(aJSArgs));
+}
+
+template <typename ResolveCallback, typename RejectCallback, typename... Args>
+void Promise::AddCallbacksWithCycleCollectedArgs(ResolveCallback&& aOnResolve,
+ RejectCallback&& aOnReject,
+ Args&&... aArgs) {
+ auto onResolve =
+ [aOnResolve](JSContext* aCx, JS::Handle<JS::Value> value,
+ ErrorResult& aRv,
+ StorageType<Args>&&... aArgs) -> already_AddRefed<Promise> {
+ aOnResolve(aCx, value, aRv, aArgs...);
+ return nullptr;
+ };
+ auto onReject =
+ [aOnReject](JSContext* aCx, JS::Handle<JS::Value> value, ErrorResult& aRv,
+ StorageType<Args>&&... aArgs) -> already_AddRefed<Promise> {
+ aOnReject(aCx, value, aRv, aArgs...);
+ return nullptr;
+ };
+
+ // Note: explicit template parameters for clang<7/gcc<8 without "Template
+ // argument deduction for class templates" support
+ AppendNativeHandler(
+ new NativeThenHandler<decltype(onResolve), decltype(onReject),
+ std::tuple<Args...>, std::tuple<>>(
+ nullptr, Some(onResolve), Some(onReject),
+ std::make_tuple(std::forward<Args>(aArgs)...), std::make_tuple()));
+}
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_Promise_inl_h
diff --git a/dom/promise/Promise.cpp b/dom/promise/Promise.cpp
new file mode 100644
index 0000000000..941b2a7cc6
--- /dev/null
+++ b/dom/promise/Promise.cpp
@@ -0,0 +1,1109 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/Promise-inl.h"
+
+#include "js/Debug.h"
+
+#include "mozilla/Atomics.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/Unused.h"
+
+#include "mozilla/dom/AutoEntryScript.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/DOMExceptionBinding.h"
+#include "mozilla/dom/Exceptions.h"
+#include "mozilla/dom/MediaStreamError.h"
+#include "mozilla/dom/PromiseBinding.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/UserActivation.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/WorkerRunnable.h"
+#include "mozilla/dom/WorkerRef.h"
+#include "mozilla/dom/WorkletImpl.h"
+#include "mozilla/dom/WorkletGlobalScope.h"
+
+#include "jsfriendapi.h"
+#include "js/Exception.h" // JS::ExceptionStack
+#include "js/Object.h" // JS::GetCompartment
+#include "js/StructuredClone.h"
+#include "nsContentUtils.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsDebug.h"
+#include "nsGlobalWindow.h"
+#include "nsIScriptObjectPrincipal.h"
+#include "nsJSEnvironment.h"
+#include "nsJSPrincipals.h"
+#include "nsJSUtils.h"
+#include "nsPIDOMWindow.h"
+#include "PromiseDebugging.h"
+#include "PromiseNativeHandler.h"
+#include "PromiseWorkerProxy.h"
+#include "WrapperFactory.h"
+#include "xpcpublic.h"
+#include "xpcprivate.h"
+
+namespace mozilla::dom {
+
+// Promise
+
+NS_IMPL_CYCLE_COLLECTION_SINGLE_ZONE_SCRIPT_HOLDER_CLASS(Promise)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Promise)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR
+ tmp->mPromiseObj = nullptr;
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Promise)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Promise)
+ // If you add new JS member variables, you may need to stop using
+ // NS_IMPL_CYCLE_COLLECTION_SINGLE_ZONE_SCRIPT_HOLDER_CLASS.
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mPromiseObj);
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+Promise::Promise(nsIGlobalObject* aGlobal)
+ : mGlobal(aGlobal), mPromiseObj(nullptr) {
+ MOZ_ASSERT(mGlobal);
+
+ mozilla::HoldJSObjects(this);
+}
+
+Promise::~Promise() { mozilla::DropJSObjects(this); }
+
+// static
+already_AddRefed<Promise> Promise::Create(
+ nsIGlobalObject* aGlobal, ErrorResult& aRv,
+ PropagateUserInteraction aPropagateUserInteraction) {
+ if (!aGlobal) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+ RefPtr<Promise> p = new Promise(aGlobal);
+ p->CreateWrapper(aRv, aPropagateUserInteraction);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ return p.forget();
+}
+
+// static
+already_AddRefed<Promise> Promise::CreateInfallible(
+ nsIGlobalObject* aGlobal,
+ PropagateUserInteraction aPropagateUserInteraction) {
+ MOZ_ASSERT(aGlobal);
+ RefPtr<Promise> p = new Promise(aGlobal);
+ IgnoredErrorResult rv;
+ p->CreateWrapper(rv, aPropagateUserInteraction);
+ if (rv.Failed() && rv.ErrorCodeIs(NS_ERROR_OUT_OF_MEMORY)) {
+ MOZ_CRASH("Out of memory");
+ }
+
+ // We may have failed to init the wrapper here, because nsIGlobalObject had
+ // null GlobalJSObject. In that case we consider the JS realm is dead, which
+ // means:
+ // 1. This promise can't be settled.
+ // 2. Nothing can subscribe this promise anymore from that realm.
+ // Such condition makes this promise a no-op object.
+ (void)NS_WARN_IF(!p->PromiseObj());
+
+ return p.forget();
+}
+
+bool Promise::MaybePropagateUserInputEventHandling() {
+ MOZ_ASSERT(mPromiseObj,
+ "Should be called only if the wrapper is successfully created");
+ JS::PromiseUserInputEventHandlingState state =
+ UserActivation::IsHandlingUserInput()
+ ? JS::PromiseUserInputEventHandlingState::HadUserInteractionAtCreation
+ : JS::PromiseUserInputEventHandlingState::
+ DidntHaveUserInteractionAtCreation;
+ JS::Rooted<JSObject*> p(RootingCx(), mPromiseObj);
+ return JS::SetPromiseUserInputEventHandlingState(p, state);
+}
+
+// static
+already_AddRefed<Promise> Promise::Resolve(
+ nsIGlobalObject* aGlobal, JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv, PropagateUserInteraction aPropagateUserInteraction) {
+ JSAutoRealm ar(aCx, aGlobal->GetGlobalJSObject());
+ JS::Rooted<JSObject*> p(aCx, JS::CallOriginalPromiseResolve(aCx, aValue));
+ if (!p) {
+ aRv.NoteJSContextException(aCx);
+ return nullptr;
+ }
+
+ return CreateFromExisting(aGlobal, p, aPropagateUserInteraction);
+}
+
+// static
+already_AddRefed<Promise> Promise::Reject(nsIGlobalObject* aGlobal,
+ JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ JSAutoRealm ar(aCx, aGlobal->GetGlobalJSObject());
+ JS::Rooted<JSObject*> p(aCx, JS::CallOriginalPromiseReject(aCx, aValue));
+ if (!p) {
+ aRv.NoteJSContextException(aCx);
+ return nullptr;
+ }
+
+ // This promise will never be resolved, so we pass
+ // eDontPropagateUserInteraction for aPropagateUserInteraction
+ // unconditionally.
+ return CreateFromExisting(aGlobal, p, eDontPropagateUserInteraction);
+}
+
+// static
+already_AddRefed<Promise> Promise::All(
+ JSContext* aCx, const nsTArray<RefPtr<Promise>>& aPromiseList,
+ ErrorResult& aRv, PropagateUserInteraction aPropagateUserInteraction) {
+ JS::Rooted<JSObject*> globalObj(aCx, JS::CurrentGlobalOrNull(aCx));
+ if (!globalObj) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(globalObj);
+ if (!global) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ JS::RootedVector<JSObject*> promises(aCx);
+ if (!promises.reserve(aPromiseList.Length())) {
+ aRv.NoteJSContextException(aCx);
+ return nullptr;
+ }
+
+ for (const auto& promise : aPromiseList) {
+ JS::Rooted<JSObject*> promiseObj(aCx, promise->PromiseObj());
+ if (!promiseObj) {
+ // No-op object will never settle, so we return a no-op Promise here,
+ // which is equivalent of returning the existing no-op one.
+ return do_AddRef(promise);
+ }
+ // Just in case, make sure these are all in the context compartment.
+ if (!JS_WrapObject(aCx, &promiseObj)) {
+ aRv.NoteJSContextException(aCx);
+ return nullptr;
+ }
+ promises.infallibleAppend(promiseObj);
+ }
+
+ JS::Rooted<JSObject*> result(aCx, JS::GetWaitForAllPromise(aCx, promises));
+ if (!result) {
+ aRv.NoteJSContextException(aCx);
+ return nullptr;
+ }
+
+ return CreateFromExisting(global, result, aPropagateUserInteraction);
+}
+
+void Promise::Then(JSContext* aCx,
+ // aCalleeGlobal may not be in the compartment of aCx, when
+ // called over Xrays.
+ JS::Handle<JSObject*> aCalleeGlobal,
+ AnyCallback* aResolveCallback, AnyCallback* aRejectCallback,
+ JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv) {
+ NS_ASSERT_OWNINGTHREAD(Promise);
+
+ // Let's hope this does the right thing with Xrays... Ensure everything is
+ // just in the caller compartment; that ought to do the trick. In theory we
+ // should consider aCalleeGlobal, but in practice our only caller is
+ // DOMRequest::Then, which is not working with a Promise subclass, so things
+ // should be OK.
+ JS::Rooted<JSObject*> promise(aCx, PromiseObj());
+ if (!promise) {
+ // This promise is no-op, so do nothing.
+ return;
+ }
+
+ if (!JS_WrapObject(aCx, &promise)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+
+ JS::Rooted<JSObject*> resolveCallback(aCx);
+ if (aResolveCallback) {
+ resolveCallback = aResolveCallback->CallbackOrNull();
+ if (!JS_WrapObject(aCx, &resolveCallback)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+ }
+
+ JS::Rooted<JSObject*> rejectCallback(aCx);
+ if (aRejectCallback) {
+ rejectCallback = aRejectCallback->CallbackOrNull();
+ if (!JS_WrapObject(aCx, &rejectCallback)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+ }
+
+ JS::Rooted<JSObject*> retval(aCx);
+ retval = JS::CallOriginalPromiseThen(aCx, promise, resolveCallback,
+ rejectCallback);
+ if (!retval) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+
+ aRetval.setObject(*retval);
+}
+
+static void SettlePromise(Promise* aSettlingPromise, Promise* aCallbackPromise,
+ ErrorResult& aRv) {
+ if (!aSettlingPromise) {
+ return;
+ }
+ if (aRv.IsUncatchableException()) {
+ return;
+ }
+ if (aRv.Failed()) {
+ aSettlingPromise->MaybeReject(std::move(aRv));
+ return;
+ }
+ if (aCallbackPromise) {
+ aSettlingPromise->MaybeResolve(aCallbackPromise);
+ } else {
+ aSettlingPromise->MaybeResolveWithUndefined();
+ }
+}
+
+void PromiseNativeThenHandlerBase::ResolvedCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv) {
+ if (!HasResolvedCallback()) {
+ mPromise->MaybeResolve(aValue);
+ return;
+ }
+ RefPtr<Promise> promise = CallResolveCallback(aCx, aValue, aRv);
+ SettlePromise(mPromise, promise, aRv);
+}
+
+void PromiseNativeThenHandlerBase::RejectedCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv) {
+ if (!HasRejectedCallback()) {
+ mPromise->MaybeReject(aValue);
+ return;
+ }
+ RefPtr<Promise> promise = CallRejectCallback(aCx, aValue, aRv);
+ SettlePromise(mPromise, promise, aRv);
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(PromiseNativeThenHandlerBase)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PromiseNativeThenHandlerBase)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise)
+ tmp->Traverse(cb);
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PromiseNativeThenHandlerBase)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise)
+ tmp->Unlink();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PromiseNativeThenHandlerBase)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(PromiseNativeThenHandlerBase)
+ tmp->Trace(aCallbacks, aClosure);
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PromiseNativeThenHandlerBase)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PromiseNativeThenHandlerBase)
+
+Result<RefPtr<Promise>, nsresult> Promise::ThenWithoutCycleCollection(
+ const std::function<already_AddRefed<Promise>(
+ JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv)>&
+ aCallback) {
+ return ThenWithCycleCollectedArgs(aCallback);
+}
+
+void Promise::CreateWrapper(
+ ErrorResult& aRv, PropagateUserInteraction aPropagateUserInteraction) {
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(mGlobal)) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+ JSContext* cx = jsapi.cx();
+ mPromiseObj = JS::NewPromiseObject(cx, nullptr);
+ if (!mPromiseObj) {
+ JS_ClearPendingException(cx);
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ if (aPropagateUserInteraction == ePropagateUserInteraction) {
+ Unused << MaybePropagateUserInputEventHandling();
+ }
+}
+
+void Promise::MaybeResolve(JSContext* aCx, JS::Handle<JS::Value> aValue) {
+ NS_ASSERT_OWNINGTHREAD(Promise);
+
+ JS::Rooted<JSObject*> p(aCx, PromiseObj());
+ if (!p || !JS::ResolvePromise(aCx, p, aValue)) {
+ // Now what? There's nothing sane to do here.
+ JS_ClearPendingException(aCx);
+ }
+}
+
+void Promise::MaybeReject(JSContext* aCx, JS::Handle<JS::Value> aValue) {
+ NS_ASSERT_OWNINGTHREAD(Promise);
+
+ JS::Rooted<JSObject*> p(aCx, PromiseObj());
+ if (!p || !JS::RejectPromise(aCx, p, aValue)) {
+ // Now what? There's nothing sane to do here.
+ JS_ClearPendingException(aCx);
+ }
+}
+
+#define SLOT_NATIVEHANDLER 0
+#define SLOT_NATIVEHANDLER_TASK 1
+
+enum class NativeHandlerTask : int32_t { Resolve, Reject };
+
+MOZ_CAN_RUN_SCRIPT
+static bool NativeHandlerCallback(JSContext* aCx, unsigned aArgc,
+ JS::Value* aVp) {
+ JS::CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+ JS::Value v =
+ js::GetFunctionNativeReserved(&args.callee(), SLOT_NATIVEHANDLER);
+ MOZ_ASSERT(v.isObject());
+
+ JS::Rooted<JSObject*> obj(aCx, &v.toObject());
+ PromiseNativeHandler* handler = nullptr;
+ if (NS_FAILED(UNWRAP_OBJECT(PromiseNativeHandler, &obj, handler))) {
+ return Throw(aCx, NS_ERROR_UNEXPECTED);
+ }
+
+ v = js::GetFunctionNativeReserved(&args.callee(), SLOT_NATIVEHANDLER_TASK);
+ NativeHandlerTask task = static_cast<NativeHandlerTask>(v.toInt32());
+
+ ErrorResult rv;
+ if (task == NativeHandlerTask::Resolve) {
+ // handler is kept alive by "obj" on the stack.
+ MOZ_KnownLive(handler)->ResolvedCallback(aCx, args.get(0), rv);
+ } else {
+ MOZ_ASSERT(task == NativeHandlerTask::Reject);
+ // handler is kept alive by "obj" on the stack.
+ MOZ_KnownLive(handler)->RejectedCallback(aCx, args.get(0), rv);
+ }
+
+ return !rv.MaybeSetPendingException(aCx);
+}
+
+static JSObject* CreateNativeHandlerFunction(JSContext* aCx,
+ JS::Handle<JSObject*> aHolder,
+ NativeHandlerTask aTask) {
+ JSFunction* func = js::NewFunctionWithReserved(aCx, NativeHandlerCallback,
+ /* nargs = */ 1,
+ /* flags = */ 0, nullptr);
+ if (!func) {
+ return nullptr;
+ }
+
+ JS::Rooted<JSObject*> obj(aCx, JS_GetFunctionObject(func));
+
+ JS::AssertObjectIsNotGray(aHolder);
+ js::SetFunctionNativeReserved(obj, SLOT_NATIVEHANDLER,
+ JS::ObjectValue(*aHolder));
+ js::SetFunctionNativeReserved(obj, SLOT_NATIVEHANDLER_TASK,
+ JS::Int32Value(static_cast<int32_t>(aTask)));
+
+ return obj;
+}
+
+namespace {
+
+class PromiseNativeHandlerShim final : public PromiseNativeHandler {
+ RefPtr<PromiseNativeHandler> mInner;
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ enum InnerState{
+ NotCleared,
+ ClearedFromResolve,
+ ClearedFromReject,
+ ClearedFromCC,
+ };
+ InnerState mState = NotCleared;
+#endif
+
+ ~PromiseNativeHandlerShim() = default;
+
+ public:
+ explicit PromiseNativeHandlerShim(PromiseNativeHandler* aInner)
+ : mInner(aInner) {
+ MOZ_DIAGNOSTIC_ASSERT(mInner);
+ }
+
+ MOZ_CAN_RUN_SCRIPT
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override {
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ MOZ_DIAGNOSTIC_ASSERT(mState != ClearedFromResolve);
+ MOZ_DIAGNOSTIC_ASSERT(mState != ClearedFromReject);
+ MOZ_DIAGNOSTIC_ASSERT(mState != ClearedFromCC);
+#else
+ if (!mInner) {
+ return;
+ }
+#endif
+ RefPtr<PromiseNativeHandler> inner = std::move(mInner);
+ inner->ResolvedCallback(aCx, aValue, aRv);
+ MOZ_ASSERT(!mInner);
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mState = ClearedFromResolve;
+#endif
+ }
+
+ MOZ_CAN_RUN_SCRIPT
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override {
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ MOZ_DIAGNOSTIC_ASSERT(mState != ClearedFromResolve);
+ MOZ_DIAGNOSTIC_ASSERT(mState != ClearedFromReject);
+ MOZ_DIAGNOSTIC_ASSERT(mState != ClearedFromCC);
+#else
+ if (!mInner) {
+ return;
+ }
+#endif
+ RefPtr<PromiseNativeHandler> inner = std::move(mInner);
+ inner->RejectedCallback(aCx, aValue, aRv);
+ MOZ_ASSERT(!mInner);
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mState = ClearedFromReject;
+#endif
+ }
+
+ bool WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto,
+ JS::MutableHandle<JSObject*> aWrapper) {
+ return PromiseNativeHandler_Binding::Wrap(aCx, this, aGivenProto, aWrapper);
+ }
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(PromiseNativeHandlerShim)
+};
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(PromiseNativeHandlerShim)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PromiseNativeHandlerShim)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mInner)
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ tmp->mState = ClearedFromCC;
+#endif
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PromiseNativeHandlerShim)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInner)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PromiseNativeHandlerShim)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PromiseNativeHandlerShim)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PromiseNativeHandlerShim)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+} // anonymous namespace
+
+void Promise::AppendNativeHandler(PromiseNativeHandler* aRunnable) {
+ NS_ASSERT_OWNINGTHREAD(Promise);
+
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!mPromiseObj || !jsapi.Init(mGlobal))) {
+ // Our API doesn't allow us to return a useful error. Not like this should
+ // happen anyway.
+ return;
+ }
+
+ // The self-hosted promise js may keep the object we pass to it alive
+ // for quite a while depending on when GC runs. Therefore, pass a shim
+ // object instead. The shim will free its inner PromiseNativeHandler
+ // after the promise has settled just like our previous c++ promises did.
+ RefPtr<PromiseNativeHandlerShim> shim =
+ new PromiseNativeHandlerShim(aRunnable);
+
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JSObject*> handlerWrapper(cx);
+ // Note: PromiseNativeHandler is NOT wrappercached. So we can't use
+ // ToJSValue here, because it will try to do XPConnect wrapping on it, sadly.
+ if (NS_WARN_IF(!shim->WrapObject(cx, nullptr, &handlerWrapper))) {
+ // Again, no way to report errors.
+ jsapi.ClearException();
+ return;
+ }
+
+ JS::Rooted<JSObject*> resolveFunc(cx);
+ resolveFunc = CreateNativeHandlerFunction(cx, handlerWrapper,
+ NativeHandlerTask::Resolve);
+ if (NS_WARN_IF(!resolveFunc)) {
+ jsapi.ClearException();
+ return;
+ }
+
+ JS::Rooted<JSObject*> rejectFunc(cx);
+ rejectFunc = CreateNativeHandlerFunction(cx, handlerWrapper,
+ NativeHandlerTask::Reject);
+ if (NS_WARN_IF(!rejectFunc)) {
+ jsapi.ClearException();
+ return;
+ }
+
+ JS::Rooted<JSObject*> promiseObj(cx, PromiseObj());
+ if (NS_WARN_IF(
+ !JS::AddPromiseReactions(cx, promiseObj, resolveFunc, rejectFunc))) {
+ jsapi.ClearException();
+ return;
+ }
+}
+
+void Promise::HandleException(JSContext* aCx) {
+ JS::Rooted<JS::Value> exn(aCx);
+ if (JS_GetPendingException(aCx, &exn)) {
+ JS_ClearPendingException(aCx);
+ // Always reject even if this was called in *Resolve.
+ MaybeReject(aCx, exn);
+ }
+}
+
+// static
+already_AddRefed<Promise> Promise::RejectWithExceptionFromContext(
+ nsIGlobalObject* aGlobal, JSContext* aCx, ErrorResult& aError) {
+ JS::Rooted<JS::Value> exn(aCx);
+ if (!JS_GetPendingException(aCx, &exn)) {
+ // This is very important: if there is no pending exception here but we're
+ // ending up in this code, that means the callee threw an uncatchable
+ // exception. Just propagate that out as-is.
+ aError.ThrowUncatchableException();
+ return nullptr;
+ }
+
+ JSAutoRealm ar(aCx, aGlobal->GetGlobalJSObject());
+ if (!JS_WrapValue(aCx, &exn)) {
+ // We just give up.
+ aError.StealExceptionFromJSContext(aCx);
+ return nullptr;
+ }
+
+ JS_ClearPendingException(aCx);
+
+ IgnoredErrorResult error;
+ RefPtr<Promise> promise = Promise::Reject(aGlobal, aCx, exn, error);
+ if (!promise) {
+ // We just give up, let's store the exception in the ErrorResult.
+ aError.ThrowJSException(aCx, exn);
+ return nullptr;
+ }
+
+ return promise.forget();
+}
+
+// static
+already_AddRefed<Promise> Promise::CreateFromExisting(
+ nsIGlobalObject* aGlobal, JS::Handle<JSObject*> aPromiseObj,
+ PropagateUserInteraction aPropagateUserInteraction) {
+ MOZ_ASSERT(JS::GetCompartment(aGlobal->GetGlobalJSObjectPreserveColor()) ==
+ JS::GetCompartment(aPromiseObj));
+ RefPtr<Promise> p = new Promise(aGlobal);
+ p->mPromiseObj = aPromiseObj;
+ if (aPropagateUserInteraction == ePropagateUserInteraction &&
+ !p->MaybePropagateUserInputEventHandling()) {
+ return nullptr;
+ }
+ return p.forget();
+}
+
+void Promise::MaybeResolveWithUndefined() {
+ NS_ASSERT_OWNINGTHREAD(Promise);
+
+ MaybeResolve(JS::UndefinedHandleValue);
+}
+
+void Promise::MaybeReject(const RefPtr<MediaStreamError>& aArg) {
+ NS_ASSERT_OWNINGTHREAD(Promise);
+
+ MaybeSomething(aArg, &Promise::MaybeReject);
+}
+
+void Promise::MaybeRejectWithUndefined() {
+ NS_ASSERT_OWNINGTHREAD(Promise);
+
+ MaybeSomething(JS::UndefinedHandleValue, &Promise::MaybeReject);
+}
+
+void Promise::ReportRejectedPromise(JSContext* aCx,
+ JS::Handle<JSObject*> aPromise) {
+ MOZ_ASSERT(!js::IsWrapper(aPromise));
+
+ MOZ_ASSERT(JS::GetPromiseState(aPromise) == JS::PromiseState::Rejected);
+
+ bool isChrome = false;
+ uint64_t innerWindowID = 0;
+ nsGlobalWindowInner* winForDispatch = nullptr;
+ if (MOZ_LIKELY(NS_IsMainThread())) {
+ isChrome = nsContentUtils::ObjectPrincipal(aPromise)->IsSystemPrincipal();
+
+ if (nsGlobalWindowInner* win = xpc::WindowGlobalOrNull(aPromise)) {
+ winForDispatch = win;
+ innerWindowID = win->WindowID();
+ } else if (nsGlobalWindowInner* win = xpc::SandboxWindowOrNull(
+ JS::GetNonCCWObjectGlobal(aPromise), aCx)) {
+ // Don't dispatch rejections from the sandbox to the associated DOM
+ // window.
+ innerWindowID = win->WindowID();
+ }
+ } else if (const WorkerPrivate* wp = GetCurrentThreadWorkerPrivate()) {
+ isChrome = wp->UsesSystemPrincipal();
+ innerWindowID = wp->WindowID();
+ } else if (nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(aPromise)) {
+ if (nsCOMPtr<WorkletGlobalScope> workletGlobal =
+ do_QueryInterface(global)) {
+ WorkletImpl* impl = workletGlobal->Impl();
+ isChrome = impl->PrincipalInfo().type() ==
+ mozilla::ipc::PrincipalInfo::TSystemPrincipalInfo;
+ innerWindowID = impl->LoadInfo().InnerWindowID();
+ }
+ }
+
+ JS::Rooted<JS::Value> result(aCx, JS::GetPromiseResult(aPromise));
+ // resolutionSite can be null if async stacks are disabled.
+ JS::Rooted<JSObject*> resolutionSite(aCx,
+ JS::GetPromiseResolutionSite(aPromise));
+
+ // We're inspecting the rejection value only to report it to the console, and
+ // we do so without side-effects, so we can safely unwrap it without regard to
+ // the privileges of the Promise object that holds it. If we don't unwrap
+ // before trying to create the error report, we wind up reporting any
+ // cross-origin objects as "uncaught exception: Object".
+ RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport();
+ {
+ Maybe<JSAutoRealm> ar;
+ JS::Rooted<JS::Value> unwrapped(aCx, result);
+ if (unwrapped.isObject()) {
+ unwrapped.setObject(*js::UncheckedUnwrap(&unwrapped.toObject()));
+ ar.emplace(aCx, &unwrapped.toObject());
+ }
+
+ JS::ErrorReportBuilder report(aCx);
+ RefPtr<Exception> exn;
+ if (unwrapped.isObject() &&
+ (NS_SUCCEEDED(UNWRAP_OBJECT(DOMException, &unwrapped, exn)) ||
+ NS_SUCCEEDED(UNWRAP_OBJECT(Exception, &unwrapped, exn)))) {
+ xpcReport->Init(aCx, exn, isChrome, innerWindowID);
+ } else {
+ // Use the resolution site as the exception stack
+ JS::ExceptionStack exnStack(aCx, unwrapped, resolutionSite);
+ if (!report.init(aCx, exnStack, JS::ErrorReportBuilder::NoSideEffects)) {
+ JS_ClearPendingException(aCx);
+ return;
+ }
+
+ xpcReport->Init(report.report(), report.toStringResult().c_str(),
+ isChrome, innerWindowID);
+ }
+ }
+
+ // Used to initialize the similarly named nsISciptError attribute.
+ xpcReport->mIsPromiseRejection = true;
+
+ // Now post an event to do the real reporting async
+ RefPtr<AsyncErrorReporter> event = new AsyncErrorReporter(xpcReport);
+ if (winForDispatch) {
+ if (!winForDispatch->IsDying()) {
+ // Exceptions from a dying window will cause the window to leak.
+ event->SetException(aCx, result);
+ if (resolutionSite) {
+ event->SerializeStack(aCx, resolutionSite);
+ }
+ }
+ winForDispatch->Dispatch(mozilla::TaskCategory::Other, event.forget());
+ } else {
+ NS_DispatchToMainThread(event);
+ }
+}
+
+void Promise::MaybeResolveWithClone(JSContext* aCx,
+ JS::Handle<JS::Value> aValue) {
+ JS::Rooted<JSObject*> sourceScope(aCx, JS::CurrentGlobalOrNull(aCx));
+ AutoEntryScript aes(GetParentObject(), "Promise resolution");
+ JSContext* cx = aes.cx();
+ JS::Rooted<JS::Value> value(cx, aValue);
+
+ xpc::StackScopedCloneOptions options;
+ options.wrapReflectors = true;
+ if (!StackScopedClone(cx, options, sourceScope, &value)) {
+ HandleException(cx);
+ return;
+ }
+ MaybeResolve(aCx, value);
+}
+
+void Promise::MaybeRejectWithClone(JSContext* aCx,
+ JS::Handle<JS::Value> aValue) {
+ JS::Rooted<JSObject*> sourceScope(aCx, JS::CurrentGlobalOrNull(aCx));
+ AutoEntryScript aes(GetParentObject(), "Promise rejection");
+ JSContext* cx = aes.cx();
+ JS::Rooted<JS::Value> value(cx, aValue);
+
+ xpc::StackScopedCloneOptions options;
+ options.wrapReflectors = true;
+ if (!StackScopedClone(cx, options, sourceScope, &value)) {
+ HandleException(cx);
+ return;
+ }
+ MaybeReject(aCx, value);
+}
+
+// A WorkerRunnable to resolve/reject the Promise on the worker thread.
+// Calling thread MUST hold PromiseWorkerProxy's mutex before creating this.
+class PromiseWorkerProxyRunnable : public WorkerRunnable {
+ public:
+ PromiseWorkerProxyRunnable(PromiseWorkerProxy* aPromiseWorkerProxy,
+ PromiseWorkerProxy::RunCallbackFunc aFunc)
+ : WorkerRunnable(aPromiseWorkerProxy->GetWorkerPrivate(),
+ WorkerThreadUnchangedBusyCount),
+ mPromiseWorkerProxy(aPromiseWorkerProxy),
+ mFunc(aFunc) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPromiseWorkerProxy);
+ }
+
+ virtual bool WorkerRun(JSContext* aCx,
+ WorkerPrivate* aWorkerPrivate) override {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate == mWorkerPrivate);
+
+ MOZ_ASSERT(mPromiseWorkerProxy);
+ RefPtr<Promise> workerPromise = mPromiseWorkerProxy->WorkerPromise();
+
+ // Here we convert the buffer to a JS::Value.
+ JS::Rooted<JS::Value> value(aCx);
+ if (!mPromiseWorkerProxy->Read(aCx, &value)) {
+ JS_ClearPendingException(aCx);
+ return false;
+ }
+
+ (workerPromise->*mFunc)(aCx, value);
+
+ // Release the Promise because it has been resolved/rejected for sure.
+ mPromiseWorkerProxy->CleanUp();
+ return true;
+ }
+
+ protected:
+ ~PromiseWorkerProxyRunnable() = default;
+
+ private:
+ RefPtr<PromiseWorkerProxy> mPromiseWorkerProxy;
+
+ // Function pointer for calling Promise::{ResolveInternal,RejectInternal}.
+ PromiseWorkerProxy::RunCallbackFunc mFunc;
+};
+
+/* static */
+already_AddRefed<PromiseWorkerProxy> PromiseWorkerProxy::Create(
+ WorkerPrivate* aWorkerPrivate, Promise* aWorkerPromise,
+ const PromiseWorkerProxyStructuredCloneCallbacks* aCb) {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPromise);
+ MOZ_ASSERT_IF(aCb, !!aCb->Write && !!aCb->Read);
+
+ RefPtr<PromiseWorkerProxy> proxy =
+ new PromiseWorkerProxy(aWorkerPromise, aCb);
+
+ // Maintain a reference so that we have a valid object to clean up when
+ // removing the feature.
+ proxy.get()->AddRef();
+
+ // We do this to make sure the worker thread won't shut down before the
+ // promise is resolved/rejected on the worker thread.
+ RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create(
+ aWorkerPrivate, "PromiseWorkerProxy", [proxy]() { proxy->CleanUp(); });
+
+ if (NS_WARN_IF(!workerRef)) {
+ // Probably the worker is terminating. We cannot complete the operation
+ // and we have to release all the resources. CleanUp releases the extra
+ // ref, too
+ proxy->CleanUp();
+ return nullptr;
+ }
+
+ proxy->mWorkerRef = new ThreadSafeWorkerRef(workerRef);
+
+ return proxy.forget();
+}
+
+NS_IMPL_ISUPPORTS0(PromiseWorkerProxy)
+
+PromiseWorkerProxy::PromiseWorkerProxy(
+ Promise* aWorkerPromise,
+ const PromiseWorkerProxyStructuredCloneCallbacks* aCallbacks)
+ : mWorkerPromise(aWorkerPromise),
+ mCleanedUp(false),
+ mCallbacks(aCallbacks),
+ mCleanUpLock("cleanUpLock") {}
+
+PromiseWorkerProxy::~PromiseWorkerProxy() {
+ MOZ_ASSERT(mCleanedUp);
+ MOZ_ASSERT(!mWorkerPromise);
+ MOZ_ASSERT(!mWorkerRef);
+}
+
+WorkerPrivate* PromiseWorkerProxy::GetWorkerPrivate() const {
+#ifdef DEBUG
+ if (NS_IsMainThread()) {
+ mCleanUpLock.AssertCurrentThreadOwns();
+ }
+#endif
+ // Safe to check this without a lock since we assert lock ownership on the
+ // main thread above.
+ MOZ_ASSERT(!mCleanedUp);
+ MOZ_ASSERT(mWorkerRef);
+
+ return mWorkerRef->Private();
+}
+
+bool PromiseWorkerProxy::OnWritingThread() const {
+ return IsCurrentThreadRunningWorker();
+}
+
+Promise* PromiseWorkerProxy::WorkerPromise() const {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(mWorkerPromise);
+ return mWorkerPromise;
+}
+
+void PromiseWorkerProxy::RunCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ RunCallbackFunc aFunc) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MutexAutoLock lock(Lock());
+ // If the worker thread's been cancelled we don't need to resolve the Promise.
+ if (CleanedUp()) {
+ return;
+ }
+
+ // The |aValue| is written into the StructuredCloneHolderBase.
+ if (!Write(aCx, aValue)) {
+ JS_ClearPendingException(aCx);
+ MOZ_ASSERT(false,
+ "cannot serialize the value with the StructuredCloneAlgorithm!");
+ }
+
+ RefPtr<PromiseWorkerProxyRunnable> runnable =
+ new PromiseWorkerProxyRunnable(this, aFunc);
+
+ runnable->Dispatch();
+}
+
+void PromiseWorkerProxy::ResolvedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ RunCallback(aCx, aValue, &Promise::MaybeResolve);
+}
+
+void PromiseWorkerProxy::RejectedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ RunCallback(aCx, aValue, &Promise::MaybeReject);
+}
+
+void PromiseWorkerProxy::CleanUp() {
+ // Can't release Mutex while it is still locked, so scope the lock.
+ {
+ MutexAutoLock lock(Lock());
+
+ if (CleanedUp()) {
+ return;
+ }
+
+ // We can be called if we failed to get a WorkerRef
+ if (mWorkerRef) {
+ mWorkerRef->Private()->AssertIsOnWorkerThread();
+ }
+
+ // Release the Promise and remove the PromiseWorkerProxy from the holders of
+ // the worker thread since the Promise has been resolved/rejected or the
+ // worker thread has been cancelled.
+ mCleanedUp = true;
+ mWorkerPromise = nullptr;
+ mWorkerRef = nullptr;
+
+ // Clear the StructuredCloneHolderBase class.
+ Clear();
+ }
+ Release();
+}
+
+JSObject* PromiseWorkerProxy::CustomReadHandler(
+ JSContext* aCx, JSStructuredCloneReader* aReader,
+ const JS::CloneDataPolicy& aCloneDataPolicy, uint32_t aTag,
+ uint32_t aIndex) {
+ if (NS_WARN_IF(!mCallbacks)) {
+ return nullptr;
+ }
+
+ return mCallbacks->Read(aCx, aReader, this, aTag, aIndex);
+}
+
+bool PromiseWorkerProxy::CustomWriteHandler(JSContext* aCx,
+ JSStructuredCloneWriter* aWriter,
+ JS::Handle<JSObject*> aObj,
+ bool* aSameProcessScopeRequired) {
+ if (NS_WARN_IF(!mCallbacks)) {
+ return false;
+ }
+
+ return mCallbacks->Write(aCx, aWriter, this, aObj);
+}
+
+// Specializations of MaybeRejectBrokenly we actually support.
+template <>
+void Promise::MaybeRejectBrokenly(const RefPtr<DOMException>& aArg) {
+ MaybeSomething(aArg, &Promise::MaybeReject);
+}
+template <>
+void Promise::MaybeRejectBrokenly(const nsAString& aArg) {
+ MaybeSomething(aArg, &Promise::MaybeReject);
+}
+
+Promise::PromiseState Promise::State() const {
+ JS::Rooted<JSObject*> p(RootingCx(), PromiseObj());
+ const JS::PromiseState state = JS::GetPromiseState(p);
+
+ if (state == JS::PromiseState::Fulfilled) {
+ return PromiseState::Resolved;
+ }
+
+ if (state == JS::PromiseState::Rejected) {
+ return PromiseState::Rejected;
+ }
+
+ return PromiseState::Pending;
+}
+
+bool Promise::SetSettledPromiseIsHandled() {
+ if (!mPromiseObj) {
+ // Do nothing as it's a no-op promise
+ return false;
+ }
+ AutoAllowLegacyScriptExecution exemption;
+ AutoEntryScript aes(mGlobal, "Set settled promise handled");
+ JSContext* cx = aes.cx();
+ JS::Rooted<JSObject*> promiseObj(cx, mPromiseObj);
+ return JS::SetSettledPromiseIsHandled(cx, promiseObj);
+}
+
+bool Promise::SetAnyPromiseIsHandled() {
+ if (!mPromiseObj) {
+ // Do nothing as it's a no-op promise
+ return false;
+ }
+ AutoAllowLegacyScriptExecution exemption;
+ AutoEntryScript aes(mGlobal, "Set any promise handled");
+ JSContext* cx = aes.cx();
+ JS::Rooted<JSObject*> promiseObj(cx, mPromiseObj);
+ return JS::SetAnyPromiseIsHandled(cx, promiseObj);
+}
+
+/* static */
+already_AddRefed<Promise> Promise::CreateResolvedWithUndefined(
+ nsIGlobalObject* global, ErrorResult& aRv) {
+ RefPtr<Promise> returnPromise = Promise::Create(global, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ returnPromise->MaybeResolveWithUndefined();
+ return returnPromise.forget();
+}
+
+already_AddRefed<Promise> Promise::CreateRejected(
+ nsIGlobalObject* aGlobal, JS::Handle<JS::Value> aRejectionError,
+ ErrorResult& aRv) {
+ RefPtr<Promise> promise = Promise::Create(aGlobal, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ promise->MaybeReject(aRejectionError);
+ return promise.forget();
+}
+
+already_AddRefed<Promise> Promise::CreateRejectedWithTypeError(
+ nsIGlobalObject* aGlobal, const nsACString& aMessage, ErrorResult& aRv) {
+ RefPtr<Promise> returnPromise = Promise::Create(aGlobal, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ returnPromise->MaybeRejectWithTypeError(aMessage);
+ return returnPromise.forget();
+}
+
+already_AddRefed<Promise> Promise::CreateRejectedWithErrorResult(
+ nsIGlobalObject* aGlobal, ErrorResult& aRejectionError) {
+ RefPtr<Promise> returnPromise = Promise::Create(aGlobal, IgnoreErrors());
+ if (!returnPromise) {
+ return nullptr;
+ }
+ returnPromise->MaybeReject(std::move(aRejectionError));
+ return returnPromise.forget();
+}
+
+} // namespace mozilla::dom
+
+extern "C" {
+
+// These functions are used in the implementation of ffi bindings for
+// dom::Promise from Rust.
+
+void DomPromise_AddRef(mozilla::dom::Promise* aPromise) {
+ MOZ_ASSERT(aPromise);
+ aPromise->AddRef();
+}
+
+void DomPromise_Release(mozilla::dom::Promise* aPromise) {
+ MOZ_ASSERT(aPromise);
+ aPromise->Release();
+}
+
+#define DOM_PROMISE_FUNC_WITH_VARIANT(name, func) \
+ void name(mozilla::dom::Promise* aPromise, nsIVariant* aVariant) { \
+ MOZ_ASSERT(aPromise); \
+ MOZ_ASSERT(aVariant); \
+ mozilla::dom::AutoEntryScript aes(aPromise->GetGlobalObject(), \
+ "Promise resolution or rejection"); \
+ JSContext* cx = aes.cx(); \
+ \
+ JS::Rooted<JS::Value> val(cx); \
+ nsresult rv = NS_OK; \
+ if (!XPCVariant::VariantDataToJS(cx, aVariant, &rv, &val)) { \
+ aPromise->MaybeRejectWithTypeError( \
+ "Failed to convert nsIVariant to JS"); \
+ return; \
+ } \
+ aPromise->func(val); \
+ }
+
+DOM_PROMISE_FUNC_WITH_VARIANT(DomPromise_RejectWithVariant, MaybeReject)
+DOM_PROMISE_FUNC_WITH_VARIANT(DomPromise_ResolveWithVariant, MaybeResolve)
+
+#undef DOM_PROMISE_FUNC_WITH_VARIANT
+}
diff --git a/dom/promise/Promise.h b/dom/promise/Promise.h
new file mode 100644
index 0000000000..e10893fd73
--- /dev/null
+++ b/dom/promise/Promise.h
@@ -0,0 +1,453 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+#ifndef mozilla_dom_Promise_h
+#define mozilla_dom_Promise_h
+
+#include <functional>
+#include <type_traits>
+#include <utility>
+#include "ErrorList.h"
+#include "js/RootingAPI.h"
+#include "js/TypeDecls.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Result.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/dom/AutoEntryScript.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsError.h"
+#include "nsISupports.h"
+#include "nsString.h"
+
+class nsCycleCollectionTraversalCallback;
+class nsIGlobalObject;
+
+namespace JS {
+class Value;
+}
+
+namespace mozilla::dom {
+
+class AnyCallback;
+class MediaStreamError;
+class PromiseInit;
+class PromiseNativeHandler;
+class PromiseDebugging;
+
+class Promise : public SupportsWeakPtr {
+ friend class PromiseTask;
+ friend class PromiseWorkerProxy;
+ friend class PromiseWorkerProxyRunnable;
+
+ public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(Promise)
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(Promise)
+
+ enum PropagateUserInteraction {
+ eDontPropagateUserInteraction,
+ ePropagateUserInteraction
+ };
+
+ // Promise creation tries to create a JS reflector for the Promise, so is
+ // fallible. Furthermore, we don't want to do JS-wrapping on a 0-refcount
+ // object, so we addref before doing that and return the addrefed pointer
+ // here.
+ // Pass ePropagateUserInteraction for aPropagateUserInteraction if you want
+ // the promise resolve handler to be called as if we were handling user
+ // input events in case we are currently handling user input events.
+ static already_AddRefed<Promise> Create(
+ nsIGlobalObject* aGlobal, ErrorResult& aRv,
+ PropagateUserInteraction aPropagateUserInteraction =
+ eDontPropagateUserInteraction);
+
+ // Same as Promise::Create but never throws, but instead:
+ // 1. Causes crash on OOM (as nearly every other web APIs do)
+ // 2. Silently creates a no-op Promise if the JS context is shut down
+ // This can be useful for implementations that produce promises but do not
+ // care whether the current global is alive to consume them.
+ // Note that PromiseObj() can return a nullptr if created this way.
+ static already_AddRefed<Promise> CreateInfallible(
+ nsIGlobalObject* aGlobal,
+ PropagateUserInteraction aPropagateUserInteraction =
+ eDontPropagateUserInteraction);
+
+ // Reports a rejected Promise by sending an error report.
+ static void ReportRejectedPromise(JSContext* aCx,
+ JS::Handle<JSObject*> aPromise);
+
+ using MaybeFunc = void (Promise::*)(JSContext*, JS::Handle<JS::Value>);
+
+ // Helpers for using Promise from C++.
+ // Most DOM objects are handled already. To add a new type T, add a
+ // ToJSValue overload in ToJSValue.h.
+ // aArg is a const reference so we can pass rvalues like integer constants
+ template <typename T>
+ void MaybeResolve(T&& aArg) {
+ MaybeSomething(std::forward<T>(aArg), &Promise::MaybeResolve);
+ }
+
+ void MaybeResolveWithUndefined();
+
+ void MaybeReject(JS::Handle<JS::Value> aValue) {
+ MaybeSomething(aValue, &Promise::MaybeReject);
+ }
+
+ // This method is deprecated. Consumers should MaybeRejectWithDOMException if
+ // they are rejecting with a DOMException, or use one of the other
+ // MaybeReject* methods otherwise. If they have a random nsresult which may
+ // or may not correspond to a DOMException type, they should consider using an
+ // appropriate DOMException-type nsresult with an informative message and
+ // calling MaybeRejectWithDOMException.
+ inline void MaybeReject(nsresult aArg) {
+ MOZ_ASSERT(NS_FAILED(aArg));
+ MaybeSomething(aArg, &Promise::MaybeReject);
+ }
+
+ inline void MaybeReject(ErrorResult&& aArg) {
+ MOZ_ASSERT(aArg.Failed());
+ MaybeSomething(std::move(aArg), &Promise::MaybeReject);
+ // That should have consumed aArg.
+ MOZ_ASSERT(!aArg.Failed());
+ }
+
+ void MaybeReject(const RefPtr<MediaStreamError>& aArg);
+
+ void MaybeRejectWithUndefined();
+
+ void MaybeResolveWithClone(JSContext* aCx, JS::Handle<JS::Value> aValue);
+ void MaybeRejectWithClone(JSContext* aCx, JS::Handle<JS::Value> aValue);
+
+ // Facilities for rejecting with various spec-defined exception values.
+#define DOMEXCEPTION(name, err) \
+ inline void MaybeRejectWith##name(const nsACString& aMessage) { \
+ ErrorResult res; \
+ res.Throw##name(aMessage); \
+ MaybeReject(std::move(res)); \
+ } \
+ template <int N> \
+ void MaybeRejectWith##name(const char(&aMessage)[N]) { \
+ MaybeRejectWith##name(nsLiteralCString(aMessage)); \
+ }
+
+#include "mozilla/dom/DOMExceptionNames.h"
+
+#undef DOMEXCEPTION
+
+ template <ErrNum errorNumber, typename... Ts>
+ void MaybeRejectWithTypeError(Ts&&... aMessageArgs) {
+ ErrorResult res;
+ res.ThrowTypeError<errorNumber>(std::forward<Ts>(aMessageArgs)...);
+ MaybeReject(std::move(res));
+ }
+
+ inline void MaybeRejectWithTypeError(const nsACString& aMessage) {
+ ErrorResult res;
+ res.ThrowTypeError(aMessage);
+ MaybeReject(std::move(res));
+ }
+
+ template <int N>
+ void MaybeRejectWithTypeError(const char (&aMessage)[N]) {
+ MaybeRejectWithTypeError(nsLiteralCString(aMessage));
+ }
+
+ template <ErrNum errorNumber, typename... Ts>
+ void MaybeRejectWithRangeError(Ts&&... aMessageArgs) {
+ ErrorResult res;
+ res.ThrowRangeError<errorNumber>(std::forward<Ts>(aMessageArgs)...);
+ MaybeReject(std::move(res));
+ }
+
+ inline void MaybeRejectWithRangeError(const nsACString& aMessage) {
+ ErrorResult res;
+ res.ThrowRangeError(aMessage);
+ MaybeReject(std::move(res));
+ }
+
+ template <int N>
+ void MaybeRejectWithRangeError(const char (&aMessage)[N]) {
+ MaybeRejectWithRangeError(nsLiteralCString(aMessage));
+ }
+
+ // DO NOT USE MaybeRejectBrokenly with in new code. Promises should be
+ // rejected with Error instances.
+ // Note: MaybeRejectBrokenly is a template so we can use it with DOMException
+ // without instantiating the DOMException specialization of MaybeSomething in
+ // every translation unit that includes this header, because that would
+ // require use to include DOMException.h either here or in all those
+ // translation units.
+ template <typename T>
+ void MaybeRejectBrokenly(const T& aArg); // Not implemented by default; see
+ // specializations in the .cpp for
+ // the T values we support.
+
+ // Mark a settled promise as already handled so that rejections will not
+ // be reported as unhandled.
+ bool SetSettledPromiseIsHandled();
+
+ // Mark a promise as handled so that rejections will not be reported as
+ // unhandled. Consider using SetSettledPromiseIsHandled if this promise is
+ // expected to be settled.
+ [[nodiscard]] bool SetAnyPromiseIsHandled();
+
+ // WebIDL
+
+ nsIGlobalObject* GetParentObject() const { return GetGlobalObject(); }
+
+ // Do the equivalent of Promise.resolve in the compartment of aGlobal. The
+ // compartment of aCx is ignored. Errors are reported on the ErrorResult; if
+ // aRv comes back !Failed(), this function MUST return a non-null value.
+ // Pass ePropagateUserInteraction for aPropagateUserInteraction if you want
+ // the promise resolve handler to be called as if we were handling user
+ // input events in case we are currently handling user input events.
+ static already_AddRefed<Promise> Resolve(
+ nsIGlobalObject* aGlobal, JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv,
+ PropagateUserInteraction aPropagateUserInteraction =
+ eDontPropagateUserInteraction);
+
+ // Do the equivalent of Promise.reject in the compartment of aGlobal. The
+ // compartment of aCx is ignored. Errors are reported on the ErrorResult; if
+ // aRv comes back !Failed(), this function MUST return a non-null value.
+ static already_AddRefed<Promise> Reject(nsIGlobalObject* aGlobal,
+ JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv);
+
+ template <typename T>
+ static already_AddRefed<Promise> Reject(nsIGlobalObject* aGlobal, T&& aValue,
+ ErrorResult& aError) {
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(aGlobal)) {
+ aError.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JS::Value> val(cx);
+ if (!ToJSValue(cx, std::forward<T>(aValue), &val)) {
+ return Promise::RejectWithExceptionFromContext(aGlobal, cx, aError);
+ }
+
+ return Reject(aGlobal, cx, val, aError);
+ }
+
+ static already_AddRefed<Promise> RejectWithExceptionFromContext(
+ nsIGlobalObject* aGlobal, JSContext* aCx, ErrorResult& aError);
+
+ // Do the equivalent of Promise.all in the current compartment of aCx. Errors
+ // are reported on the ErrorResult; if aRv comes back !Failed(), this function
+ // MUST return a non-null value.
+ // Pass ePropagateUserInteraction for aPropagateUserInteraction if you want
+ // the promise resolve handler to be called as if we were handling user
+ // input events in case we are currently handling user input events.
+ static already_AddRefed<Promise> All(
+ JSContext* aCx, const nsTArray<RefPtr<Promise>>& aPromiseList,
+ ErrorResult& aRv,
+ PropagateUserInteraction aPropagateUserInteraction =
+ eDontPropagateUserInteraction);
+
+ void Then(JSContext* aCx,
+ // aCalleeGlobal may not be in the compartment of aCx, when called
+ // over Xrays.
+ JS::Handle<JSObject*> aCalleeGlobal, AnyCallback* aResolveCallback,
+ AnyCallback* aRejectCallback, JS::MutableHandle<JS::Value> aRetval,
+ ErrorResult& aRv);
+
+ template <typename Callback, typename... Args>
+ using IsHandlerCallback =
+ std::is_same<already_AddRefed<Promise>,
+ decltype(std::declval<Callback>()(
+ (JSContext*)(nullptr),
+ std::declval<JS::Handle<JS::Value>>(),
+ std::declval<ErrorResult&>(), std::declval<Args>()...))>;
+
+ template <typename Callback, typename... Args>
+ using ThenResult =
+ std::enable_if_t<IsHandlerCallback<Callback, Args...>::value,
+ Result<RefPtr<Promise>, nsresult>>;
+
+ // Similar to the JavaScript then() function. Accepts two lambda function
+ // arguments, which it attaches as native resolve/reject handlers, and
+ // returns a new promise which:
+ // 1. if the ErrorResult contains an error value, rejects with it.
+ // 2. else, resolves with a return value.
+ //
+ // Any additional arguments passed after the callback functions are stored and
+ // passed as additional arguments to the functions when it is called. These
+ // values will participate in cycle collection for the promise handler, and
+ // therefore may safely form reference cycles with the promise chain.
+ //
+ // Any strong references required by the callbacks should be passed in this
+ // manner, rather than using lambda capture, lambda captures do not support
+ // cycle collection, and can easily lead to leaks.
+ template <typename ResolveCallback, typename RejectCallback, typename... Args>
+ ThenResult<ResolveCallback, Args...> ThenCatchWithCycleCollectedArgs(
+ ResolveCallback&& aOnResolve, RejectCallback&& aOnReject,
+ Args&&... aArgs);
+
+ // Same as ThenCatchWithCycleCollectedArgs, except the rejection error will
+ // simply be propagated.
+ template <typename Callback, typename... Args>
+ ThenResult<Callback, Args...> ThenWithCycleCollectedArgs(
+ Callback&& aOnResolve, Args&&... aArgs);
+
+ // Same as ThenCatchWithCycleCollectedArgs, except the resolved value will
+ // simply be propagated.
+ template <typename Callback, typename... Args>
+ ThenResult<Callback, Args...> CatchWithCycleCollectedArgs(
+ Callback&& aOnReject, Args&&... aArgs);
+
+ // Same as Then[Catch]CycleCollectedArgs but the arguments are gathered into
+ // an `std::tuple` and there is an additional `std::tuple` for JS arguments
+ // after that.
+ template <typename ResolveCallback, typename RejectCallback,
+ typename ArgsTuple, typename JSArgsTuple>
+ Result<RefPtr<Promise>, nsresult> ThenCatchWithCycleCollectedArgsJS(
+ ResolveCallback&& aOnResolve, RejectCallback&& aOnReject,
+ ArgsTuple&& aArgs, JSArgsTuple&& aJSArgs);
+ template <typename Callback, typename ArgsTuple, typename JSArgsTuple>
+ Result<RefPtr<Promise>, nsresult> ThenWithCycleCollectedArgsJS(
+ Callback&& aOnResolve, ArgsTuple&& aArgs, JSArgsTuple&& aJSArgs);
+
+ Result<RefPtr<Promise>, nsresult> ThenWithoutCycleCollection(
+ const std::function<already_AddRefed<Promise>(
+ JSContext*, JS::Handle<JS::Value>, ErrorResult& aRv)>& aCallback);
+
+ // Similar to ThenCatchWithCycleCollectedArgs but doesn't care with return
+ // values of the callbacks and does not return a new promise.
+ template <typename ResolveCallback, typename RejectCallback, typename... Args>
+ void AddCallbacksWithCycleCollectedArgs(ResolveCallback&& aOnResolve,
+ RejectCallback&& aOnReject,
+ Args&&... aArgs);
+
+ // This can be null if this promise is made after the corresponding JSContext
+ // is dead.
+ JSObject* PromiseObj() const { return mPromiseObj; }
+
+ void AppendNativeHandler(PromiseNativeHandler* aRunnable);
+
+ nsIGlobalObject* GetGlobalObject() const { return mGlobal; }
+
+ // Create a dom::Promise from a given SpiderMonkey Promise object.
+ // aPromiseObj MUST be in the compartment of aGlobal's global JS object.
+ // Pass ePropagateUserInteraction for aPropagateUserInteraction if you want
+ // the promise resolve handler to be called as if we were handling user
+ // input events in case we are currently handling user input events.
+ static already_AddRefed<Promise> CreateFromExisting(
+ nsIGlobalObject* aGlobal, JS::Handle<JSObject*> aPromiseObj,
+ PropagateUserInteraction aPropagateUserInteraction =
+ eDontPropagateUserInteraction);
+
+ enum class PromiseState { Pending, Resolved, Rejected };
+
+ PromiseState State() const;
+
+ static already_AddRefed<Promise> CreateResolvedWithUndefined(
+ nsIGlobalObject* aGlobal, ErrorResult& aRv);
+
+ static already_AddRefed<Promise> CreateRejected(
+ nsIGlobalObject* aGlobal, JS::Handle<JS::Value> aRejectionError,
+ ErrorResult& aRv);
+
+ static already_AddRefed<Promise> CreateRejectedWithTypeError(
+ nsIGlobalObject* aGlobal, const nsACString& aMessage, ErrorResult& aRv);
+
+ // The rejection error will be consumed if the promise is successfully
+ // created, else the error will remain and rv.Failed() will keep being true.
+ // This intentionally is not an overload of CreateRejected to prevent
+ // accidental omission of the second argument. (See also bug 1762233 about
+ // removing its third argument.)
+ static already_AddRefed<Promise> CreateRejectedWithErrorResult(
+ nsIGlobalObject* aGlobal, ErrorResult& aRejectionError);
+
+ protected:
+ template <typename ResolveCallback, typename RejectCallback, typename... Args,
+ typename... JSArgs>
+ Result<RefPtr<Promise>, nsresult> ThenCatchWithCycleCollectedArgsJSImpl(
+ Maybe<ResolveCallback>&& aOnResolve, Maybe<RejectCallback>&& aOnReject,
+ std::tuple<Args...>&& aArgs, std::tuple<JSArgs...>&& aJSArgs);
+ template <typename ResolveCallback, typename RejectCallback, typename... Args>
+ ThenResult<ResolveCallback, Args...> ThenCatchWithCycleCollectedArgsImpl(
+ Maybe<ResolveCallback>&& aOnResolve, Maybe<RejectCallback>&& aOnReject,
+ Args&&... aArgs);
+
+ // Legacy method for throwing DOMExceptions. Only used by media code at this
+ // point, via DetailedPromise. Do NOT add new uses! When this is removed,
+ // remove the friend declaration in ErrorResult.h.
+ inline void MaybeRejectWithDOMException(nsresult rv,
+ const nsACString& aMessage) {
+ ErrorResult res;
+ res.ThrowDOMException(rv, aMessage);
+ MaybeReject(std::move(res));
+ }
+
+ struct PromiseCapability;
+
+ // Do NOT call this unless you're Promise::Create or
+ // Promise::CreateFromExisting. I wish we could enforce that from inside this
+ // class too, somehow.
+ explicit Promise(nsIGlobalObject* aGlobal);
+
+ virtual ~Promise();
+
+ // Do JS-wrapping after Promise creation.
+ // Pass ePropagateUserInteraction for aPropagateUserInteraction if you want
+ // the promise resolve handler to be called as if we were handling user
+ // input events in case we are currently handling user input events.
+ void CreateWrapper(ErrorResult& aRv,
+ PropagateUserInteraction aPropagateUserInteraction =
+ eDontPropagateUserInteraction);
+
+ private:
+ void MaybeResolve(JSContext* aCx, JS::Handle<JS::Value> aValue);
+ void MaybeReject(JSContext* aCx, JS::Handle<JS::Value> aValue);
+
+ template <typename T>
+ void MaybeSomething(T&& aArgument, MaybeFunc aFunc) {
+ MOZ_ASSERT(PromiseObj()); // It was preserved!
+
+ AutoAllowLegacyScriptExecution exemption;
+ AutoEntryScript aes(mGlobal, "Promise resolution or rejection");
+ JSContext* cx = aes.cx();
+
+ JS::Rooted<JS::Value> val(cx);
+ if (!ToJSValue(cx, std::forward<T>(aArgument), &val)) {
+ HandleException(cx);
+ return;
+ }
+
+ (this->*aFunc)(cx, val);
+ }
+
+ void HandleException(JSContext* aCx);
+
+ bool MaybePropagateUserInputEventHandling();
+
+ RefPtr<nsIGlobalObject> mGlobal;
+
+ JS::Heap<JSObject*> mPromiseObj;
+};
+
+} // namespace mozilla::dom
+
+extern "C" {
+// These functions are used in the implementation of ffi bindings for
+// dom::Promise from Rust in xpcom crate.
+void DomPromise_AddRef(mozilla::dom::Promise* aPromise);
+void DomPromise_Release(mozilla::dom::Promise* aPromise);
+void DomPromise_RejectWithVariant(mozilla::dom::Promise* aPromise,
+ nsIVariant* aVariant);
+void DomPromise_ResolveWithVariant(mozilla::dom::Promise* aPromise,
+ nsIVariant* aVariant);
+}
+
+#endif // mozilla_dom_Promise_h
diff --git a/dom/promise/PromiseDebugging.cpp b/dom/promise/PromiseDebugging.cpp
new file mode 100644
index 0000000000..b27661ef5e
--- /dev/null
+++ b/dom/promise/PromiseDebugging.cpp
@@ -0,0 +1,296 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+#include "js/Value.h"
+#include "nsThreadUtils.h"
+
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/ThreadLocal.h"
+#include "mozilla/TimeStamp.h"
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseBinding.h"
+#include "mozilla/dom/PromiseDebugging.h"
+#include "mozilla/dom/PromiseDebuggingBinding.h"
+
+namespace mozilla::dom {
+
+class FlushRejections : public DiscardableRunnable {
+ public:
+ FlushRejections() : DiscardableRunnable("dom::FlushRejections") {}
+
+ static void Init() {
+ if (!sDispatched.init()) {
+ MOZ_CRASH("Could not initialize FlushRejections::sDispatched");
+ }
+ sDispatched.set(false);
+ }
+
+ static void DispatchNeeded() {
+ if (sDispatched.get()) {
+ // An instance of `FlushRejections` has already been dispatched
+ // and not run yet. No need to dispatch another one.
+ return;
+ }
+ sDispatched.set(true);
+
+ // Dispatch the runnable to the current thread where
+ // the Promise was rejected, e.g. workers or worklets.
+ NS_DispatchToCurrentThread(new FlushRejections());
+ }
+
+ static void FlushSync() {
+ sDispatched.set(false);
+
+ // Call the callbacks if necessary.
+ // Note that these callbacks may in turn cause Promise to turn
+ // uncaught or consumed. Since `sDispatched` is `false`,
+ // `FlushRejections` will be called once again, on an ulterior
+ // tick.
+ PromiseDebugging::FlushUncaughtRejectionsInternal();
+ }
+
+ NS_IMETHOD Run() override {
+ FlushSync();
+ return NS_OK;
+ }
+
+ private:
+ // `true` if an instance of `FlushRejections` is currently dispatched
+ // and has not been executed yet.
+ static MOZ_THREAD_LOCAL(bool) sDispatched;
+};
+
+/* static */ MOZ_THREAD_LOCAL(bool) FlushRejections::sDispatched;
+
+/* static */
+void PromiseDebugging::GetState(GlobalObject& aGlobal,
+ JS::Handle<JSObject*> aPromise,
+ PromiseDebuggingStateHolder& aState,
+ ErrorResult& aRv) {
+ JSContext* cx = aGlobal.Context();
+ // CheckedUnwrapStatic is fine, since we're looking for promises only.
+ JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrapStatic(aPromise));
+ if (!obj || !JS::IsPromiseObject(obj)) {
+ aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>();
+ return;
+ }
+ switch (JS::GetPromiseState(obj)) {
+ case JS::PromiseState::Pending:
+ aState.mState = PromiseDebuggingState::Pending;
+ break;
+ case JS::PromiseState::Fulfilled:
+ aState.mState = PromiseDebuggingState::Fulfilled;
+ aState.mValue = JS::GetPromiseResult(obj);
+ break;
+ case JS::PromiseState::Rejected:
+ aState.mState = PromiseDebuggingState::Rejected;
+ aState.mReason = JS::GetPromiseResult(obj);
+ break;
+ }
+}
+
+/* static */
+void PromiseDebugging::GetPromiseID(GlobalObject& aGlobal,
+ JS::Handle<JSObject*> aPromise,
+ nsString& aID, ErrorResult& aRv) {
+ JSContext* cx = aGlobal.Context();
+ // CheckedUnwrapStatic is fine, since we're looking for promises only.
+ JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrapStatic(aPromise));
+ if (!obj || !JS::IsPromiseObject(obj)) {
+ aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>();
+ return;
+ }
+ uint64_t promiseID = JS::GetPromiseID(obj);
+ aID = sIDPrefix;
+ aID.AppendInt(promiseID);
+}
+
+/* static */
+void PromiseDebugging::GetAllocationStack(GlobalObject& aGlobal,
+ JS::Handle<JSObject*> aPromise,
+ JS::MutableHandle<JSObject*> aStack,
+ ErrorResult& aRv) {
+ JSContext* cx = aGlobal.Context();
+ // CheckedUnwrapStatic is fine, since we're looking for promises only.
+ JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrapStatic(aPromise));
+ if (!obj || !JS::IsPromiseObject(obj)) {
+ aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>();
+ return;
+ }
+ aStack.set(JS::GetPromiseAllocationSite(obj));
+}
+
+/* static */
+void PromiseDebugging::GetRejectionStack(GlobalObject& aGlobal,
+ JS::Handle<JSObject*> aPromise,
+ JS::MutableHandle<JSObject*> aStack,
+ ErrorResult& aRv) {
+ JSContext* cx = aGlobal.Context();
+ // CheckedUnwrapStatic is fine, since we're looking for promises only.
+ JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrapStatic(aPromise));
+ if (!obj || !JS::IsPromiseObject(obj)) {
+ aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>();
+ return;
+ }
+ aStack.set(JS::GetPromiseResolutionSite(obj));
+}
+
+/* static */
+void PromiseDebugging::GetFullfillmentStack(GlobalObject& aGlobal,
+ JS::Handle<JSObject*> aPromise,
+ JS::MutableHandle<JSObject*> aStack,
+ ErrorResult& aRv) {
+ JSContext* cx = aGlobal.Context();
+ // CheckedUnwrapStatic is fine, since we're looking for promises only.
+ JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrapStatic(aPromise));
+ if (!obj || !JS::IsPromiseObject(obj)) {
+ aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>();
+ return;
+ }
+ aStack.set(JS::GetPromiseResolutionSite(obj));
+}
+
+/*static */
+nsString PromiseDebugging::sIDPrefix;
+
+/* static */
+void PromiseDebugging::Init() {
+ FlushRejections::Init();
+
+ // Generate a prefix for identifiers: "PromiseDebugging.$processid."
+ sIDPrefix = u"PromiseDebugging."_ns;
+ if (XRE_IsContentProcess()) {
+ sIDPrefix.AppendInt(ContentChild::GetSingleton()->GetID());
+ sIDPrefix.Append('.');
+ } else {
+ sIDPrefix.AppendLiteral("0.");
+ }
+}
+
+/* static */
+void PromiseDebugging::Shutdown() { sIDPrefix.SetIsVoid(true); }
+
+/* static */
+void PromiseDebugging::FlushUncaughtRejections() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ FlushRejections::FlushSync();
+}
+
+/* static */
+void PromiseDebugging::AddUncaughtRejectionObserver(
+ GlobalObject&, UncaughtRejectionObserver& aObserver) {
+ CycleCollectedJSContext* storage = CycleCollectedJSContext::Get();
+ nsTArray<nsCOMPtr<nsISupports>>& observers =
+ storage->mUncaughtRejectionObservers;
+ observers.AppendElement(&aObserver);
+}
+
+/* static */
+bool PromiseDebugging::RemoveUncaughtRejectionObserver(
+ GlobalObject&, UncaughtRejectionObserver& aObserver) {
+ CycleCollectedJSContext* storage = CycleCollectedJSContext::Get();
+ nsTArray<nsCOMPtr<nsISupports>>& observers =
+ storage->mUncaughtRejectionObservers;
+ for (size_t i = 0; i < observers.Length(); ++i) {
+ UncaughtRejectionObserver* observer =
+ static_cast<UncaughtRejectionObserver*>(observers[i].get());
+ if (*observer == aObserver) {
+ observers.RemoveElementAt(i);
+ return true;
+ }
+ }
+ return false;
+}
+
+/* static */
+void PromiseDebugging::AddUncaughtRejection(JS::Handle<JSObject*> aPromise) {
+ // This might OOM, but won't set a pending exception, so we'll just ignore it.
+ if (CycleCollectedJSContext::Get()->mUncaughtRejections.append(aPromise)) {
+ FlushRejections::DispatchNeeded();
+ }
+}
+
+/* void */
+void PromiseDebugging::AddConsumedRejection(JS::Handle<JSObject*> aPromise) {
+ // If the promise is in our list of uncaught rejections, we haven't yet
+ // reported it as unhandled. In that case, just remove it from the list
+ // and don't add it to the list of consumed rejections.
+ auto& uncaughtRejections =
+ CycleCollectedJSContext::Get()->mUncaughtRejections;
+ for (size_t i = 0; i < uncaughtRejections.length(); i++) {
+ if (uncaughtRejections[i] == aPromise) {
+ // To avoid large amounts of memmoves, we don't shrink the vector here.
+ // Instead, we filter out nullptrs when iterating over the vector later.
+ uncaughtRejections[i].set(nullptr);
+ return;
+ }
+ }
+ // This might OOM, but won't set a pending exception, so we'll just ignore it.
+ if (CycleCollectedJSContext::Get()->mConsumedRejections.append(aPromise)) {
+ FlushRejections::DispatchNeeded();
+ }
+}
+
+/* static */
+void PromiseDebugging::FlushUncaughtRejectionsInternal() {
+ CycleCollectedJSContext* storage = CycleCollectedJSContext::Get();
+
+ auto& uncaught = storage->mUncaughtRejections;
+ auto& consumed = storage->mConsumedRejections;
+
+ AutoJSAPI jsapi;
+ jsapi.Init();
+ JSContext* cx = jsapi.cx();
+
+ // Notify observers of uncaught Promise.
+ auto& observers = storage->mUncaughtRejectionObservers;
+
+ for (size_t i = 0; i < uncaught.length(); i++) {
+ JS::Rooted<JSObject*> promise(cx, uncaught[i]);
+ // Filter out nullptrs which might've been added by
+ // PromiseDebugging::AddConsumedRejection.
+ if (!promise) {
+ continue;
+ }
+
+ bool suppressReporting = false;
+ for (size_t j = 0; j < observers.Length(); ++j) {
+ RefPtr<UncaughtRejectionObserver> obs =
+ static_cast<UncaughtRejectionObserver*>(observers[j].get());
+
+ if (obs->OnLeftUncaught(promise, IgnoreErrors())) {
+ suppressReporting = true;
+ }
+ }
+
+ if (!suppressReporting) {
+ JSAutoRealm ar(cx, promise);
+ Promise::ReportRejectedPromise(cx, promise);
+ }
+ }
+ storage->mUncaughtRejections.clear();
+
+ // Notify observers of consumed Promise.
+
+ for (size_t i = 0; i < consumed.length(); i++) {
+ JS::Rooted<JSObject*> promise(cx, consumed[i]);
+
+ for (size_t j = 0; j < observers.Length(); ++j) {
+ RefPtr<UncaughtRejectionObserver> obs =
+ static_cast<UncaughtRejectionObserver*>(observers[j].get());
+
+ obs->OnConsumed(promise, IgnoreErrors());
+ }
+ }
+ storage->mConsumedRejections.clear();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/promise/PromiseDebugging.h b/dom/promise/PromiseDebugging.h
new file mode 100644
index 0000000000..ed14e2215b
--- /dev/null
+++ b/dom/promise/PromiseDebugging.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+#ifndef mozilla_dom_PromiseDebugging_h
+#define mozilla_dom_PromiseDebugging_h
+
+#include "js/TypeDecls.h"
+#include "nsTArray.h"
+#include "mozilla/RefPtr.h"
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+class Promise;
+struct PromiseDebuggingStateHolder;
+class GlobalObject;
+class UncaughtRejectionObserver;
+class FlushRejections;
+class WorkerPrivate;
+
+void TriggerFlushRejections();
+
+class PromiseDebugging {
+ public:
+ static void Init();
+ static void Shutdown();
+
+ static void GetState(GlobalObject&, JS::Handle<JSObject*> aPromise,
+ PromiseDebuggingStateHolder& aState, ErrorResult& aRv);
+
+ static void GetPromiseID(GlobalObject&, JS::Handle<JSObject*>, nsString&,
+ ErrorResult&);
+
+ static void GetAllocationStack(GlobalObject&, JS::Handle<JSObject*> aPromise,
+ JS::MutableHandle<JSObject*> aStack,
+ ErrorResult& aRv);
+ static void GetRejectionStack(GlobalObject&, JS::Handle<JSObject*> aPromise,
+ JS::MutableHandle<JSObject*> aStack,
+ ErrorResult& aRv);
+ static void GetFullfillmentStack(GlobalObject&,
+ JS::Handle<JSObject*> aPromise,
+ JS::MutableHandle<JSObject*> aStack,
+ ErrorResult& aRv);
+
+ // Mechanism for watching uncaught instances of Promise.
+ static void AddUncaughtRejectionObserver(
+ GlobalObject&, UncaughtRejectionObserver& aObserver);
+ static bool RemoveUncaughtRejectionObserver(
+ GlobalObject&, UncaughtRejectionObserver& aObserver);
+
+ // Mark a Promise as having been left uncaught at script completion.
+ static void AddUncaughtRejection(JS::Handle<JSObject*>);
+ // Mark a Promise previously added with `AddUncaughtRejection` as
+ // eventually consumed.
+ static void AddConsumedRejection(JS::Handle<JSObject*>);
+ // Propagate the informations from AddUncaughtRejection
+ // and AddConsumedRejection to observers.
+ static void FlushUncaughtRejections();
+
+ protected:
+ static void FlushUncaughtRejectionsInternal();
+ friend class FlushRejections;
+ friend class mozilla::dom::WorkerPrivate;
+
+ private:
+ // Identity of the process.
+ // This property is:
+ // - set during initialization of the layout module,
+ // prior to any Worker using it;
+ // - read by both the main thread and the Workers;
+ // - unset during shutdown of the layout module,
+ // after any Worker has been shutdown.
+ static nsString sIDPrefix;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PromiseDebugging_h
diff --git a/dom/promise/PromiseNativeHandler.cpp b/dom/promise/PromiseNativeHandler.cpp
new file mode 100644
index 0000000000..88f7edef3a
--- /dev/null
+++ b/dom/promise/PromiseNativeHandler.cpp
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+#include "PromiseNativeHandler.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/DOMExceptionBinding.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_ISUPPORTS0(DomPromiseListener)
+
+DomPromiseListener::DomPromiseListener(CallbackTypeResolved&& aResolve,
+ CallbackTypeRejected&& aReject)
+ : mResolve(std::move(aResolve)), mReject(std::move(aReject)) {}
+
+DomPromiseListener::~DomPromiseListener() {
+ if (mReject) {
+ mReject(NS_BINDING_ABORTED);
+ }
+}
+
+void DomPromiseListener::ResolvedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ if (mResolve) {
+ mResolve(aCx, aValue);
+ }
+ // Let's clear the lambdas in case we have a cycle to ourselves.
+ Clear();
+}
+
+void DomPromiseListener::RejectedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ if (mReject) {
+ nsresult errorCode = NS_ERROR_DOM_NOT_NUMBER_ERR;
+ if (aValue.isInt32()) {
+ errorCode = nsresult(aValue.toInt32());
+ } else if (aValue.isObject()) {
+ RefPtr<::mozilla::dom::DOMException> domException;
+ UNWRAP_OBJECT(DOMException, aValue, domException);
+ if (domException) {
+ errorCode = domException->GetResult();
+ }
+ }
+ mReject(errorCode);
+ }
+ // Let's clear the lambdas in case we have a cycle to ourselves.
+ Clear();
+}
+
+void DomPromiseListener::Clear() {
+ mResolve = nullptr;
+ mReject = nullptr;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/promise/PromiseNativeHandler.h b/dom/promise/PromiseNativeHandler.h
new file mode 100644
index 0000000000..0867e7f75d
--- /dev/null
+++ b/dom/promise/PromiseNativeHandler.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+#ifndef mozilla_dom_PromiseNativeHandler_h
+#define mozilla_dom_PromiseNativeHandler_h
+
+#include <functional>
+#include "js/TypeDecls.h"
+#include "js/Value.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Maybe.h"
+#include "nsISupports.h"
+
+namespace mozilla::dom {
+
+/*
+ * PromiseNativeHandler allows C++ to react to a Promise being
+ * rejected/resolved. A PromiseNativeHandler can be appended to a Promise using
+ * Promise::AppendNativeHandler().
+ */
+class PromiseNativeHandler : public nsISupports {
+ protected:
+ virtual ~PromiseNativeHandler() = default;
+
+ public:
+ MOZ_CAN_RUN_SCRIPT
+ virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) = 0;
+
+ MOZ_CAN_RUN_SCRIPT
+ virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) = 0;
+};
+
+// This class is used to set C++ callbacks once a dom Promise a resolved or
+// rejected.
+class DomPromiseListener final : public PromiseNativeHandler {
+ NS_DECL_ISUPPORTS
+
+ public:
+ using CallbackTypeResolved =
+ std::function<void(JSContext*, JS::Handle<JS::Value>)>;
+ using CallbackTypeRejected = std::function<void(nsresult)>;
+
+ DomPromiseListener(CallbackTypeResolved&& aResolve,
+ CallbackTypeRejected&& aReject);
+
+ void Clear();
+
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ private:
+ ~DomPromiseListener();
+ CallbackTypeResolved mResolve;
+ CallbackTypeRejected mReject;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_PromiseNativeHandler_h
diff --git a/dom/promise/PromiseWorkerProxy.h b/dom/promise/PromiseWorkerProxy.h
new file mode 100644
index 0000000000..92ebd5eaf2
--- /dev/null
+++ b/dom/promise/PromiseWorkerProxy.h
@@ -0,0 +1,219 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+#ifndef mozilla_dom_PromiseWorkerProxy_h
+#define mozilla_dom_PromiseWorkerProxy_h
+
+#include <cstdint>
+#include "js/TypeDecls.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/StructuredCloneHolder.h"
+#include "nsISupports.h"
+
+struct JSStructuredCloneReader;
+struct JSStructuredCloneWriter;
+
+namespace JS {
+class CloneDataPolicy;
+} // namespace JS
+
+namespace mozilla::dom {
+
+class ThreadSafeWorkerRef;
+class WorkerPrivate;
+
+// A proxy to (eventually) mirror a resolved/rejected Promise's result from the
+// main thread to a Promise on the worker thread.
+//
+// How to use:
+//
+// 1. Create a Promise on the worker thread and return it to the content
+// script:
+//
+// RefPtr<Promise> promise =
+// Promise::Create(workerPrivate->GlobalScope(), aRv);
+// if (aRv.Failed()) {
+// return nullptr;
+// }
+//
+// 2. Create a PromiseWorkerProxy wrapping the Promise. If this fails, the
+// worker is shutting down and you should fail the original call. This is
+// only likely to happen in (Gecko-specific) worker onclose handlers.
+//
+// RefPtr<PromiseWorkerProxy> proxy =
+// PromiseWorkerProxy::Create(workerPrivate, promise);
+// if (!proxy) {
+// // You may also reject the Promise with an AbortError or similar.
+// return nullptr;
+// }
+//
+// 3. Dispatch a runnable to the main thread, with a reference to the proxy to
+// perform the main thread operation. PromiseWorkerProxy is thread-safe
+// refcounted.
+//
+// 4. Return the worker thread promise to the JS caller:
+//
+// return promise.forget();
+//
+// 5. In your main thread runnable Run(), obtain a Promise on
+// the main thread and call its AppendNativeHandler(PromiseNativeHandler*)
+// to bind the PromiseWorkerProxy created at #2.
+//
+// 4. Then the Promise results returned by ResolvedCallback/RejectedCallback
+// will be dispatched as a WorkerRunnable to the worker thread to
+// resolve/reject the Promise created at #1.
+//
+// PromiseWorkerProxy can also be used in situations where there is no main
+// thread Promise, or where special handling is required on the worker thread
+// for promise resolution. Create a PromiseWorkerProxy as in steps 1 to 3
+// above. When the main thread is ready to resolve the worker thread promise:
+//
+// 1. Acquire the mutex before attempting to access the worker private.
+//
+// AssertIsOnMainThread();
+// MutexAutoLock lock(proxy->Lock());
+// if (proxy->CleanedUp()) {
+// // Worker has already shut down, can't access worker private.
+// return;
+// }
+//
+// 2. Dispatch a runnable to the worker. Use GetWorkerPrivate() to acquire the
+// worker.
+//
+// RefPtr<FinishTaskWorkerRunnable> runnable =
+// new FinishTaskWorkerRunnable(proxy->GetWorkerPrivate(), proxy,
+// result);
+// if (!r->Dispatch()) {
+// // Worker is alive but not Running any more, so the Promise can't
+// // be resolved, give up. The proxy will get Release()d at some
+// // point.
+//
+// // Usually do nothing, but you may want to log the fact.
+// }
+//
+// 3. In the WorkerRunnable's WorkerRun() use WorkerPromise() to access the
+// Promise and resolve/reject it. Then call CleanUp().
+//
+// bool
+// WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
+// {
+// aWorkerPrivate->AssertIsOnWorkerThread();
+// RefPtr<Promise> promise = mProxy->WorkerPromise();
+// promise->MaybeResolve(mResult);
+// mProxy->CleanUp();
+// }
+//
+// Note: If a PromiseWorkerProxy is not cleaned up by a WorkerRunnable - this
+// can happen if the main thread Promise is never fulfilled - it will
+// stay alive till the worker reaches a Canceling state, even if all external
+// references to it are dropped.
+
+class PromiseWorkerProxy : public PromiseNativeHandler,
+ public StructuredCloneHolderBase,
+ public SingleWriterLockOwner {
+ friend class PromiseWorkerProxyRunnable;
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ public:
+ typedef JSObject* (*ReadCallbackOp)(JSContext* aCx,
+ JSStructuredCloneReader* aReader,
+ const PromiseWorkerProxy* aProxy,
+ uint32_t aTag, uint32_t aData);
+ typedef bool (*WriteCallbackOp)(JSContext* aCx,
+ JSStructuredCloneWriter* aWorker,
+ PromiseWorkerProxy* aProxy,
+ JS::Handle<JSObject*> aObj);
+
+ bool OnWritingThread() const override;
+
+ struct PromiseWorkerProxyStructuredCloneCallbacks {
+ ReadCallbackOp Read;
+ WriteCallbackOp Write;
+ };
+
+ static already_AddRefed<PromiseWorkerProxy> Create(
+ WorkerPrivate* aWorkerPrivate, Promise* aWorkerPromise,
+ const PromiseWorkerProxyStructuredCloneCallbacks* aCallbacks = nullptr);
+
+ // Main thread callers must hold Lock() and check CleanUp() before calling
+ // this. Worker thread callers, this will assert that the proxy has not been
+ // cleaned up.
+ WorkerPrivate* GetWorkerPrivate() const MOZ_NO_THREAD_SAFETY_ANALYSIS;
+
+ // This should only be used within WorkerRunnable::WorkerRun() running on the
+ // worker thread! Do not call this after calling CleanUp().
+ Promise* WorkerPromise() const;
+
+ // Worker thread only. Calling this invalidates several assumptions, so be
+ // sure this is the last thing you do.
+ // 1. WorkerPrivate() will no longer return a valid worker.
+ // 2. WorkerPromise() will crash!
+ void CleanUp();
+
+ Mutex& Lock() MOZ_RETURN_CAPABILITY(mCleanUpLock) { return mCleanUpLock; }
+
+ bool CleanedUp() const MOZ_REQUIRES(mCleanUpLock) {
+ mCleanUpLock.AssertCurrentThreadOwns();
+ return mCleanedUp;
+ }
+
+ // StructuredCloneHolderBase
+
+ JSObject* CustomReadHandler(JSContext* aCx, JSStructuredCloneReader* aReader,
+ const JS::CloneDataPolicy& aCloneDataPolicy,
+ uint32_t aTag, uint32_t aIndex) override;
+
+ bool CustomWriteHandler(JSContext* aCx, JSStructuredCloneWriter* aWriter,
+ JS::Handle<JSObject*> aObj,
+ bool* aSameProcessScopeRequired) override;
+
+ protected:
+ virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ private:
+ explicit PromiseWorkerProxy(
+ Promise* aWorkerPromise,
+ const PromiseWorkerProxyStructuredCloneCallbacks* aCallbacks = nullptr);
+
+ virtual ~PromiseWorkerProxy();
+
+ // Function pointer for calling Promise::{ResolveInternal,RejectInternal}.
+ typedef void (Promise::*RunCallbackFunc)(JSContext*, JS::Handle<JS::Value>);
+
+ void RunCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ RunCallbackFunc aFunc);
+
+ // Any thread with appropriate checks.
+ RefPtr<ThreadSafeWorkerRef> mWorkerRef;
+
+ // Worker thread only.
+ RefPtr<Promise> mWorkerPromise;
+
+ // Modified on the worker thread.
+ // It is ok to *read* this without a lock on the worker.
+ // Main thread must always acquire a lock.
+ bool mCleanedUp MOZ_GUARDED_BY(
+ mCleanUpLock); // To specify if the cleanUp() has been done.
+
+ const PromiseWorkerProxyStructuredCloneCallbacks* mCallbacks;
+
+ // Ensure the worker and the main thread won't race to access |mCleanedUp|.
+ // Should be a MutexSingleWriter, but that causes a lot of issues when you
+ // expose the lock via Lock().
+ Mutex mCleanUpLock;
+};
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_PromiseWorkerProxy_h
diff --git a/dom/promise/gtest/NativeThenHandler.cpp b/dom/promise/gtest/NativeThenHandler.cpp
new file mode 100644
index 0000000000..13ed6d2c11
--- /dev/null
+++ b/dom/promise/gtest/NativeThenHandler.cpp
@@ -0,0 +1,160 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+#include "gtest/gtest.h"
+
+#include "js/TypeDecls.h"
+#include "js/Value.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/Promise-inl.h"
+#include "xpcpublic.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+struct TraceCounts {
+ int32_t mValue = 0;
+ int32_t mId = 0;
+ int32_t mObject = 0;
+ int32_t mWrapperCache = 0;
+ int32_t mTenuredHeapObject = 0;
+ int32_t mString = 0;
+ int32_t mScript = 0;
+ int32_t mFunction = 0;
+};
+
+struct DummyCallbacks final : public TraceCallbacks {
+ void Trace(JS::Heap<JS::Value>*, const char*, void* aClosure) const override {
+ static_cast<TraceCounts*>(aClosure)->mValue++;
+ }
+
+ void Trace(JS::Heap<jsid>*, const char*, void* aClosure) const override {
+ static_cast<TraceCounts*>(aClosure)->mId++;
+ }
+
+ void Trace(JS::Heap<JSObject*>*, const char*, void* aClosure) const override {
+ static_cast<TraceCounts*>(aClosure)->mObject++;
+ }
+
+ void Trace(nsWrapperCache*, const char* aName,
+ void* aClosure) const override {
+ static_cast<TraceCounts*>(aClosure)->mWrapperCache++;
+ }
+
+ void Trace(JS::TenuredHeap<JSObject*>*, const char*,
+ void* aClosure) const override {
+ static_cast<TraceCounts*>(aClosure)->mTenuredHeapObject++;
+ }
+
+ void Trace(JS::Heap<JSString*>*, const char*, void* aClosure) const override {
+ static_cast<TraceCounts*>(aClosure)->mString++;
+ }
+
+ void Trace(JS::Heap<JSScript*>*, const char*, void* aClosure) const override {
+ static_cast<TraceCounts*>(aClosure)->mScript++;
+ }
+
+ void Trace(JS::Heap<JSFunction*>*, const char*,
+ void* aClosure) const override {
+ static_cast<TraceCounts*>(aClosure)->mFunction++;
+ }
+};
+
+TEST(NativeThenHandler, TraceValue)
+{
+ auto onResolve = [](JSContext*, JS::Handle<JS::Value>, ErrorResult&,
+ JS::Handle<JS::Value>) -> already_AddRefed<Promise> {
+ return nullptr;
+ };
+ auto onReject = [](JSContext*, JS::Handle<JS::Value>, ErrorResult&,
+ JS::Handle<JS::Value>) -> already_AddRefed<Promise> {
+ return nullptr;
+ };
+
+ // Explicit type for backward compatibility with clang<7 / gcc<8
+ using HandlerType =
+ NativeThenHandler<decltype(onResolve), decltype(onReject), std::tuple<>,
+ std::tuple<JS::HandleValue>>;
+ RefPtr<HandlerType> handler = new HandlerType(
+ nullptr, Some(onResolve), Some(onReject), std::make_tuple(),
+ std::make_tuple(JS::UndefinedHandleValue));
+
+ TraceCounts counts;
+ NS_CYCLE_COLLECTION_PARTICIPANT(HandlerType)
+ ->Trace(handler.get(), DummyCallbacks(), &counts);
+
+ EXPECT_EQ(counts.mValue, 1);
+}
+
+TEST(NativeThenHandler, TraceObject)
+{
+ auto onResolve = [](JSContext*, JS::Handle<JS::Value>, ErrorResult&,
+ JS::Handle<JSObject*>) -> already_AddRefed<Promise> {
+ return nullptr;
+ };
+ auto onReject = [](JSContext*, JS::Handle<JS::Value>, ErrorResult&,
+ JS::Handle<JSObject*>) -> already_AddRefed<Promise> {
+ return nullptr;
+ };
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
+
+ // Explicit type for backward compatibility with clang<7 / gcc<8
+ using HandlerType =
+ NativeThenHandler<decltype(onResolve), decltype(onReject), std::tuple<>,
+ std::tuple<JS::HandleObject>>;
+ RefPtr<HandlerType> handler = new HandlerType(
+ nullptr, Some(onResolve), Some(onReject), std::make_tuple(),
+ std::make_tuple(JS::HandleObject(obj)));
+
+ TraceCounts counts;
+ NS_CYCLE_COLLECTION_PARTICIPANT(HandlerType)
+ ->Trace(handler.get(), DummyCallbacks(), &counts);
+
+ EXPECT_EQ(counts.mObject, 1);
+}
+
+TEST(NativeThenHandler, TraceMixed)
+{
+ auto onResolve = [](JSContext*, JS::Handle<JS::Value>, ErrorResult&,
+ nsIGlobalObject*, Promise*, JS::Handle<JS::Value>,
+ JS::Handle<JSObject*>) -> already_AddRefed<Promise> {
+ return nullptr;
+ };
+ auto onReject = [](JSContext*, JS::Handle<JS::Value>, ErrorResult&,
+ nsIGlobalObject*, Promise*, JS::Handle<JS::Value>,
+ JS::Handle<JSObject*>) -> already_AddRefed<Promise> {
+ return nullptr;
+ };
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(cx);
+ JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
+
+ RefPtr<Promise> promise = Promise::Create(global, IgnoreErrors());
+
+ // Explicit type for backward compatibility with clang<7 / gcc<8
+ using HandlerType =
+ NativeThenHandler<decltype(onResolve), decltype(onReject),
+ std::tuple<nsCOMPtr<nsIGlobalObject>, RefPtr<Promise>>,
+ std::tuple<JS::HandleValue, JS::HandleObject>>;
+ RefPtr<HandlerType> handler = new HandlerType(
+ nullptr, Some(onResolve), Some(onReject),
+ std::make_tuple(global, promise),
+ std::make_tuple(JS::UndefinedHandleValue, JS::HandleObject(obj)));
+
+ TraceCounts counts;
+ NS_CYCLE_COLLECTION_PARTICIPANT(HandlerType)
+ ->Trace(handler.get(), DummyCallbacks(), &counts);
+
+ EXPECT_EQ(counts.mValue, 1);
+ EXPECT_EQ(counts.mObject, 1);
+}
diff --git a/dom/promise/gtest/ThenWithCycleCollectedArgsJS.cpp b/dom/promise/gtest/ThenWithCycleCollectedArgsJS.cpp
new file mode 100644
index 0000000000..31b9fde3ca
--- /dev/null
+++ b/dom/promise/gtest/ThenWithCycleCollectedArgsJS.cpp
@@ -0,0 +1,158 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+#include "gtest/gtest.h"
+
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/Promise-inl.h"
+#include "xpcpublic.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+TEST(ThenWithCycleCollectedArgsJS, Empty)
+{
+ nsCOMPtr<nsIGlobalObject> global =
+ xpc::NativeGlobal(xpc::PrivilegedJunkScope());
+
+ RefPtr<Promise> promise = Promise::Create(global, IgnoreErrors());
+ auto result = promise->ThenWithCycleCollectedArgsJS(
+ [](JSContext*, JS::Handle<JS::Value>, ErrorResult&) { return nullptr; },
+ std::make_tuple(), std::make_tuple());
+}
+
+TEST(ThenWithCycleCollectedArgsJS, nsCOMPtr)
+{
+ nsCOMPtr<nsIGlobalObject> global =
+ xpc::NativeGlobal(xpc::PrivilegedJunkScope());
+
+ RefPtr<Promise> promise = Promise::Create(global, IgnoreErrors());
+ auto result = promise->ThenWithCycleCollectedArgsJS(
+ [](JSContext*, JS::Handle<JS::Value>, ErrorResult&, nsIGlobalObject*) {
+ return nullptr;
+ },
+ std::make_tuple(global), std::make_tuple());
+}
+
+TEST(ThenWithCycleCollectedArgsJS, RefPtr)
+{
+ nsCOMPtr<nsIGlobalObject> global =
+ xpc::NativeGlobal(xpc::PrivilegedJunkScope());
+
+ RefPtr<Promise> promise = Promise::Create(global, IgnoreErrors());
+ auto result = promise->ThenWithCycleCollectedArgsJS(
+ [](JSContext*, JS::Handle<JS::Value>, ErrorResult&, Promise*) {
+ return nullptr;
+ },
+ std::make_tuple(promise), std::make_tuple());
+}
+
+TEST(ThenWithCycleCollectedArgsJS, RefPtrAndJSHandle)
+{
+ nsCOMPtr<nsIGlobalObject> global =
+ xpc::NativeGlobal(xpc::PrivilegedJunkScope());
+
+ RefPtr<Promise> promise = Promise::Create(global, IgnoreErrors());
+ auto result = promise->ThenWithCycleCollectedArgsJS(
+ [](JSContext*, JS::Handle<JS::Value> v, ErrorResult&, Promise*,
+ JS::Handle<JS::Value>) { return nullptr; },
+ std::make_tuple(promise), std::make_tuple(JS::UndefinedHandleValue));
+}
+
+TEST(ThenWithCycleCollectedArgsJS, Mixed)
+{
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(cx);
+ JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
+
+ RefPtr<Promise> promise = Promise::Create(global, IgnoreErrors());
+ auto result = promise->ThenWithCycleCollectedArgsJS(
+ [](JSContext*, JS::Handle<JS::Value>, ErrorResult&, nsIGlobalObject*,
+ Promise*, JS::Handle<JS::Value>,
+ JS::Handle<JSObject*>) { return nullptr; },
+ std::make_tuple(global, promise),
+ std::make_tuple(JS::UndefinedHandleValue, JS::HandleObject(obj)));
+}
+
+TEST(ThenCatchWithCycleCollectedArgsJS, Empty)
+{
+ nsCOMPtr<nsIGlobalObject> global =
+ xpc::NativeGlobal(xpc::PrivilegedJunkScope());
+
+ RefPtr<Promise> promise = Promise::Create(global, IgnoreErrors());
+ auto result = promise->ThenCatchWithCycleCollectedArgsJS(
+ [](JSContext*, JS::Handle<JS::Value>, ErrorResult&) { return nullptr; },
+ [](JSContext*, JS::Handle<JS::Value>, ErrorResult&) { return nullptr; },
+ std::make_tuple(), std::make_tuple());
+}
+
+TEST(ThenCatchWithCycleCollectedArgsJS, nsCOMPtr)
+{
+ nsCOMPtr<nsIGlobalObject> global =
+ xpc::NativeGlobal(xpc::PrivilegedJunkScope());
+
+ RefPtr<Promise> promise = Promise::Create(global, IgnoreErrors());
+ auto result = promise->ThenCatchWithCycleCollectedArgsJS(
+ [](JSContext*, JS::Handle<JS::Value>, ErrorResult&, nsIGlobalObject*) {
+ return nullptr;
+ },
+ [](JSContext*, JS::Handle<JS::Value>, ErrorResult&, nsIGlobalObject*) {
+ return nullptr;
+ },
+ std::make_tuple(global), std::make_tuple());
+}
+
+TEST(ThenCatchWithCycleCollectedArgsJS, RefPtr)
+{
+ nsCOMPtr<nsIGlobalObject> global =
+ xpc::NativeGlobal(xpc::PrivilegedJunkScope());
+
+ RefPtr<Promise> promise = Promise::Create(global, IgnoreErrors());
+ auto result = promise->ThenCatchWithCycleCollectedArgsJS(
+ [](JSContext*, JS::Handle<JS::Value>, ErrorResult&, Promise*) {
+ return nullptr;
+ },
+ [](JSContext*, JS::Handle<JS::Value>, ErrorResult&, Promise*) {
+ return nullptr;
+ },
+ std::make_tuple(promise), std::make_tuple());
+}
+
+TEST(ThenCatchWithCycleCollectedArgsJS, RefPtrAndJSHandle)
+{
+ nsCOMPtr<nsIGlobalObject> global =
+ xpc::NativeGlobal(xpc::PrivilegedJunkScope());
+
+ RefPtr<Promise> promise = Promise::Create(global, IgnoreErrors());
+ auto result = promise->ThenCatchWithCycleCollectedArgsJS(
+ [](JSContext*, JS::Handle<JS::Value> v, ErrorResult&, Promise*,
+ JS::Handle<JS::Value>) { return nullptr; },
+ [](JSContext*, JS::Handle<JS::Value> v, ErrorResult&, Promise*,
+ JS::Handle<JS::Value>) { return nullptr; },
+ std::make_tuple(promise), std::make_tuple(JS::UndefinedHandleValue));
+}
+
+TEST(ThenCatchWithCycleCollectedArgsJS, Mixed)
+{
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(cx);
+ JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
+
+ RefPtr<Promise> promise = Promise::Create(global, IgnoreErrors());
+ auto result = promise->ThenCatchWithCycleCollectedArgsJS(
+ [](JSContext*, JS::Handle<JS::Value>, ErrorResult&, nsIGlobalObject*,
+ Promise*, JS::Handle<JS::Value>,
+ JS::Handle<JSObject*>) { return nullptr; },
+ [](JSContext*, JS::Handle<JS::Value>, ErrorResult&, nsIGlobalObject*,
+ Promise*, JS::Handle<JS::Value>,
+ JS::Handle<JSObject*>) { return nullptr; },
+ std::make_tuple(global, promise),
+ std::make_tuple(JS::UndefinedHandleValue, JS::HandleObject(obj)));
+}
diff --git a/dom/promise/gtest/moz.build b/dom/promise/gtest/moz.build
new file mode 100644
index 0000000000..a220822834
--- /dev/null
+++ b/dom/promise/gtest/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+
+UNIFIED_SOURCES += [
+ "NativeThenHandler.cpp",
+ "ThenWithCycleCollectedArgsJS.cpp",
+]
+
+LOCAL_INCLUDES += ["/dom/promise"]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/promise/moz.build b/dom/promise/moz.build
new file mode 100644
index 0000000000..c317f1fae0
--- /dev/null
+++ b/dom/promise/moz.build
@@ -0,0 +1,42 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+EXPORTS.mozilla.dom += [
+ "Promise-inl.h",
+ "Promise.h",
+ "PromiseDebugging.h",
+ "PromiseNativeHandler.h",
+ "PromiseWorkerProxy.h",
+]
+
+UNIFIED_SOURCES += [
+ "Promise.cpp",
+ "PromiseDebugging.cpp",
+ "PromiseNativeHandler.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "../base",
+ "../ipc",
+ "/js/xpconnect/src",
+]
+
+TEST_DIRS += [
+ "gtest",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+MOCHITEST_MANIFESTS += ["tests/mochitest.ini"]
+
+MOCHITEST_CHROME_MANIFESTS += ["tests/chrome.ini"]
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/unit/xpcshell.ini"]
diff --git a/dom/promise/tests/chrome.ini b/dom/promise/tests/chrome.ini
new file mode 100644
index 0000000000..1df9608c38
--- /dev/null
+++ b/dom/promise/tests/chrome.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+
+[test_on_new_promise.html]
+[test_on_promise_settled.html]
+[test_on_promise_settled_duplicates.html]
+[test_promise_xrays.html]
+support-files = file_promise_xrays.html
+[test_promise_argument_xrays.html]
+support-files = file_promise_xrays.html file_promise_argument_tests.js
+skip-if = debug == false
+[test_promise_retval_xrays.html]
+support-files = file_promise_xrays.html file_promise_retval_tests.js
+skip-if = debug == false
+[test_promise_job_with_bind_from_discarded_iframe.html]
+support-files = file_promise_job_with_bind_from_discarded_iframe.html
diff --git a/dom/promise/tests/file_promise_and_timeout_ordering.js b/dom/promise/tests/file_promise_and_timeout_ordering.js
new file mode 100644
index 0000000000..734621eae0
--- /dev/null
+++ b/dom/promise/tests/file_promise_and_timeout_ordering.js
@@ -0,0 +1,18 @@
+var log = [];
+var resolvedPromise = Promise.resolve(null);
+function schedulePromiseTask(f) {
+ resolvedPromise.then(f);
+}
+
+setTimeout(function () {
+ log.push("t1start");
+ schedulePromiseTask(function () {
+ log.push("promise");
+ });
+ log.push("t1end");
+}, 10);
+
+setTimeout(function () {
+ log.push("t2");
+ postMessage(log.join(", "));
+}, 10);
diff --git a/dom/promise/tests/file_promise_argument_tests.js b/dom/promise/tests/file_promise_argument_tests.js
new file mode 100644
index 0000000000..2a3b4e78c9
--- /dev/null
+++ b/dom/promise/tests/file_promise_argument_tests.js
@@ -0,0 +1,175 @@
+/*
+ * This file is meant to provide common infrastructure for several consumers.
+ * The consumer is expected to define the following things:
+ *
+ * 1) An verifyPromiseGlobal function which does whatever test the consumer
+ * wants.
+ * 2) An isXrayArgumentTest global boolean, because some of these tests act
+ * differenly based on that boolean.
+ * 3) A function named getPromise. This function is given a global object and a
+ * single argument to use for getting the promise. The getPromise function
+ * is expected to trigger the canonical Promise.resolve for the given global
+ * with the given argument in some way that depends on the test, and return
+ * the result.
+ * 4) A subframe (frames[0]) which can be used as a second global for creating
+ * promises.
+ */
+
+/* global verifyPromiseGlobal, getPromise, isXrayArgumentTest */
+
+var label = "parent";
+
+function passBasicPromise() {
+ var p1 = Promise.resolve();
+ verifyPromiseGlobal(p1, window, "Promise.resolve return value 1");
+ var p2 = getPromise(window, p1);
+ is(p1, p2, "Basic promise should just pass on through");
+ return p2;
+}
+
+function passPrimitive(global) {
+ var p = getPromise(global, 5);
+ verifyPromiseGlobal(p, global, "Promise wrapping primitive");
+ return p.then(function (arg) {
+ is(arg, 5, "Should have the arg we passed in");
+ });
+}
+
+function passThenable(global) {
+ var called = false;
+ var thenable = {
+ then(f) {
+ called = true;
+ f(7);
+ },
+ };
+ var p = getPromise(global, thenable);
+ verifyPromiseGlobal(p, global, "Promise wrapping thenable");
+ return p.then(function (arg) {
+ ok(called, "Thenable should have been called");
+ is(arg, 7, "Should have the arg our thenable passed in");
+ });
+}
+
+function passWrongPromiseWithMatchingConstructor() {
+ var p1 = Promise.resolve();
+ verifyPromiseGlobal(p1, window, "Promise.resolve() return value 2");
+ p1.constructor = frames[0].Promise;
+ var p2 = getPromise(frames[0], p1);
+ // The behavior here will depend on whether we're touching frames[0] via Xrays
+ // or not. If we are not, the current compartment while getting our promise
+ // will be that of frames[0]. If we are, it will be our window's compartment.
+ if (isXrayArgumentTest) {
+ isnot(
+ p1,
+ p2,
+ "Should have wrapped the Promise in a new promise, because its constructor is not matching the current-compartment Promise constructor"
+ );
+ verifyPromiseGlobal(
+ p2,
+ window,
+ "Promise wrapping xrayed promise with therefore non-matching constructor"
+ );
+ } else {
+ is(
+ p1,
+ p2,
+ "Should have left the Promise alone because its constructor matched"
+ );
+ }
+ return p2;
+}
+
+function passCorrectPromiseWithMismatchedConstructor() {
+ var p1 = Promise.resolve(9);
+ verifyPromiseGlobal(p1, window, "Promise.resolve() return value 3");
+ p1.constructor = frames[0].Promise;
+ var p2 = getPromise(window, p1);
+ isnot(
+ p1,
+ p2,
+ "Should have wrapped promise in a new promise, since its .constructor was wrong"
+ );
+ verifyPromiseGlobal(
+ p2,
+ window,
+ "Promise wrapping passed-in promise with mismatched constructor"
+ );
+ return p2.then(function (arg) {
+ is(arg, 9, "Should have propagated along our resolution value");
+ });
+}
+
+function passPromiseToOtherGlobal() {
+ var p1 = Promise.resolve();
+ verifyPromiseGlobal(p1, window, "Promise.resolve() return value 4");
+ var p2 = getPromise(frames[0], p1);
+ // The behavior here will depend on whether we're touching frames[0] via Xrays
+ // or not. If we are not, the current compartment while getting our promise
+ // will be that of frames[0]. If we are, it will be our window's compartment.
+ if (isXrayArgumentTest) {
+ is(
+ p1,
+ p2,
+ "Should have left the Promise alone, because its constructor matches the current compartment's constructor"
+ );
+ } else {
+ isnot(
+ p1,
+ p2,
+ "Should have wrapped promise in a promise from the other global"
+ );
+ verifyPromiseGlobal(
+ p2,
+ frames[0],
+ "Promise wrapping passed-in basic promise"
+ );
+ }
+ return p2;
+}
+
+function passPromiseSubclass() {
+ class PromiseSubclass extends Promise {
+ constructor(func) {
+ super(func);
+ }
+ }
+
+ var p1 = PromiseSubclass.resolve(11);
+ verifyPromiseGlobal(p1, window, "PromiseSubclass.resolve() return value");
+ var p2 = getPromise(window, p1);
+ isnot(p1, p2, "Should have wrapped promise subclass in a new promise");
+ verifyPromiseGlobal(
+ p2,
+ window,
+ "Promise wrapping passed-in promise subclass"
+ );
+ return p2.then(function (arg) {
+ is(
+ arg,
+ 11,
+ "Should have propagated along our resolution value from subclass"
+ );
+ });
+}
+
+function runPromiseArgumentTests(finishFunc) {
+ Promise.resolve()
+ .then(passBasicPromise)
+ .then(passPrimitive.bind(undefined, window))
+ .then(passPrimitive.bind(undefined, frames[0]))
+ .then(passThenable.bind(undefined, window))
+ .then(passThenable.bind(undefined, frames[0]))
+ .then(passWrongPromiseWithMatchingConstructor)
+ .then(passCorrectPromiseWithMismatchedConstructor)
+ .then(passPromiseToOtherGlobal)
+ .then(passPromiseSubclass)
+ .then(finishFunc)
+ .catch(function (e) {
+ ok(
+ false,
+ `Exception thrown: ${e}@${location.pathname}:${e.lineNumber}:${e.columnNumber}`
+ );
+ finishFunc();
+ });
+}
diff --git a/dom/promise/tests/file_promise_job_with_bind_from_discarded_iframe.html b/dom/promise/tests/file_promise_job_with_bind_from_discarded_iframe.html
new file mode 100644
index 0000000000..352cf6623e
--- /dev/null
+++ b/dom/promise/tests/file_promise_job_with_bind_from_discarded_iframe.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>iframe in http</title>
+</head>
+<body>
+<div id="result"></div>
+<script type="text/javascript">
+if (typeof SpecialPowers !== "undefined") {
+ document.getElementById("result").textContent = "ok";
+}
+</script>
+</body>
+</html>
diff --git a/dom/promise/tests/file_promise_retval_tests.js b/dom/promise/tests/file_promise_retval_tests.js
new file mode 100644
index 0000000000..37ede9e514
--- /dev/null
+++ b/dom/promise/tests/file_promise_retval_tests.js
@@ -0,0 +1,56 @@
+/*
+ * This file is meant to provide common infrastructure for several consumers.
+ * The consumer is expected to define the following things:
+ *
+ * 1) A verifyPromiseGlobal function which does whatever test the consumer
+ * wants. This function is passed a promise and the global whose
+ * TestFunctions was used to get the promise.
+ * 2) A expectedExceptionGlobal function which is handed the global whose
+ * TestFunctions was used to trigger the exception and should return the
+ * global the exception is expected to live in.
+ * 3) A subframe (frames[0]) which can be used as a second global for creating
+ * promises.
+ */
+
+/* global verifyPromiseGlobal, expectedExceptionGlobal */
+
+var label = "parent";
+
+function testThrownException(global) {
+ var p = global.TestFunctions.throwToRejectPromise();
+ verifyPromiseGlobal(p, global, "throwToRejectPromise return value");
+ return p
+ .then(() => {})
+ .catch(err => {
+ var expected = expectedExceptionGlobal(global);
+ is(
+ SpecialPowers.unwrap(SpecialPowers.Cu.getGlobalForObject(err)),
+ expected,
+ "Should have an exception object from the right global too"
+ );
+ ok(
+ err instanceof expected.DOMException,
+ "Should have a DOMException here"
+ );
+ is(
+ Object.getPrototypeOf(err),
+ expected.DOMException.prototype,
+ "Should have a DOMException from the right global"
+ );
+ is(err.name, "InvalidStateError", "Should have the right DOMException");
+ });
+}
+
+function runPromiseRetvalTests(finishFunc) {
+ Promise.resolve()
+ .then(testThrownException.bind(undefined, window))
+ .then(testThrownException.bind(undefined, frames[0]))
+ .then(finishFunc)
+ .catch(function (e) {
+ ok(
+ false,
+ `Exception thrown: ${e}@${location.pathname}:${e.lineNumber}:${e.columnNumber}`
+ );
+ finishFunc();
+ });
+}
diff --git a/dom/promise/tests/file_promise_xrays.html b/dom/promise/tests/file_promise_xrays.html
new file mode 100644
index 0000000000..e08014a337
--- /dev/null
+++ b/dom/promise/tests/file_promise_xrays.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+ <script>
+ function vendGetter(name) {
+ // eslint-disable-next-line no-throw-literal
+ return function() { throw "Getting " + String(name); };
+ }
+ function vendSetter(name) {
+ // eslint-disable-next-line no-throw-literal
+ return function() { throw "Setting " + String(name); };
+ }
+ var setupThrew = false;
+ try {
+ // Neuter everything we can think of on Promise.
+ for (var obj of [Promise, Promise.prototype]) {
+ let propNames = Object.getOwnPropertyNames(obj);
+ propNames = propNames.concat(Object.getOwnPropertySymbols(obj));
+ for (var propName of propNames) {
+ if ((propName == "prototype" ||
+ propName == Symbol.hasInstance) &&
+ obj == Promise) {
+ // They're not configurable.
+ continue;
+ }
+ Object.defineProperty(obj, propName,
+ { get: vendGetter(propName), set: vendSetter(propName) });
+ }
+ }
+ } catch (e) {
+ // Something went wrong. Save that info so the test can check for it.
+ setupThrew = e;
+ }
+ </script>
+</html>
diff --git a/dom/promise/tests/mochitest.ini b/dom/promise/tests/mochitest.ini
new file mode 100644
index 0000000000..bc3c3e4787
--- /dev/null
+++ b/dom/promise/tests/mochitest.ini
@@ -0,0 +1,28 @@
+[DEFAULT]
+support-files =
+ promise_uncatchable_exception.js
+
+[test_bug883683.html]
+[test_promise.html]
+[test_promise_uncatchable_exception.html]
+skip-if = debug == false
+[test_promise_utils.html]
+[test_resolve.html]
+[test_resolver_return_value.html]
+[test_thenable_vs_promise_ordering.html]
+[test_promise_and_timeout_ordering.html]
+support-files = file_promise_and_timeout_ordering.js
+[test_promise_and_timeout_ordering_workers.html]
+support-files = file_promise_and_timeout_ordering.js
+[test_species_getter.html]
+[test_webassembly_compile.html]
+support-files = test_webassembly_compile_sample.wasm test_webassembly_compile_worker.js test_webassembly_compile_worker_terminate.js
+[test_promise_argument.html]
+support-files = file_promise_argument_tests.js
+skip-if = debug == false
+[test_promise_callback_retval.html]
+support-files = file_promise_argument_tests.js
+skip-if = debug == false
+[test_promise_retval.html]
+support-files = file_promise_retval_tests.js
+skip-if = debug == false
diff --git a/dom/promise/tests/promise_uncatchable_exception.js b/dom/promise/tests/promise_uncatchable_exception.js
new file mode 100644
index 0000000000..eafc9e5448
--- /dev/null
+++ b/dom/promise/tests/promise_uncatchable_exception.js
@@ -0,0 +1,11 @@
+/* global TestFunctions */
+
+postMessage("Done", "*");
+
+var p = new Promise(function (resolve, reject) {
+ TestFunctions.throwUncatchableException();
+ ok(false, "Shouldn't get here!");
+}).catch(function (exception) {
+ ok(false, "Shouldn't get here!");
+});
+ok(false, "Shouldn't get here!");
diff --git a/dom/promise/tests/test_bug883683.html b/dom/promise/tests/test_bug883683.html
new file mode 100644
index 0000000000..1b31e32330
--- /dev/null
+++ b/dom/promise/tests/test_bug883683.html
@@ -0,0 +1,41 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>Promise - bug 883683</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript"><!--
+
+function runTest() {
+ [{}, {}, {}, {}, {}].reduce(Promise.reject.bind(Promise));
+ ok(true, "No leaks with reject?");
+
+ [{}, {}, {}, {}, {}].reduce(Promise.resolve.bind(Promise));
+ ok(true, "No leaks with resolve?");
+
+ [{}, {}, {}, {}, {}].reduce(function(a, b, c, d) { return new Promise(function(r1, r2) { throw a; }); });
+ ok(true, "No leaks with exception?");
+
+ [{}, {}, {}, {}, {}].reduce(function(a, b, c, d) { return new Promise(function(r1, r2) { }); });
+ ok(true, "No leaks with empty promise?");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+runTest();
+// -->
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_on_new_promise.html b/dom/promise/tests/test_on_new_promise.html
new file mode 100644
index 0000000000..195707f1d7
--- /dev/null
+++ b/dom/promise/tests/test_on_new_promise.html
@@ -0,0 +1,45 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+
+<!--
+Bug 1083210 - Sanity test for interaction between DOM promises and
+Debugger.prototype.onNewPromise.
+-->
+
+<html>
+<head>
+ <title>Test for interaction with SpiderMonkey's Debugger.prototype.onNewPromise</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ is(Object.prototype.toString.call(new Promise(function() {})),
+ "[object Promise]",
+ "We should have the native DOM promise implementation.");
+
+ const {addDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs");
+ var dbgGlobal = new Cu.Sandbox(document.nodePrincipal,
+ {freshCompartment: true});
+ addDebuggerToGlobal(dbgGlobal);
+ var dbg = new dbgGlobal.Debugger(this);
+
+ var wrappedPromise;
+ dbg.onNewPromise = function(wp) { wrappedPromise = wp; };
+
+ var promise = new Promise(function() {});
+ // eslint-disable-next-line no-debugger
+ debugger;
+ ok(wrappedPromise);
+ is(wrappedPromise.unsafeDereference(), promise);
+ </script>
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_on_promise_settled.html b/dom/promise/tests/test_on_promise_settled.html
new file mode 100644
index 0000000000..b40475206b
--- /dev/null
+++ b/dom/promise/tests/test_on_promise_settled.html
@@ -0,0 +1,53 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+
+<!--
+Bug 1084065 - Sanity test for interaction between DOM promises and
+Debugger.prototype.onPromiseResolved.
+-->
+
+<html>
+<head>
+ <title>Test for interaction with SpiderMonkey's Debugger.prototype.onNewPromise</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ is(Object.prototype.toString.call(new Promise(function() {})),
+ "[object Promise]",
+ "We should have the native DOM promise implementation.");
+
+ const {addDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs");
+ var dbgGlobal = new Cu.Sandbox(document.nodePrincipal,
+ {freshCompartment: true});
+ addDebuggerToGlobal(dbgGlobal);
+ var dbg = new dbgGlobal.Debugger(this);
+
+ var wrappedPromise;
+ dbg.onPromiseSettled = function(wp) { wrappedPromise = wp; };
+
+ var promise = Promise.resolve();
+ promise
+ .then(function() {
+ ok(wrappedPromise);
+ is(wrappedPromise.unsafeDereference(), promise);
+ dbg.onPromiseSettled = undefined;
+ })
+ .catch(function(e) {
+ ok(false, "Got an unexpected error: " + e);
+ })
+ .then(SimpleTest.finish);
+ </script>
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_on_promise_settled_duplicates.html b/dom/promise/tests/test_on_promise_settled_duplicates.html
new file mode 100644
index 0000000000..e11f4eaa60
--- /dev/null
+++ b/dom/promise/tests/test_on_promise_settled_duplicates.html
@@ -0,0 +1,58 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+
+<!--
+Bug 1084065 - Test that Debugger.prototype.onPromiseResolved doesn't get dupes.
+-->
+
+<html>
+<head>
+ <title>Test for interaction with SpiderMonkey's Debugger.prototype.onNewPromise</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ is(Object.prototype.toString.call(new Promise(function() {})),
+ "[object Promise]",
+ "We should have the native DOM promise implementation.");
+
+ const {addDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs");
+ var dbgGlobal = new Cu.Sandbox(document.nodePrincipal,
+ {freshCompartment: true});
+ addDebuggerToGlobal(dbgGlobal);
+ var dbg = new dbgGlobal.Debugger(this);
+
+ var seen = new Set();
+ dbg.onPromiseSettled = function(wp) {
+ is(seen.has(wp), false);
+ seen.add(wp);
+ };
+
+ var promise = new Promise(function(fulfill, reject) {
+ fulfill(1);
+ fulfill(2);
+ fulfill(3);
+ });
+
+ promise
+ .then(function() {
+ dbg.onPromiseSettled = undefined;
+ })
+ .catch(function(e) {
+ ok(false, "Got an unexpected error: " + e);
+ })
+ .then(SimpleTest.finish);
+ </script>
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_promise.html b/dom/promise/tests/test_promise.html
new file mode 100644
index 0000000000..7c724daf51
--- /dev/null
+++ b/dom/promise/tests/test_promise.html
@@ -0,0 +1,844 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>Basic Promise Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript"><!--
+
+function promiseResolve() {
+ ok(Promise, "Promise object should exist");
+
+ var promise = new Promise(function(resolve, reject) {
+ ok(resolve, "Promise.resolve exists");
+ ok(reject, "Promise.reject exists");
+
+ resolve(42);
+ });
+ promise.then(function(what) {
+ ok(true, "Then - resolveCb has been called");
+ is(what, 42, "ResolveCb received 42");
+ runTest();
+ }, function() {
+ ok(false, "Then - rejectCb has been called");
+ runTest();
+ });
+}
+
+function promiseResolveNoArg() {
+ var promise = new Promise(function(resolve, reject) {
+ ok(resolve, "Promise.resolve exists");
+ ok(reject, "Promise.reject exists");
+
+ resolve();
+ });
+ promise.then(function(what) {
+ ok(true, "Then - resolveCb has been called");
+ is(what, undefined, "ResolveCb received undefined");
+ runTest();
+ }, function() {
+ ok(false, "Then - rejectCb has been called");
+ runTest();
+ });
+}
+
+function promiseReject() {
+ var promise = new Promise(function(resolve, reject) {
+ reject(42);
+ });
+ promise.then(function(what) {
+ ok(false, "Then - resolveCb has been called");
+ runTest();
+ }, function(what) {
+ ok(true, "Then - rejectCb has been called");
+ is(what, 42, "RejectCb received 42");
+ runTest();
+ });
+}
+
+function promiseRejectNoHandler() {
+ // This test only checks that the code that reports unhandled errors in the
+ // Promises implementation does not crash or leak.
+
+ new Promise(function(res, rej) {
+ // eslint-disable-next-line no-undef
+ noSuchMethod();
+ });
+ runTest();
+}
+
+function promiseRejectNoArg() {
+ var promise = new Promise(function(resolve, reject) {
+ reject();
+ });
+ promise.then(function(what) {
+ ok(false, "Then - resolveCb has been called");
+ runTest();
+ }, function(what) {
+ ok(true, "Then - rejectCb has been called");
+ is(what, undefined, "RejectCb received undefined");
+ runTest();
+ });
+}
+
+function promiseException() {
+ var promise = new Promise(function(resolve, reject) {
+ // eslint-disable-next-line no-throw-literal
+ throw 42;
+ });
+ promise.then(function(what) {
+ ok(false, "Then - resolveCb has been called");
+ runTest();
+ }, function(what) {
+ ok(true, "Then - rejectCb has been called");
+ is(what, 42, "RejectCb received 42");
+ runTest();
+ });
+}
+
+function promiseGC() {
+ var resolve;
+ var promise = new Promise(function(r1, r2) {
+ resolve = r1;
+ });
+ promise.then(function(what) {
+ ok(true, "Then - promise is still alive");
+ runTest();
+ });
+
+ promise = null;
+
+ SpecialPowers.gc();
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+
+ resolve(42);
+}
+
+function promiseAsync_TimeoutResolveThen() {
+ var handlerExecuted = false;
+
+ setTimeout(function() {
+ ok(handlerExecuted, "Handler should have been called before the timeout.");
+
+ // Allow other assertions to run so the test could fail before the next one.
+ setTimeout(runTest, 0);
+ }, 0);
+
+ Promise.resolve().then(function() {
+ handlerExecuted = true;
+ });
+
+ ok(!handlerExecuted, "Handlers are not called before 'then' returns.");
+}
+
+function promiseAsync_ResolveTimeoutThen() {
+ var handlerExecuted = false;
+
+ var promise = Promise.resolve();
+
+ setTimeout(function() {
+ ok(handlerExecuted, "Handler should have been called before the timeout.");
+
+ // Allow other assertions to run so the test could fail before the next one.
+ setTimeout(runTest, 0);
+ }, 0);
+
+ promise.then(function() {
+ handlerExecuted = true;
+ });
+
+ ok(!handlerExecuted, "Handlers are not called before 'then' returns.");
+}
+
+function promiseAsync_ResolveThenTimeout() {
+ var handlerExecuted = false;
+
+ Promise.resolve().then(function() {
+ handlerExecuted = true;
+ });
+
+ setTimeout(function() {
+ ok(handlerExecuted, "Handler should have been called before the timeout.");
+
+ // Allow other assertions to run so the test could fail before the next one.
+ setTimeout(runTest, 0);
+ }, 0);
+
+ ok(!handlerExecuted, "Handlers are not called before 'then' returns.");
+}
+
+function promiseAsync_SyncXHR() {
+ var handlerExecuted = false;
+
+ Promise.resolve().then(function() {
+ handlerExecuted = true;
+
+ // Allow other assertions to run so the test could fail before the next one.
+ setTimeout(runTest, 0);
+ });
+
+ ok(!handlerExecuted, "Handlers are not called until the next microtask.");
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "testXHR.txt", false);
+ xhr.send(null);
+
+ ok(!handlerExecuted, "Sync XHR should not trigger microtask execution.");
+}
+
+function promiseDoubleThen() {
+ var steps = 0;
+ var promise = new Promise(function(r1, r2) {
+ r1(42);
+ });
+
+ promise.then(function(what) {
+ ok(true, "Then.resolve has been called");
+ is(what, 42, "Value == 42");
+ steps++;
+ }, function(what) {
+ ok(false, "Then.reject has been called");
+ });
+
+ promise.then(function(what) {
+ ok(true, "Then.resolve has been called");
+ is(steps, 1, "Then.resolve - step == 1");
+ is(what, 42, "Value == 42");
+ runTest();
+ }, function(what) {
+ ok(false, "Then.reject has been called");
+ });
+}
+
+function promiseThenException() {
+ var promise = new Promise(function(resolve, reject) {
+ resolve(42);
+ });
+
+ promise.then(function(what) {
+ ok(true, "Then.resolve has been called");
+ // eslint-disable-next-line no-throw-literal
+ throw "booh";
+ }).catch(function(e) {
+ ok(true, "window.onerror has been called!");
+ runTest();
+ });
+}
+
+function promiseThenCatchThen() {
+ var promise = new Promise(function(resolve, reject) {
+ resolve(42);
+ });
+
+ var promise2 = promise.then(function(what) {
+ ok(true, "Then.resolve has been called");
+ is(what, 42, "Value == 42");
+ return what + 1;
+ }, function(what) {
+ ok(false, "Then.reject has been called");
+ });
+
+ isnot(promise, promise2, "These 2 promise objs are different");
+
+ promise2.then(function(what) {
+ ok(true, "Then.resolve has been called");
+ is(what, 43, "Value == 43");
+ return what + 1;
+ }, function(what) {
+ ok(false, "Then.reject has been called");
+ }).catch(function() {
+ ok(false, "Catch has been called");
+ }).then(function(what) {
+ ok(true, "Then.resolve has been called");
+ is(what, 44, "Value == 44");
+ runTest();
+ }, function(what) {
+ ok(false, "Then.reject has been called");
+ });
+}
+
+function promiseThenNoArg() {
+ var promise = new Promise(function(resolve, reject) {
+ resolve(42);
+ });
+
+ var clone = promise.then();
+ isnot(promise, clone, "These 2 promise objs are different");
+ promise.then(function(v) {
+ clone.then(function(cv) {
+ is(v, cv, "Both resolve to the same value");
+ runTest();
+ });
+ });
+}
+
+function promiseThenUndefinedResolveFunction() {
+ var promise = new Promise(function(resolve, reject) {
+ reject(42);
+ });
+
+ try {
+ promise.then(undefined, function(v) {
+ is(v, 42, "Promise rejected with 42");
+ runTest();
+ });
+ } catch (e) {
+ ok(false, "then should not throw on undefined resolve function");
+ }
+}
+
+function promiseThenNullResolveFunction() {
+ var promise = new Promise(function(resolve, reject) {
+ reject(42);
+ });
+
+ try {
+ promise.then(null, function(v) {
+ is(v, 42, "Promise rejected with 42");
+ runTest();
+ });
+ } catch (e) {
+ ok(false, "then should not throw on null resolve function");
+ }
+}
+
+function promiseRejectThenCatchThen() {
+ var promise = new Promise(function(resolve, reject) {
+ reject(42);
+ });
+
+ var promise2 = promise.then(function(what) {
+ ok(false, "Then.resolve has been called");
+ }, function(what) {
+ ok(true, "Then.reject has been called");
+ is(what, 42, "Value == 42");
+ return what + 1;
+ });
+
+ isnot(promise, promise2, "These 2 promise objs are different");
+
+ promise2.then(function(what) {
+ ok(true, "Then.resolve has been called");
+ is(what, 43, "Value == 43");
+ return what + 1;
+ }).catch(function(what) {
+ ok(false, "Catch has been called");
+ }).then(function(what) {
+ ok(true, "Then.resolve has been called");
+ is(what, 44, "Value == 44");
+ runTest();
+ });
+}
+
+function promiseRejectThenCatchThen2() {
+ var promise = new Promise(function(resolve, reject) {
+ reject(42);
+ });
+
+ promise.then(function(what) {
+ ok(true, "Then.resolve has been called");
+ is(what, 42, "Value == 42");
+ return what + 1;
+ }).catch(function(what) {
+ is(what, 42, "Value == 42");
+ ok(true, "Catch has been called");
+ return what + 1;
+ }).then(function(what) {
+ ok(true, "Then.resolve has been called");
+ is(what, 43, "Value == 43");
+ runTest();
+ });
+}
+
+function promiseRejectThenCatchExceptionThen() {
+ var promise = new Promise(function(resolve, reject) {
+ reject(42);
+ });
+
+ promise.then(function(what) {
+ ok(false, "Then.resolve has been called");
+ }, function(what) {
+ ok(true, "Then.reject has been called");
+ is(what, 42, "Value == 42");
+ // eslint-disable-next-line no-throw-literal
+ throw (what + 1);
+ }).catch(function(what) {
+ ok(true, "Catch has been called");
+ is(what, 43, "Value == 43");
+ return what + 1;
+ }).then(function(what) {
+ ok(true, "Then.resolve has been called");
+ is(what, 44, "Value == 44");
+ runTest();
+ });
+}
+
+function promiseThenCatchOrderingResolve() {
+ var global = 0;
+ var f = new Promise(function(r1, r2) {
+ r1(42);
+ });
+
+ f.then(function() {
+ f.then(function() {
+ global++;
+ });
+ f.catch(function() {
+ global++;
+ });
+ f.then(function() {
+ global++;
+ });
+ setTimeout(function() {
+ is(global, 2, "Many steps... should return 2");
+ runTest();
+ }, 0);
+ });
+}
+
+function promiseThenCatchOrderingReject() {
+ var global = 0;
+ var f = new Promise(function(r1, r2) {
+ r2(42);
+ });
+
+ f.then(function() {}, function() {
+ f.then(function() {
+ global++;
+ });
+ f.catch(function() {
+ global++;
+ });
+ f.then(function() {}, function() {
+ global++;
+ });
+ setTimeout(function() {
+ is(global, 2, "Many steps... should return 2");
+ runTest();
+ }, 0);
+ });
+}
+
+function promiseCatchNoArg() {
+ var promise = new Promise(function(resolve, reject) {
+ reject(42);
+ });
+
+ var clone = promise.catch();
+ isnot(promise, clone, "These 2 promise objs are different");
+ promise.catch(function(v) {
+ clone.catch(function(cv) {
+ is(v, cv, "Both reject to the same value");
+ runTest();
+ });
+ });
+}
+
+function promiseNestedPromise() {
+ new Promise(function(resolve, reject) {
+ resolve(new Promise(function(res, rej) {
+ ok(true, "Nested promise is executed");
+ res(42);
+ }));
+ }).then(function(value) {
+ is(value, 42, "Nested promise is executed and then == 42");
+ runTest();
+ });
+}
+
+function promiseNestedNestedPromise() {
+ new Promise(function(resolve, reject) {
+ resolve(new Promise(function(res, rej) {
+ ok(true, "Nested promise is executed");
+ res(42);
+ }).then(function(what) { return what + 1; }));
+ }).then(function(value) {
+ is(value, 43, "Nested promise is executed and then == 43");
+ runTest();
+ });
+}
+
+function promiseWrongNestedPromise() {
+ new Promise(function(resolve, reject) {
+ resolve(new Promise(function(r, r2) {
+ ok(true, "Nested promise is executed");
+ r(42);
+ }));
+ reject(42);
+ }).then(function(value) {
+ is(value, 42, "Nested promise is executed and then == 42");
+ runTest();
+ }, function(value) {
+ ok(false, "This is wrong");
+ });
+}
+
+function promiseLoop() {
+ new Promise(function(resolve, reject) {
+ resolve(new Promise(function(res, rej) {
+ ok(true, "Nested promise is executed");
+ res(new Promise(function(resInner, rejInner) {
+ ok(true, "Nested nested promise is executed");
+ resInner(42);
+ }));
+ }));
+ }).then(function(value) {
+ is(value, 42, "Nested nested promise is executed and then == 42");
+ runTest();
+ }, function(value) {
+ ok(false, "This is wrong");
+ });
+}
+
+function promiseStaticReject() {
+ var promise = Promise.reject(42);
+ promise.then(function(what) {
+ ok(false, "This should not be called");
+ }, function(what) {
+ is(what, 42, "Value == 42");
+ runTest();
+ });
+}
+
+function promiseStaticResolve() {
+ var promise = Promise.resolve(42);
+ promise.then(function(what) {
+ is(what, 42, "Value == 42");
+ runTest();
+ }, function() {
+ ok(false, "This should not be called");
+ });
+}
+
+function promiseResolveNestedPromise() {
+ var promise = Promise.resolve(new Promise(function(r, r2) {
+ ok(true, "Nested promise is executed");
+ r(42);
+ }, function() {
+ ok(false, "This should not be called");
+ }));
+ promise.then(function(what) {
+ is(what, 42, "Value == 42");
+ runTest();
+ }, function() {
+ ok(false, "This should not be called");
+ });
+}
+
+function promiseSimpleThenableResolve() {
+ var thenable = { then(resolve) { resolve(5); } };
+ var promise = new Promise(function(resolve, reject) {
+ resolve(thenable);
+ });
+
+ promise.then(function(v) {
+ ok(v === 5, "promiseSimpleThenableResolve");
+ runTest();
+ }, function(e) {
+ ok(false, "promiseSimpleThenableResolve: Should not reject");
+ });
+}
+
+function promiseSimpleThenableReject() {
+ var thenable = { then(resolve, reject) { reject(5); } };
+ var promise = new Promise(function(resolve, reject) {
+ resolve(thenable);
+ });
+
+ promise.then(function() {
+ ok(false, "promiseSimpleThenableReject: Should not resolve");
+ runTest();
+ }, function(e) {
+ ok(e === 5, "promiseSimpleThenableReject");
+ runTest();
+ });
+}
+
+function promiseThenableThrowsBeforeCallback() {
+ var thenable = { then(resolve) {
+ throw new TypeError("Hi there");
+
+ // eslint-disable-next-line no-unreachable
+ resolve(5);
+ }};
+
+ var promise = Promise.resolve(thenable);
+ promise.then(function(v) {
+ ok(false, "promiseThenableThrowsBeforeCallback: Should've rejected");
+ runTest();
+ }, function(e) {
+ ok(e instanceof TypeError, "promiseThenableThrowsBeforeCallback");
+ runTest();
+ });
+}
+
+function promiseThenableThrowsAfterCallback() {
+ var thenable = { then(resolve) {
+ resolve(5);
+ throw new TypeError("Hi there");
+ }};
+
+ var promise = Promise.resolve(thenable);
+ promise.then(function(v) {
+ ok(v === 5, "promiseThenableThrowsAfterCallback");
+ runTest();
+ }, function(e) {
+ ok(false, "promiseThenableThrowsAfterCallback: Should've resolved");
+ runTest();
+ });
+}
+
+function promiseThenableRejectThenResolve() {
+ var thenable = { then(resolve, reject) {
+ reject(new TypeError("Hi there"));
+ resolve(5);
+ }};
+
+ var promise = Promise.resolve(thenable);
+ promise.then(function(v) {
+ ok(false, "promiseThenableRejectThenResolve should have rejected");
+ runTest();
+ }, function(e) {
+ ok(e instanceof TypeError, "promiseThenableRejectThenResolve");
+ runTest();
+ });
+}
+
+function promiseWithThenReplaced() {
+ // Ensure that we call the 'then' on the promise and not the internal then.
+ var promise = new Promise(function(resolve, reject) {
+ resolve(5);
+ });
+
+ // Rogue `then` always rejects.
+ promise.then = function(onFulfill, onReject) {
+ onReject(new TypeError("Foo"));
+ };
+
+ var promise2 = Promise.resolve(promise);
+ promise2.then(function(v) {
+ ok(false, "promiseWithThenReplaced: Should've rejected");
+ runTest();
+ }, function(e) {
+ ok(e instanceof TypeError, "promiseWithThenReplaced");
+ runTest();
+ });
+}
+
+function promiseStrictHandlers() {
+ var promise = Promise.resolve(5);
+ promise.then(function() {
+ "use strict";
+ ok(this === undefined, "Strict mode callback should have this === undefined.");
+ runTest();
+ });
+}
+
+function promiseStrictExecutorThisArg() {
+ new Promise(function(resolve, reject) {
+ "use strict";
+ ok(this === undefined, "thisArg should be undefined.");
+ runTest();
+ });
+}
+
+function promiseResolveArray() {
+ var p = Promise.resolve([1, 2, 3]);
+ ok(p instanceof Promise, "Should return a Promise.");
+ p.then(function(v) {
+ ok(Array.isArray(v), "Resolved value should be an Array");
+ is(v.length, 3, "Length should match");
+ is(v[0], 1, "Resolved value should match original");
+ is(v[1], 2, "Resolved value should match original");
+ is(v[2], 3, "Resolved value should match original");
+ runTest();
+ });
+}
+
+function promiseResolveThenable() {
+ var p = Promise.resolve({ then(onFulfill, onReject) { onFulfill(2); } });
+ ok(p instanceof Promise, "Should cast to a Promise.");
+ p.then(function(v) {
+ is(v, 2, "Should resolve to 2.");
+ runTest();
+ }, function(e) {
+ ok(false, "promiseResolveThenable should've resolved");
+ runTest();
+ });
+}
+
+function promiseResolvePromise() {
+ var original = Promise.resolve(true);
+ var cast = Promise.resolve(original);
+
+ ok(cast instanceof Promise, "Should cast to a Promise.");
+ is(cast, original, "Should return original Promise.");
+ cast.then(function(v) {
+ is(v, true, "Should resolve to true.");
+ runTest();
+ });
+}
+
+// Bug 1009569.
+// Ensure that thenables are run on a clean stack asynchronously.
+// Test case adopted from
+// https://gist.github.com/getify/d64bb01751b50ed6b281#file-bug1-js.
+function promiseResolveThenableCleanStack() {
+ function immed(s) { x++; s(); }
+ function incX() { x++; }
+
+ var x = 0;
+ var thenable = { then: immed };
+ var results = [];
+
+ var p = Promise.resolve(thenable).then(incX);
+ results.push(x);
+
+ // check what happens after all "next cycle" steps
+ // have had a chance to complete
+ setTimeout(function() {
+ // Result should be [0, 2] since `thenable` will be called async.
+ is(results[0], 0, "Expected thenable to be called asynchronously");
+ // See Bug 1023547 comment 13 for why this check has to be gated on p.
+ p.then(function() {
+ results.push(x);
+ is(results[1], 2, "Expected thenable to be called asynchronously");
+ runTest();
+ });
+ }, 1000);
+}
+
+// Bug 1008467 - Promise fails with "too much recursion".
+// The bug was that the callbacks passed to a thenable would resolve the
+// promise synchronously when the fulfill handler returned a non-thenable.
+//
+// For example:
+// var p = new Promise(function(resolve) {
+// resolve(5);
+// });
+// var m = Promise.resolve(p);
+//
+// At this point `m` is a Promise that is resolved with a thenable `p`, so it
+// calls `p.then()` with two callbacks, both of which would synchronously resolve
+// `m` when `p` invoked them (on account of itself being resolved, possibly
+// synchronously. A chain of these 'Promise resolved by a Promise' would lead to
+// stack overflow.
+function promiseTestAsyncThenableResolution() {
+ var k = 3000;
+ Promise.resolve().then(function next() {
+ k--;
+ if (k > 0) return Promise.resolve().then(next);
+ return undefined;
+ }).then(function() {
+ ok(true, "Resolution of a chain of thenables should not be synchronous.");
+ runTest();
+ });
+}
+
+// Bug 1062323
+function promiseWrapperAsyncResolution() {
+ var p = new Promise(function(resolve, reject) {
+ resolve();
+ });
+
+ var results = [];
+ var q = p.then(function() {
+ results.push("1-1");
+ }).then(function() {
+ results.push("1-2");
+ }).then(function() {
+ results.push("1-3");
+ });
+
+ var r = p.then(function() {
+ results.push("2-1");
+ }).then(function() {
+ results.push("2-2");
+ }).then(function() {
+ results.push("2-3");
+ });
+
+ Promise.all([q, r]).then(function() {
+ var match = results[0] == "1-1" &&
+ results[1] == "2-1" &&
+ results[2] == "1-2" &&
+ results[3] == "2-2" &&
+ results[4] == "1-3" &&
+ results[5] == "2-3";
+ info(results);
+ ok(match, "Chained promises should resolve asynchronously.");
+ runTest();
+ }, function() {
+ ok(false, "promiseWrapperAsyncResolution: One of the promises failed.");
+ runTest();
+ });
+}
+
+var tests = [ promiseResolve, promiseReject,
+ promiseException, promiseGC,
+ promiseAsync_TimeoutResolveThen,
+ promiseAsync_ResolveTimeoutThen,
+ promiseAsync_ResolveThenTimeout,
+ promiseAsync_SyncXHR,
+ promiseDoubleThen, promiseThenException,
+ promiseThenCatchThen, promiseRejectThenCatchThen,
+ promiseRejectThenCatchThen2,
+ promiseRejectThenCatchExceptionThen,
+ promiseThenCatchOrderingResolve,
+ promiseThenCatchOrderingReject,
+ promiseNestedPromise, promiseNestedNestedPromise,
+ promiseWrongNestedPromise, promiseLoop,
+ promiseStaticReject, promiseStaticResolve,
+ promiseResolveNestedPromise,
+ promiseResolveNoArg,
+ promiseRejectNoArg,
+ promiseThenNoArg,
+ promiseThenUndefinedResolveFunction,
+ promiseThenNullResolveFunction,
+ promiseCatchNoArg,
+ promiseRejectNoHandler,
+ promiseSimpleThenableResolve,
+ promiseSimpleThenableReject,
+ promiseThenableThrowsBeforeCallback,
+ promiseThenableThrowsAfterCallback,
+ promiseThenableRejectThenResolve,
+ promiseWithThenReplaced,
+ promiseStrictHandlers,
+ promiseStrictExecutorThisArg,
+ promiseResolveArray,
+ promiseResolveThenable,
+ promiseResolvePromise,
+ promiseResolveThenableCleanStack,
+ promiseTestAsyncThenableResolution,
+ promiseWrapperAsyncResolution,
+ ];
+
+function runTest() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+runTest();
+// -->
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_promise_and_timeout_ordering.html b/dom/promise/tests/test_promise_and_timeout_ordering.html
new file mode 100644
index 0000000000..e92e928e75
--- /dev/null
+++ b/dom/promise/tests/test_promise_and_timeout_ordering.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test for promise and timeout ordering</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+/* global async_test, assert_equals */
+var t = async_test("Promise callbacks should run immediately after the setTimeout handler that enqueues them");
+var origPostMessage = window.postMessage;
+window.postMessage = function(msg) { origPostMessage.call(window, msg, "*"); };
+window.onmessage = t.step_func_done(function(e) {
+ assert_equals(e.data, "t1start, t1end, promise, t2");
+});
+</script>
+<script src="file_promise_and_timeout_ordering.js"></script>
diff --git a/dom/promise/tests/test_promise_and_timeout_ordering_workers.html b/dom/promise/tests/test_promise_and_timeout_ordering_workers.html
new file mode 100644
index 0000000000..85fd4c0019
--- /dev/null
+++ b/dom/promise/tests/test_promise_and_timeout_ordering_workers.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test for promise and timeout ordering in workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+/* global async_test, assert_equals */
+var t = async_test("Promise callbacks in workers should run immediately after the setTimeout handler that enqueues them");
+var w = new Worker("file_promise_and_timeout_ordering.js");
+w.onmessage = t.step_func_done(function(e) {
+ assert_equals(e.data, "t1start, t1end, promise, t2");
+});
+</script>
diff --git a/dom/promise/tests/test_promise_argument.html b/dom/promise/tests/test_promise_argument.html
new file mode 100644
index 0000000000..22343ef00f
--- /dev/null
+++ b/dom/promise/tests/test_promise_argument.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1323324
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1323324</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="file_promise_argument_tests.js"></script>
+ <script type="application/javascript">
+ /** Test for Bug 1323324 **/
+ SimpleTest.waitForExplicitFinish();
+
+ var globalWrapper;
+ function verifyPromiseGlobal(p, global, msg) {
+ // SpecialPowers.Cu.getGlobalForObject returns a SpecialPowers wrapper for
+ // the actual global. We want to grab the underlying object.
+ globalWrapper = SpecialPowers.Cu.getGlobalForObject(p);
+ is(SpecialPowers.unwrap(globalWrapper), global,
+ msg + " should come from " + global.label);
+ }
+
+ const isXrayArgumentTest = false;
+
+ function getPromise(global, arg) {
+ return global.TestFunctions.passThroughPromise(arg);
+ }
+
+ addLoadEvent(function() {
+ frames[0].label = "child";
+ SpecialPowers.pushPrefEnv({set: [["dom.expose_test_interfaces", true]]},
+ runPromiseArgumentTests.bind(undefined,
+ SimpleTest.finish));
+ });
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1323324">Mozilla Bug 1323324</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <!-- A subframe so we have another global to work with -->
+ <iframe></iframe>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_promise_argument_xrays.html b/dom/promise/tests/test_promise_argument_xrays.html
new file mode 100644
index 0000000000..f3a234cf6d
--- /dev/null
+++ b/dom/promise/tests/test_promise_argument_xrays.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1233324
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1233324</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1233324">Mozilla Bug 1233324</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe id="t" src="http://example.org/chrome/dom/promise/tests/file_promise_xrays.html"></iframe>
+</div>
+
+<pre id="test">
+<script src="file_promise_argument_tests.js"></script>
+<script type="application/javascript">
+
+var win = $("t").contentWindow;
+
+/** Test for Bug 1233324 **/
+SimpleTest.waitForExplicitFinish();
+
+function testLoadComplete() {
+ is(win.location.href, $("t").src, "Should have loaded the right thing");
+ nextTest();
+}
+
+function testHaveXray() {
+ is(typeof win.Promise.race, "function", "Should see a race() function");
+ var exception;
+ try {
+ win.Promise.wrappedJSObject.race;
+ } catch (e) {
+ exception = e;
+ }
+ is(exception, "Getting race", "Should have thrown the right exception");
+ is(win.wrappedJSObject.setupThrew, false, "Setup should not have thrown");
+ nextTest();
+}
+
+function verifyPromiseGlobal(p, _, msg) {
+ // SpecialPowers.Cu.getGlobalForObject returns a SpecialPowers wrapper for
+ // the actual global. We want to grab the underlying object.
+ var global = SpecialPowers.unwrap(SpecialPowers.Cu.getGlobalForObject(p));
+
+ // We expect our global to always be "window" here, because we're working over
+ // Xrays.
+ is(global, window, msg + " should come from " + window.label);
+}
+
+const isXrayArgumentTest = true;
+
+function getPromise(global, arg) {
+ return global.TestFunctions.passThroughPromise(arg);
+}
+
+function testPromiseArgumentConversions() {
+ runPromiseArgumentTests(nextTest);
+}
+
+var tests = [
+ testLoadComplete,
+ testHaveXray,
+ testPromiseArgumentConversions,
+];
+
+function nextTest() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ tests.shift()();
+}
+
+addLoadEvent(function() {
+ frames[0].label = "child";
+ SpecialPowers.pushPrefEnv({set: [["dom.expose_test_interfaces", true]]},
+ nextTest);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_promise_callback_retval.html b/dom/promise/tests/test_promise_callback_retval.html
new file mode 100644
index 0000000000..332c370a3e
--- /dev/null
+++ b/dom/promise/tests/test_promise_callback_retval.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1323324
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1323324</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="file_promise_argument_tests.js"></script>
+ <script type="application/javascript">
+ /* global TestFunctions */
+
+ /** Test for Bug 1323324 **/
+ SimpleTest.waitForExplicitFinish();
+
+ var globalWrapper;
+ function verifyPromiseGlobal(p, global, msg) {
+ // SpecialPowers.Cu.getGlobalForObject returns a SpecialPowers wrapper for
+ // the actual global. We want to grab the underlying object.
+ globalWrapper = SpecialPowers.Cu.getGlobalForObject(p);
+ is(SpecialPowers.unwrap(globalWrapper), global,
+ msg + " should come from " + global.label);
+ }
+
+ const isXrayArgumentTest = false;
+
+ var func;
+ function getPromise(global, arg) {
+ func = new global.Function("x", "return x").bind(undefined, arg);
+ return TestFunctions.passThroughCallbackPromise(func);
+ }
+
+ addLoadEvent(function() {
+ frames[0].label = "child";
+ SpecialPowers.pushPrefEnv({set: [["dom.expose_test_interfaces", true]]},
+ runPromiseArgumentTests.bind(undefined,
+ SimpleTest.finish));
+ });
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1323324">Mozilla Bug 1323324</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <!-- A subframe so we have another global to work with -->
+ <iframe></iframe>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_promise_job_with_bind_from_discarded_iframe.html b/dom/promise/tests/test_promise_job_with_bind_from_discarded_iframe.html
new file mode 100644
index 0000000000..1cea250072
--- /dev/null
+++ b/dom/promise/tests/test_promise_job_with_bind_from_discarded_iframe.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1723124.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1723124.</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1723124.">Mozilla Bug 1723124.</a>
+
+<iframe id="frame" src="http://example.org/chrome/dom/promise/tests/file_promise_job_with_bind_from_discarded_iframe.html"></iframe>
+
+<pre id="test">
+<script type="text/javascript">
+/** Test for Bug 1723124. **/
+SimpleTest.waitForExplicitFinish();
+
+var frame = document.getElementById("frame");
+
+SimpleTest.waitForCondition(() => {
+ var result = frame.contentDocument.getElementById("result");
+ if (!result) {
+ return false;
+ }
+ // Wait for the iframe's script to check if it has no access to SpecialPowers.
+ return result.textContent == "ok";
+}, () => {
+ var iframe_bind = frame.contentWindow.Function.prototype.bind;
+ // Removing iframe from the tree discards the browsing context,
+ // and promise jobs in the iframe global stops working.
+ frame.remove();
+
+ Promise.resolve(10)
+ .then(function (v) {
+ // Handler in top-level realm, without bind.
+ //
+ // This job is created with the top-level realm, and should be called.
+ is(v, 10, "normal function should get the value from promise");
+ return 20;
+ }, function () {
+ ok(false, "unexpectedly rejected");
+ })
+ .then(iframe_bind.call(function (bound_arg, v) {
+ // Handler in top-level realm, with bind in discarded iframe.
+ //
+ // This job is also created with the top-level realm, and should be
+ // called.
+ is(v, 20, "bound function should get the value from promise");
+ is(bound_arg, 30, "bound function should get the arguments from bind");
+ SimpleTest.finish();
+ }, this, 30), function () {
+ ok(false, "unexpectedly rejected");
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_promise_retval.html b/dom/promise/tests/test_promise_retval.html
new file mode 100644
index 0000000000..e425b8e203
--- /dev/null
+++ b/dom/promise/tests/test_promise_retval.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1436276.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1436276.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="file_promise_retval_tests.js"></script>
+ <script type="application/javascript">
+ /** Test for Bug 1436276. **/
+ SimpleTest.waitForExplicitFinish();
+
+ function verifyPromiseGlobal(p, global, msg) {
+ // SpecialPowers.Cu.getGlobalForObject returns a SpecialPowers wrapper for
+ // the actual global. We want to grab the underlying object.
+ var globalWrapper = SpecialPowers.Cu.getGlobalForObject(p);
+ is(SpecialPowers.unwrap(globalWrapper), global,
+ msg + " should come from " + global.label);
+ }
+
+ function expectedExceptionGlobal(global) {
+ // We should end up with an exception from "global".
+ return global;
+ }
+
+ function getPromise(global, arg) {
+ return global.TestFunctions.passThroughPromise(arg);
+ }
+
+ addLoadEvent(function() {
+ frames[0].label = "child";
+ SpecialPowers.pushPrefEnv({set: [["dom.expose_test_interfaces", true]]},
+ runPromiseRetvalTests.bind(undefined,
+ SimpleTest.finish));
+ });
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1323324">Mozilla Bug 1323324</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <!-- A subframe so we have another global to work with -->
+ <iframe></iframe>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_promise_retval_xrays.html b/dom/promise/tests/test_promise_retval_xrays.html
new file mode 100644
index 0000000000..1270e3a3bb
--- /dev/null
+++ b/dom/promise/tests/test_promise_retval_xrays.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1436276.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1436276.</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1436276.">Mozilla Bug 1436276.</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe id="t" src="http://example.org/chrome/dom/promise/tests/file_promise_xrays.html"></iframe>
+</div>
+
+<pre id="test">
+<script src="file_promise_retval_tests.js"></script>
+<script type="application/javascript">
+
+var win = $("t").contentWindow;
+
+/** Test for Bug 1233324 **/
+SimpleTest.waitForExplicitFinish();
+
+function testLoadComplete() {
+ is(win.location.href, $("t").src, "Should have loaded the right thing");
+ nextTest();
+}
+
+function testHaveXray() {
+ is(typeof win.Promise.race, "function", "Should see a race() function");
+ var exception;
+ try {
+ win.Promise.wrappedJSObject.race;
+ } catch (e) {
+ exception = e;
+ }
+ is(exception, "Getting race", "Should have thrown the right exception");
+ is(win.wrappedJSObject.setupThrew, false, "Setup should not have thrown");
+ nextTest();
+}
+
+function verifyPromiseGlobal(p, _, msg) {
+ // SpecialPowers.Cu.getGlobalForObject returns a SpecialPowers wrapper for
+ // the actual global. We want to grab the underlying object.
+ var global = SpecialPowers.unwrap(SpecialPowers.Cu.getGlobalForObject(p));
+
+ // We expect our global to always be "window" here, because we're working over
+ // Xrays.
+ is(global, window, msg + " should come from " + window.label);
+}
+
+function expectedExceptionGlobal(_) {
+ // We should end up with an exception from "window" no matter what global
+ // was involved to start with, because we're working over Xrays.
+ return window;
+}
+
+function getPromise(global, arg) {
+ return global.TestFunctions.passThroughPromise(arg);
+}
+
+function testPromiseRetvals() {
+ runPromiseRetvalTests(nextTest);
+}
+
+var tests = [
+ testLoadComplete,
+ testHaveXray,
+ testPromiseRetvals,
+];
+
+function nextTest() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ tests.shift()();
+}
+
+addLoadEvent(function() {
+ frames[0].label = "child";
+ SpecialPowers.pushPrefEnv({set: [["dom.expose_test_interfaces", true]]},
+ nextTest);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_promise_uncatchable_exception.html b/dom/promise/tests/test_promise_uncatchable_exception.html
new file mode 100644
index 0000000000..2bb6f1fe17
--- /dev/null
+++ b/dom/promise/tests/test_promise_uncatchable_exception.html
@@ -0,0 +1,35 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>Promise - uncatchable exceptions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+onmessage = function(evt) {
+ ok(true, "finished");
+ SimpleTest.finish();
+};
+
+function go() {
+ var script = document.createElement("script");
+ script.src = "promise_uncatchable_exception.js";
+ document.body.appendChild(script);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({set: [["dom.expose_test_interfaces", true]]}, go);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_promise_utils.html b/dom/promise/tests/test_promise_utils.html
new file mode 100644
index 0000000000..b20d909351
--- /dev/null
+++ b/dom/promise/tests/test_promise_utils.html
@@ -0,0 +1,313 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>Test for Promise.all, Promise.race</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript"><!--
+
+function promiseUtilitiesDefined() {
+ ok(Promise.all, "Promise.all must be defined when Promise is enabled.");
+ ok(Promise.race, "Promise.race must be defined when Promise is enabled.");
+ runTest();
+}
+
+function promiseAllEmptyArray() {
+ var p = Promise.all([]);
+ ok(p instanceof Promise, "Return value of Promise.all should be a Promise.");
+ p.then(function(values) {
+ ok(Array.isArray(values), "Resolved value should be an array.");
+ is(values.length, 0, "Resolved array length should match iterable's length.");
+ runTest();
+ }, function() {
+ ok(false, "Promise.all shouldn't fail when iterable has no rejected Promises.");
+ runTest();
+ });
+}
+
+function promiseAllArray() {
+ var p = Promise.all([1, new Date(), Promise.resolve("firefox")]);
+ ok(p instanceof Promise, "Return value of Promise.all should be a Promise.");
+ p.then(function(values) {
+ ok(Array.isArray(values), "Resolved value should be an array.");
+ is(values.length, 3, "Resolved array length should match iterable's length.");
+ is(values[0], 1, "Array values should match.");
+ ok(values[1] instanceof Date, "Array values should match.");
+ is(values[2], "firefox", "Array values should match.");
+ runTest();
+ }, function() {
+ ok(false, "Promise.all shouldn't fail when iterable has no rejected Promises.");
+ runTest();
+ });
+}
+
+function promiseAllIterable() {
+ function* promiseGen() {
+ var i = 3;
+ while (--i) {
+ yield Promise.resolve(i);
+ }
+
+ yield new Promise(function(resolve) {
+ setTimeout(resolve, 10);
+ });
+ }
+
+ Promise.all(promiseGen()).then(function(values) {
+ is(values.length, 3, "Resolved array length should match iterable's length.");
+ is(values[0], 2, "Array values should match.");
+ is(values[1], 1, "Array values should match.");
+ is(values[2], undefined, "Array values should match.");
+ runTest();
+ }, function(e) {
+ ok(false, "Promise.all shouldn't fail when an iterable is passed.");
+ runTest();
+ });
+}
+
+function promiseAllWaitsForAllPromises() {
+ var arr = [
+ new Promise(function(resolve) {
+ setTimeout(resolve.bind(undefined, 1), 50);
+ }),
+ new Promise(function(resolve) {
+ setTimeout(resolve.bind(undefined, 2), 10);
+ }),
+ new Promise(function(resolve) {
+ setTimeout(resolve.bind(undefined, new Promise(function(resolve2) {
+ resolve2(3);
+ })), 10);
+ }),
+ new Promise(function(resolve) {
+ setTimeout(resolve.bind(undefined, 4), 20);
+ }),
+ ];
+
+ var p = Promise.all(arr);
+ p.then(function(values) {
+ ok(Array.isArray(values), "Resolved value should be an array.");
+ is(values.length, 4, "Resolved array length should match iterable's length.");
+ is(values[0], 1, "Array values should match.");
+ is(values[1], 2, "Array values should match.");
+ is(values[2], 3, "Array values should match.");
+ is(values[3], 4, "Array values should match.");
+ runTest();
+ }, function() {
+ ok(false, "Promise.all shouldn't fail when iterable has no rejected Promises.");
+ runTest();
+ });
+}
+
+function promiseAllRejectFails() {
+ var arr = [
+ new Promise(function(resolve) {
+ setTimeout(resolve.bind(undefined, 1), 50);
+ }),
+ new Promise(function(resolve, reject) {
+ setTimeout(reject.bind(undefined, 2), 10);
+ }),
+ new Promise(function(resolve) {
+ setTimeout(resolve.bind(undefined, 3), 10);
+ }),
+ new Promise(function(resolve) {
+ setTimeout(resolve.bind(undefined, 4), 20);
+ }),
+ ];
+
+ var p = Promise.all(arr);
+ p.then(function(values) {
+ ok(false, "Promise.all shouldn't resolve when iterable has rejected Promises.");
+ runTest();
+ }, function(e) {
+ ok(true, "Promise.all should reject when iterable has rejected Promises.");
+ is(e, 2, "Rejection value should match.");
+ runTest();
+ });
+}
+
+function promiseAllCastError() {
+ var p = Promise.all([Promise.resolve(2), { then() {
+ throw new ReferenceError("placeholder for nonexistent function call");
+ } }]);
+ ok(p instanceof Promise, "Should cast to a Promise.");
+ p.then(function(v) {
+ ok(false, "promiseAllCastError: should've rejected.");
+ runTest();
+ }, function(e) {
+ ok(e instanceof ReferenceError, "promiseCastThenableError");
+ runTest();
+ });
+}
+
+// Check that the resolved array is enumerable.
+function promiseAllEnumerable() {
+ var p = Promise.all([1, new Date(), Promise.resolve("firefox")]);
+ p.then(function(v) {
+ var count = 0;
+ for (let key in v) {
+ ++count;
+ ok(v[key] === 1 || v[key] instanceof Date || v[key] === "firefox",
+ "Enumerated properties don't match.");
+ }
+ is(count, 3, "Resolved array from Promise.all should be enumerable");
+ runTest();
+ }, function(e) {
+ ok(false, "promiseAllEnumerable: should've resolved.");
+ runTest();
+ });
+}
+
+function promiseRaceEmpty() {
+ var p = Promise.race([]);
+ ok(p instanceof Promise, "Should return a Promise.");
+ p.then(function() {
+ ok(false, "Should not resolve");
+ }, function() {
+ ok(false, "Should not reject");
+ });
+ // Per spec, An empty race never resolves or rejects.
+ setTimeout(function() {
+ ok(true);
+ runTest();
+ }, 50);
+}
+
+function promiseRaceValuesArray() {
+ var p = Promise.race([true, new Date(), 3]);
+ ok(p instanceof Promise, "Should return a Promise.");
+ p.then(function(winner) {
+ is(winner, true, "First value should win.");
+ runTest();
+ }, function(err) {
+ ok(false, "Should not fail " + err + ".");
+ runTest();
+ });
+}
+
+function promiseRacePromiseArray() {
+ var arr = [
+ new Promise(function(resolve) {
+ resolve("first");
+ }),
+ Promise.resolve("second"),
+ new Promise(function() {}),
+ new Promise(function(resolve) {
+ setTimeout(function() {
+ setTimeout(function() {
+ resolve("fourth");
+ }, 0);
+ }, 0);
+ }),
+ ];
+
+ var p = Promise.race(arr);
+ p.then(function(winner) {
+ is(winner, "first", "First queued resolution should win the race.");
+ runTest();
+ });
+}
+
+function promiseRaceIterable() {
+ function* participants() {
+ yield new Promise(function(resolve) {
+ setTimeout(resolve, 10, 10);
+ });
+ yield new Promise(function(resolve) {
+ setTimeout(resolve, 20, 20);
+ });
+ }
+
+ Promise.race(participants()).then(function(winner) {
+ is(winner, 10, "Winner should be the one that finished earlier.");
+ runTest();
+ }, function(e) {
+ ok(false, "Promise.race shouldn't throw when an iterable is passed!");
+ runTest();
+ });
+}
+
+function promiseRaceReject() {
+ var p = Promise.race([
+ Promise.reject(new Error("Fail bad!")),
+ new Promise(function(resolve) {
+ setTimeout(resolve, 0);
+ }),
+ ]);
+
+ p.then(function() {
+ ok(false, "Should not resolve when winning Promise rejected.");
+ runTest();
+ }, function(e) {
+ ok(true, "Should be rejected");
+ ok(e instanceof Error, "Should reject with Error.");
+ ok(e.message == "Fail bad!", "Message should match.");
+ runTest();
+ });
+}
+
+function promiseRaceThrow() {
+ var p = Promise.race([
+ new Promise(function(resolve) {
+ throw new ReferenceError("placeholder for nonexistent function call");
+ }),
+ new Promise(function(resolve) {
+ setTimeout(resolve, 0);
+ }),
+ ]);
+
+ p.then(function() {
+ ok(false, "Should not resolve when winning Promise had an error.");
+ runTest();
+ }, function(e) {
+ ok(true, "Should be rejected");
+ ok(e instanceof ReferenceError, "Should reject with ReferenceError for function nonExistent().");
+ runTest();
+ });
+}
+
+var tests = [
+ promiseUtilitiesDefined,
+ promiseAllEmptyArray,
+ promiseAllArray,
+ promiseAllIterable,
+ promiseAllWaitsForAllPromises,
+ promiseAllRejectFails,
+ promiseAllCastError,
+ promiseAllEnumerable,
+
+ promiseRaceEmpty,
+ promiseRaceValuesArray,
+ promiseRacePromiseArray,
+ promiseRaceIterable,
+ promiseRaceReject,
+ promiseRaceThrow,
+ ];
+
+function runTest() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+runTest();
+// -->
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_promise_xrays.html b/dom/promise/tests/test_promise_xrays.html
new file mode 100644
index 0000000000..8559dbb2a4
--- /dev/null
+++ b/dom/promise/tests/test_promise_xrays.html
@@ -0,0 +1,365 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1170760
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1170760</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1170760">Mozilla Bug 1170760</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe id="t" src="http://example.org/chrome/dom/promise/tests/file_promise_xrays.html"></iframe>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+
+var win = $("t").contentWindow;
+
+/** Test for Bug 1170760 **/
+SimpleTest.waitForExplicitFinish();
+
+function testLoadComplete() {
+ is(win.location.href, $("t").src, "Should have loaded the right thing");
+ nextTest();
+}
+
+function testHaveXray() {
+ is(typeof win.Promise.race, "function", "Should see a race() function");
+ var exception;
+ try {
+ win.Promise.wrappedJSObject.race;
+ } catch (e) {
+ exception = e;
+ }
+ is(exception, "Getting race", "Should have thrown the right exception");
+ is(win.wrappedJSObject.setupThrew, false, "Setup should not have thrown");
+ nextTest();
+}
+
+function testConstructor1() {
+ var p = new win.Promise(function(resolve, reject) { resolve(win.Promise.resolve(5)); });
+ p.then(
+ function(arg) {
+ is(arg, 5, "Content Promise constructor resolved with content promise should work");
+ },
+ function(e) {
+ ok(false, "Content Promise constructor resolved with content promise should not fail");
+ }
+ ).then(nextTest);
+}
+
+function testConstructor2() {
+ var p = new win.Promise(function(resolve, reject) { resolve(Promise.resolve(5)); });
+ p.then(
+ function(arg) {
+ is(arg, 5, "Content Promise constructor resolved with chrome promise should work");
+ },
+ function(e) {
+ ok(false, "Content Promise constructor resolved with chrome promise should not fail");
+ }
+ ).then(nextTest);
+}
+
+function testRace1() {
+ var p = win.Promise.race(new win.Array(1, 2));
+ p.then(
+ function(arg) {
+ ok(arg == 1 || arg == 2,
+ "Should get the right value when racing content-side array");
+ },
+ function(e) {
+ ok(false, "testRace1 threw exception: " + e);
+ }
+ ).then(nextTest);
+}
+
+function testRace2() {
+ var p = win.Promise.race(
+ [win.Promise.resolve(1), win.Promise.resolve(2)]);
+ p.then(
+ function(arg) {
+ ok(arg == 1 || arg == 2,
+ "Should get the right value when racing content-side array of explicit Promises");
+ },
+ function(e) {
+ ok(false, "testRace2 threw exception: " + e);
+ }
+ ).then(nextTest);
+}
+
+function testRace3() {
+ // This works with a chrome-side array because we do the iteration
+ // while still in the Xray compartment.
+ var p = win.Promise.race([1, 2]);
+ p.then(
+ function(arg) {
+ ok(arg == 1 || arg == 2,
+ "Should get the right value when racing chrome-side array");
+ },
+ function(e) {
+ ok(false, "testRace3 threw exception: " + e);
+ }
+ ).then(nextTest);
+}
+
+function testRace4() {
+ // This works with both content-side and chrome-side Promises because we want
+ // it to and go to some lengths to make it work.
+ var p = win.Promise.race([Promise.resolve(1), win.Promise.resolve(2)]);
+ p.then(
+ function(arg) {
+ ok(arg == 1 || arg == 2,
+ "Should get the right value when racing chrome-side promises");
+ },
+ function(e) {
+ ok(false, "testRace4 threw exception: " + e);
+ }
+ ).then(nextTest);
+}
+
+function testAll1() {
+ var p = win.Promise.all(new win.Array(1, 2));
+ p.then(
+ function(arg) {
+ ok(arg instanceof win.Array, "Should get an Array from Promise.all (1)");
+ is(arg[0], 1, "First entry of Promise.all return value should be correct (1)");
+ is(arg[1], 2, "Second entry of Promise.all return value should be correct (1)");
+ },
+ function(e) {
+ ok(false, "testAll1 threw exception: " + e);
+ }
+ ).then(nextTest);
+}
+
+function testAll2() {
+ var p = win.Promise.all(
+ [win.Promise.resolve(1), win.Promise.resolve(2)]);
+ p.then(
+ function(arg) {
+ ok(arg instanceof win.Array, "Should get an Array from Promise.all (2)");
+ is(arg[0], 1, "First entry of Promise.all return value should be correct (2)");
+ is(arg[1], 2, "Second entry of Promise.all return value should be correct (2)");
+ },
+ function(e) {
+ ok(false, "testAll2 threw exception: " + e);
+ }
+ ).then(nextTest);
+}
+
+function testAll3() {
+ // This works with a chrome-side array because we do the iteration
+ // while still in the Xray compartment.
+ var p = win.Promise.all([1, 2]);
+ p.then(
+ function(arg) {
+ ok(arg instanceof win.Array, "Should get an Array from Promise.all (3)");
+ is(arg[0], 1, "First entry of Promise.all return value should be correct (3)");
+ is(arg[1], 2, "Second entry of Promise.all return value should be correct (3)");
+ },
+ function(e) {
+ ok(false, "testAll3 threw exception: " + e);
+ }
+ ).then(nextTest);
+}
+
+function testAll4() {
+ // This works with both content-side and chrome-side Promises because we want
+ // it to and go to some lengths to make it work.
+ var p = win.Promise.all([Promise.resolve(1), win.Promise.resolve(2)]);
+ p.then(
+ function(arg) {
+ ok(arg instanceof win.Array, "Should get an Array from Promise.all (4)");
+ is(arg[0], 1, "First entry of Promise.all return value should be correct (4)");
+ is(arg[1], 2, "Second entry of Promise.all return value should be correct (4)");
+ },
+ function(e) {
+ ok(false, "testAll4 threw exception: " + e);
+ }
+ ).then(nextTest);
+}
+
+function testAll5() {
+ var p = win.Promise.all(new win.Array());
+ p.then(
+ function(arg) {
+ ok(arg instanceof win.Array, "Should get an Array from Promise.all (5)");
+ },
+ function(e) {
+ ok(false, "testAll5 threw exception: " + e);
+ }
+ ).then(nextTest);
+}
+
+function testResolve1() {
+ var p = win.Promise.resolve(5);
+ ok(p instanceof win.Promise, "Promise.resolve should return a promise");
+ p.then(
+ function(arg) {
+ is(arg, 5, "Should get correct Promise.resolve value");
+ },
+ function(e) {
+ ok(false, "testAll5 threw exception: " + e);
+ }
+ ).then(nextTest);
+}
+
+function testResolve2() {
+ var p = win.Promise.resolve(5);
+ var q = win.Promise.resolve(p);
+ is(q, p, "Promise.resolve should just pass through Promise values");
+ nextTest();
+}
+
+function testResolve3() {
+ var p = win.Promise.resolve(Promise.resolve(5));
+ p.then(
+ function(arg) {
+ 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) => {});
+ Cu.getJSTestingFunctions().resolvePromise(p, 42);
+ p.then(
+ function(arg) {
+ is(arg, 42, "Resolving an Xray to a promise with TestingFunctions resolvePromise should work");
+ },
+ function(e) {
+ ok(false, "Resolving an Xray to a promise with TestingFunctions resolvePromise 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) => {});
+ Cu.getJSTestingFunctions().rejectPromise(p, 42);
+ p.then(
+ function(arg) {
+ ok(false, "Rejecting an Xray to a promise with TestingFunctions rejectPromise should trigger catch handler");
+ },
+ function(e) {
+ is(e, 42, "Rejecting an Xray to a promise with TestingFunctions rejectPromise 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");
+ },
+ function(e) {
+ ok(false, "Promise.then should not fail");
+ }
+ ).then(nextTest);
+}
+
+function testThen2() {
+ var p = win.Promise.resolve(5);
+ var q = p.then((x) => Promise.resolve(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 resolved with chrome promise should work");
+ },
+ function(e) {
+ ok(false, "Promise.then resolved with chrome promise should not fail");
+ }
+ ).then(nextTest);
+}
+
+function testCatch1() {
+ var p = win.Promise.reject(5);
+ ok(p instanceof win.Promise, "Promise.reject should return a promise");
+ var q = p.catch((x) => x * x);
+ ok(q instanceof win.Promise,
+ "Promise.catch should return a promise from the right global");
+ q.then(
+ function(arg) {
+ is(arg, 25, "Promise.catch should work");
+ },
+ function(e) {
+ ok(false, "Promise.catch should not fail");
+ }
+ ).then(nextTest);
+}
+
+function testToStringTag1() {
+ is(win.Promise.prototype[Symbol.toStringTag], "Promise", "@@toStringTag was incorrect");
+ var p = win.Promise.resolve();
+ is(String(p), "[object Promise]", "String() result was incorrect");
+ is(p.toString(), "[object Promise]", "toString result was incorrect");
+ is(Object.prototype.toString.call(p), "[object Promise]", "second toString result was incorrect");
+ nextTest();
+}
+
+var tests = [
+ testLoadComplete,
+ testHaveXray,
+ testConstructor1,
+ testConstructor2,
+ testRace1,
+ testRace2,
+ testRace3,
+ testRace4,
+ testAll1,
+ testAll2,
+ testAll3,
+ testAll4,
+ testAll5,
+ testResolve1,
+ testResolve2,
+ testResolve3,
+ testResolve4,
+ testReject1,
+ testReject2,
+ testThen1,
+ testThen2,
+ testCatch1,
+ testToStringTag1,
+];
+
+function nextTest() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ tests.shift()();
+}
+
+addLoadEvent(nextTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_resolve.html b/dom/promise/tests/test_resolve.html
new file mode 100644
index 0000000000..7e4745b47a
--- /dev/null
+++ b/dom/promise/tests/test_resolve.html
@@ -0,0 +1,66 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>Promise.resolve(anything) Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript"><!--
+
+var tests = [
+ null,
+ 42,
+ "hello world",
+ true,
+ false,
+ {},
+ { a: 42 },
+ [ 1, 2, 3, 4, null, true, "hello world" ],
+ function() {},
+ window,
+ undefined,
+ document.createElement("input"),
+ new Date(),
+];
+
+function cbError() {
+ ok(false, "Nothing should arrive here!");
+}
+
+function runTest() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.pop();
+
+ new Promise(function(resolve, reject) {
+ resolve(test);
+ }).then(function(what) {
+ ok(test === what, "What is: " + what);
+ }, cbError).then(function() {
+ new Promise(function(resolve, reject) {
+ reject(test);
+ }).then(cbError, function(what) {
+ ok(test === what, "What is: " + what);
+ }).then(runTest, cbError);
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+runTest();
+// -->
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_resolver_return_value.html b/dom/promise/tests/test_resolver_return_value.html
new file mode 100644
index 0000000000..82e8793824
--- /dev/null
+++ b/dom/promise/tests/test_resolver_return_value.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1120235
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1120235</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1120235 **/
+ var res, rej;
+ var p = new Promise(function(resolve, reject) { res = resolve; rej = reject; });
+ is(res(1), undefined, "Resolve function should return undefined");
+ is(rej(2), undefined, "Reject function should return undefined");
+
+ var thenable = {
+ then(resolve, reject) {
+ is(resolve(3), undefined, "Thenable resolve argument should return undefined");
+ is(reject(4), undefined, "Thenable reject argument should return undefined");
+ SimpleTest.finish();
+ },
+ };
+
+ SimpleTest.waitForExplicitFinish();
+ p = Promise.resolve(thenable);
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1120235">Mozilla Bug 1120235</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/promise/tests/test_species_getter.html b/dom/promise/tests/test_species_getter.html
new file mode 100644
index 0000000000..23cb7d8ae0
--- /dev/null
+++ b/dom/promise/tests/test_species_getter.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test for ...</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ /* global test, assert_not_equals, assert_equals */
+
+ var desc = Object.getOwnPropertyDescriptor(Promise, Symbol.species);
+ assert_not_equals(desc, undefined, "Should have a property");
+
+ assert_equals(desc.configurable, true, "Property should be configurable");
+ assert_equals(desc.enumerable, false, "Property should not be enumerable");
+ assert_equals(desc.set, undefined, "Should not have a setter");
+ var getter = desc.get;
+
+ var things = [undefined, null, 5, "xyz", Promise, Object];
+ for (var thing of things) {
+ assert_equals(getter.call(thing), thing,
+ "Getter should return its this value");
+ }
+}, "Promise should have an @@species getter that works per spec");
+</script>
diff --git a/dom/promise/tests/test_thenable_vs_promise_ordering.html b/dom/promise/tests/test_thenable_vs_promise_ordering.html
new file mode 100644
index 0000000000..a537490b16
--- /dev/null
+++ b/dom/promise/tests/test_thenable_vs_promise_ordering.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test for promise resolution ordering with thenables and promises</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+/* global async_test, assert_true, assert_equals */
+
+var t = async_test("A promise resolved first (with a thenable) should trigger its callbacks before a promise resolved second (with a promise).");
+t.step(function() {
+ var customThenCalled = false;
+ var p0 = Promise.resolve();
+ p0.then = function(resolved, rejected) {
+ customThenCalled = true;
+ Promise.prototype.then.call(this, resolved, rejected);
+ };
+ var p1 = new Promise(function(r) { r(p0); });
+ delete p0.then;
+ var p2 = new Promise(function(r) { r(p0); });
+ var resolutionOrder = "";
+ Promise.all([ p1.then(function() { resolutionOrder += "1"; }),
+ p2.then(function() { resolutionOrder += "2"; }) ])
+ .then(t.step_func_done(function() {
+ assert_true(customThenCalled, "Should have called custom then");
+ assert_equals(resolutionOrder, "12");
+ }));
+});
+</script>
diff --git a/dom/promise/tests/test_webassembly_compile.html b/dom/promise/tests/test_webassembly_compile.html
new file mode 100644
index 0000000000..351f0f4ae4
--- /dev/null
+++ b/dom/promise/tests/test_webassembly_compile.html
@@ -0,0 +1,446 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>WebAssembly.compile Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script>
+const testingFunctions = SpecialPowers.Cu.getJSTestingFunctions();
+const wasmIsSupported = SpecialPowers.unwrap(testingFunctions.wasmIsSupported);
+const wasmHasTier2CompilationCompleted = SpecialPowers.unwrap(testingFunctions.wasmHasTier2CompilationCompleted);
+const wasmLoadedFromCache = SpecialPowers.unwrap(testingFunctions.wasmLoadedFromCache);
+const isCachingEnabled = SpecialPowers.getBoolPref("javascript.options.wasm_caching");
+
+// The test_webassembly_compile_sample.wasm is a medium-sized module with 100
+// functions that call each other recursively, returning a computed sum.
+// Any other non-trivial module could be generated and used.
+var sampleCode;
+const sampleURL = "test_webassembly_compile_sample.wasm";
+const sampleFileSize = 16053;
+const sampleURLWithRandomQuery = () => sampleURL + "?id=" + String(Math.ceil(Math.random()*100000));
+const sampleExportName = "run";
+const sampleResult = 1275;
+
+function checkSampleModule(m) {
+ ok(m instanceof WebAssembly.Module, "got a module");
+ var i = new WebAssembly.Instance(m);
+ ok(i instanceof WebAssembly.Instance, "got an instance");
+ ok(i.exports[sampleExportName]() === sampleResult, "got result");
+}
+
+function checkSampleInstance(i) {
+ ok(i instanceof WebAssembly.Instance, "got a module");
+ ok(i.exports[sampleExportName]() === sampleResult, "got result");
+}
+
+function fetchSampleModuleCode() {
+ fetch(sampleURL)
+ .then(response => response.arrayBuffer())
+ .then(buffer => { sampleCode = buffer; runTest(); })
+ .catch(err => ok(false, String(err)));
+}
+
+function propertiesExist() {
+ if (!wasmIsSupported()) {
+ ok(!this.WebAssembly, "If the device doesn't support, there will be no WebAssembly object");
+ SimpleTest.finish();
+ return;
+ }
+
+ ok(WebAssembly, "WebAssembly object should exist");
+ ok(WebAssembly.compile, "WebAssembly.compile function should exist");
+ runTest();
+}
+
+function compileFail() {
+ WebAssembly.compile().then(
+ () => { ok(false, "should have failed"); runTest(); }
+ ).catch(
+ err => { ok(err instanceof TypeError, "empty compile failed"); runTest(); }
+ );
+}
+
+function compileSuccess() {
+ WebAssembly.compile(sampleCode).then(
+ m => { checkSampleModule(m); runTest(); }
+ ).catch(
+ err => { ok(false, String(err)); runTest(); }
+ );
+}
+
+function compileManySuccess() {
+ const N = 100;
+
+ var arr = [];
+ for (var i = 0; i < N; i++)
+ arr.push(WebAssembly.compile(sampleCode));
+
+ SpecialPowers.gc();
+
+ Promise.all(arr).then(ms => {
+ ok(ms.length === N, "got the right number");
+ for (var j = 0; j < N; j++)
+ checkSampleModule(ms[j]);
+ runTest();
+ }).catch(
+ err => { ok(false, String(err)); runTest(); }
+ );
+}
+
+function terminateCompileInWorker() {
+ var w = new Worker(`data:text/plain,
+ var sampleCode;
+ function spawnWork() {
+ const N = 100;
+ var arr = [];
+ for (var i = 0; i < N; i++)
+ arr.push(WebAssembly.compile(sampleCode));
+ Promise.all(arr).then(spawnWork);
+ }
+ onmessage = e => {
+ sampleCode = e.data;
+ spawnWork();
+ postMessage("ok");
+ }
+ `);
+ w.postMessage(sampleCode);
+ w.onmessage = e => {
+ ok(e.data === "ok", "worker finished first step");
+ w.terminate();
+ runTest();
+ };
+}
+
+function instantiateFail() {
+ WebAssembly.instantiate().then(
+ () => { ok(false, "should have failed"); runTest(); }
+ ).catch(
+ err => { ok(err instanceof TypeError, "empty compile failed"); runTest(); }
+ );
+}
+
+function instantiateSuccess() {
+ WebAssembly.instantiate(sampleCode).then(
+ r => { checkSampleModule(r.module); checkSampleInstance(r.instance); runTest(); }
+ ).catch(
+ err => { ok(false, String(err)); runTest(); }
+ );
+}
+
+function chainSuccess() {
+ WebAssembly.compile(sampleCode).then(
+ m => WebAssembly.instantiate(m)
+ ).then(
+ i => { checkSampleInstance(i); runTest(); }
+ ).catch(
+ err => { ok(false, String(err)); runTest(); }
+ );
+}
+
+function compileStreamingNonResponse() {
+ WebAssembly.compileStreaming({})
+ .then(() => { ok(false); })
+ .catch(err => { ok(err instanceof TypeError, "rejected {}"); runTest(); });
+}
+
+function compileStreamingNoMime() {
+ WebAssembly.compileStreaming(new Response(new ArrayBuffer()))
+ .then(() => { ok(false); })
+ .catch(err => { ok(err instanceof TypeError, "rejected no MIME type"); runTest(); });
+}
+
+function compileStreamingBadMime() {
+ var badMimes = [
+ "",
+ "application/js",
+ "application/js;application/wasm",
+ "application/wasm;application/js",
+ "application/wasm;",
+ "application/wasm1",
+ ];
+ var promises = [];
+ for (let mimeType of badMimes) {
+ var init = { headers: { "Content-Type": mimeType } };
+ promises.push(
+ WebAssembly.compileStreaming(new Response(sampleCode, init))
+ .then(() => Promise.reject(), err => {
+ is(err.message,
+ `WebAssembly: Response has unsupported MIME type '${mimeType}' expected 'application/wasm'`,
+ "correct MIME type error message");
+ return Promise.resolve();
+ })
+ );
+ }
+ Promise.all(promises)
+ .then(() => { ok(true, "all bad MIME types rejected"); runTest(); });
+}
+
+function compileStreamingGoodMime() {
+ var badMimes = [
+ "application/wasm",
+ " application/wasm ",
+ "application/wasm ",
+ ];
+ var promises = [];
+ for (let mimeType of badMimes) {
+ var init = { headers: { "Content-Type": mimeType } };
+ promises.push(
+ WebAssembly.compileStreaming(new Response(sampleCode, init))
+ );
+ }
+ Promise.all(promises)
+ .then(() => { ok(true, "all good MIME types accepted"); runTest(); });
+}
+
+function compileStreamingDoubleUseFail() {
+ fetch(sampleURL)
+ .then(response => {
+ WebAssembly.compileStreaming(response)
+ .then(m => {
+ checkSampleModule(m);
+ return WebAssembly.compileStreaming(response);
+ })
+ .then(
+ () => ok(false, "should have failed on second use"),
+ err => { ok(true, "failed on second use"); runTest(); }
+ );
+ });
+}
+
+function compileStreamingNullBody() {
+ var init = { headers: { "Content-Type": "application/wasm" } };
+ WebAssembly.compileStreaming(new Response(undefined, init))
+ .then(() => { ok(false); })
+ .catch(err => { ok(err instanceof WebAssembly.CompileError, "null body"); runTest(); });
+}
+
+function compileStreamingFetch() {
+ WebAssembly.compileStreaming(fetch(sampleURL))
+ .then(m => { checkSampleModule(m); runTest(); })
+ .catch(err => { ok(false, String(err)); });
+}
+
+function compileCachedBasic() {
+ const url = sampleURLWithRandomQuery();
+ WebAssembly.compileStreaming(fetch(url))
+ .then(module => {
+ checkSampleModule(module);
+ ok(!wasmLoadedFromCache(module), "not cached yet");
+ while(!wasmHasTier2CompilationCompleted(module));
+ return WebAssembly.compileStreaming(fetch(url));
+ })
+ .then(module => {
+ checkSampleModule(module);
+ ok(wasmLoadedFromCache(module), "loaded from cache");
+ })
+ .then(() => runTest())
+ .catch(err => { ok(false, String(err)) });
+}
+
+function compileCachedCompressed() {
+ const url = sampleURLWithRandomQuery();
+
+ // It is a rough estimate that compilation code is about
+ // 2-4 times of the wasm file size. After it compression
+ // it will be less (about 60% ?)
+ const EstimatedCompilationArtifactSize = 2 * sampleFileSize;
+ const EstimatedCompressedArtifactSize = 0.6 * EstimatedCompilationArtifactSize;
+
+ // Set limit on cache entry so it will fail if it is not
+ // compressed.
+ const cleanup = () => {
+ SpecialPowers.clearUserPref("browser.cache.disk.max_entry_size")
+ };
+ Promise.resolve(SpecialPowers.setIntPref("browser.cache.disk.max_entry_size",
+ Math.round(EstimatedCompressedArtifactSize / 1024) /* kb */))
+ .then(() => WebAssembly.compileStreaming(fetch(url)))
+ .then(module => {
+ checkSampleModule(module);
+ ok(!wasmLoadedFromCache(module), "not cached yet");
+ while(!wasmHasTier2CompilationCompleted(module));
+ return WebAssembly.compileStreaming(fetch(url));
+ })
+ .then(module => {
+ checkSampleModule(module);
+ ok(wasmLoadedFromCache(module), "loaded from cache");
+ })
+ .then(() => { cleanup(); runTest() })
+ .catch(err => { cleanup(); ok(false, String(err)) });
+}
+
+function compileCachedTooLargeForCache() {
+ const url = sampleURLWithRandomQuery();
+ // Set unreasonable limit, caching will fail.
+ // Bug 1719508 can change name of pref, this and
+ // compileCachedCompressed tests will become invalid.
+ const cleanup = () => {
+ SpecialPowers.clearUserPref("browser.cache.disk.max_entry_size")
+ };
+ Promise.resolve(SpecialPowers.setIntPref("browser.cache.disk.max_entry_size", 1 /* kb */))
+ .then(() => WebAssembly.compileStreaming(fetch(url)))
+ .then(module => {
+ console.log(module)
+ checkSampleModule(module);
+ ok(!wasmLoadedFromCache(module), "not cached yet");
+ while(!wasmHasTier2CompilationCompleted(module));
+ return WebAssembly.compileStreaming(fetch(url));
+ })
+ .then(module => {
+ checkSampleModule(module);
+ ok(!wasmLoadedFromCache(module), "not cached (size limit)");
+ })
+ .then(() => { cleanup(); runTest() })
+ .catch(err => { cleanup(); ok(false, String(err)) });
+}
+
+const Original = "original";
+const Clone = "clone";
+
+function compileCachedBothClonesHitCache(which) {
+ const url = sampleURLWithRandomQuery();
+ WebAssembly.compileStreaming(fetch(url))
+ .then(module => {
+ checkSampleModule(module);
+ ok(!wasmLoadedFromCache(module), "not cached yet");
+ while(!wasmHasTier2CompilationCompleted(module));
+ return fetch(url);
+ })
+ .then(original => {
+ let clone = original.clone();
+ if (which === Clone) [clone, original] = [original, clone];
+ return Promise.all([
+ WebAssembly.compileStreaming(original),
+ WebAssembly.compileStreaming(clone)
+ ]);
+ })
+ .then(([m1, m2]) => {
+ checkSampleModule(m1);
+ ok(wasmLoadedFromCache(m1), "clone loaded from cache");
+ checkSampleModule(m2);
+ ok(wasmLoadedFromCache(m2), "original loaded from cache");
+ })
+ .then(() => runTest())
+ .catch(err => { ok(false, String(err)) });
+}
+
+function compileCachedCacheThroughClone(which) {
+ const url = sampleURLWithRandomQuery();
+ fetch(url)
+ .then(original => {
+ ok(true, "fun time");
+ let clone = original.clone();
+ if (which === Clone) [clone, original] = [original, clone];
+ return Promise.all([
+ WebAssembly.compileStreaming(original),
+ clone.arrayBuffer()
+ ]);
+ })
+ .then(([module, buffer]) => {
+ ok(!wasmLoadedFromCache(module), "not cached yet");
+ ok(buffer instanceof ArrayBuffer);
+ while(!wasmHasTier2CompilationCompleted(module));
+ return WebAssembly.compileStreaming(fetch(url));
+ })
+ .then(m => {
+ ok(wasmLoadedFromCache(m), "cache hit of " + which);
+ })
+ .then(() => runTest())
+ .catch(err => { ok(false, String(err)) });
+}
+
+function instantiateStreamingFetch() {
+ WebAssembly.instantiateStreaming(fetch(sampleURL))
+ .then(({module, instance}) => { checkSampleModule(module); checkSampleInstance(instance); runTest(); })
+ .catch(err => { ok(false, String(err)); });
+}
+
+function compileManyStreamingFetch() {
+ const N = 20;
+
+ var arr = [];
+ for (var i = 0; i < N; i++)
+ arr.push(WebAssembly.compileStreaming(fetch(sampleURL)));
+
+ SpecialPowers.gc();
+
+ Promise.all(arr).then(ms => {
+ ok(ms.length === N, "got the right number");
+ for (var j = 0; j < N; j++)
+ checkSampleModule(ms[j]);
+ runTest();
+ }).catch(
+ err => { ok(false, String(err)); runTest(); }
+ );
+}
+
+function runWorkerTests() {
+ var w = new Worker("test_webassembly_compile_worker.js");
+ w.postMessage(sampleCode);
+ w.onmessage = e => {
+ ok(e.data === "ok", "worker test: " + e.data);
+ runTest();
+ };
+}
+
+function terminateCompileStreamingInWorker() {
+ var w = new Worker("test_webassembly_compile_worker_terminate.js");
+ w.onmessage = e => {
+ ok(e.data === "ok", "worker streaming terminate test: " + e.data);
+ w.terminate();
+ runTest();
+ };
+}
+
+var tests = [ propertiesExist,
+ compileFail,
+ compileSuccess,
+ compileManySuccess,
+ terminateCompileInWorker,
+ instantiateFail,
+ instantiateSuccess,
+ chainSuccess,
+ compileStreamingNonResponse,
+ compileStreamingNoMime,
+ compileStreamingBadMime,
+ compileStreamingGoodMime,
+ compileStreamingDoubleUseFail,
+ compileStreamingNullBody,
+ compileStreamingFetch,
+ ...(isCachingEnabled ? [
+ compileCachedBasic,
+ compileCachedCompressed,
+ compileCachedTooLargeForCache,
+ compileCachedBothClonesHitCache.bind(Original),
+ compileCachedBothClonesHitCache.bind(Clone),
+ compileCachedCacheThroughClone.bind(Original),
+ compileCachedCacheThroughClone.bind(Clone),
+ ]: []),
+ instantiateStreamingFetch,
+ compileManyStreamingFetch,
+ runWorkerTests,
+ terminateCompileStreamingInWorker,
+ ];
+
+// This initialization must always run
+tests.unshift(fetchSampleModuleCode);
+
+function runTest() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+SimpleTest.waitForExplicitFinish();
+runTest();
+</script>
+</body>
+</html>
diff --git a/dom/promise/tests/test_webassembly_compile_sample.wasm b/dom/promise/tests/test_webassembly_compile_sample.wasm
new file mode 100644
index 0000000000..787e19a5df
--- /dev/null
+++ b/dom/promise/tests/test_webassembly_compile_sample.wasm
Binary files differ
diff --git a/dom/promise/tests/test_webassembly_compile_worker.js b/dom/promise/tests/test_webassembly_compile_worker.js
new file mode 100644
index 0000000000..90c3551137
--- /dev/null
+++ b/dom/promise/tests/test_webassembly_compile_worker.js
@@ -0,0 +1,55 @@
+const sampleURL = "test_webassembly_compile_sample.wasm";
+const sampleExportName = "run";
+const sampleResult = 1275;
+
+/* eslint-disable no-throw-literal */
+
+function checkSampleModule(m) {
+ if (!(m instanceof WebAssembly.Module)) {
+ throw "not a module";
+ }
+ var i = new WebAssembly.Instance(m);
+ if (!(i instanceof WebAssembly.Instance)) {
+ throw "not an instance";
+ }
+ if (i.exports[sampleExportName]() !== sampleResult) {
+ throw "wrong result";
+ }
+}
+
+function checkSampleInstance(i) {
+ if (!(i instanceof WebAssembly.Instance)) {
+ throw "not an instance";
+ }
+ if (i.exports[sampleExportName]() !== sampleResult) {
+ throw "wrong result";
+ }
+}
+
+const initObj = { headers: { "Content-Type": "application/wasm" } };
+
+onmessage = e => {
+ WebAssembly.compile(e.data)
+ .then(m => checkSampleModule(m))
+ .then(() => WebAssembly.instantiate(e.data))
+ .then(({ module, instance }) => {
+ checkSampleModule(module);
+ checkSampleInstance(instance);
+ })
+ .then(() => WebAssembly.compileStreaming(new Response(e.data, initObj)))
+ .then(m => checkSampleModule(m))
+ .then(() => WebAssembly.instantiateStreaming(new Response(e.data, initObj)))
+ .then(({ module, instance }) => {
+ checkSampleModule(module);
+ checkSampleInstance(instance);
+ })
+ .then(() => WebAssembly.compileStreaming(fetch(sampleURL)))
+ .then(m => checkSampleModule(m))
+ .then(() => WebAssembly.instantiateStreaming(fetch(sampleURL)))
+ .then(({ module, instance }) => {
+ checkSampleModule(module);
+ checkSampleInstance(instance);
+ })
+ .then(() => postMessage("ok"))
+ .catch(err => postMessage("fail: " + err));
+};
diff --git a/dom/promise/tests/test_webassembly_compile_worker_terminate.js b/dom/promise/tests/test_webassembly_compile_worker_terminate.js
new file mode 100644
index 0000000000..5b96c9034b
--- /dev/null
+++ b/dom/promise/tests/test_webassembly_compile_worker_terminate.js
@@ -0,0 +1,13 @@
+const sampleURL = "test_webassembly_compile_sample.wasm";
+
+function spawnWork() {
+ const N = 50;
+ var arr = [];
+ for (var i = 0; i < N; i++) {
+ arr.push(WebAssembly.compileStreaming(fetch(sampleURL)));
+ }
+ Promise.all(arr).then(spawnWork);
+}
+
+spawnWork();
+postMessage("ok");
diff --git a/dom/promise/tests/unit/test_monitor_uncaught.js b/dom/promise/tests/unit/test_monitor_uncaught.js
new file mode 100644
index 0000000000..afa328cb97
--- /dev/null
+++ b/dom/promise/tests/unit/test_monitor_uncaught.js
@@ -0,0 +1,322 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+"use strict";
+
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+
+// Prevent test failures due to the unhandled rejections in this test file.
+PromiseTestUtils.disableUncaughtRejectionObserverForSelfTest();
+
+add_task(async function test_globals() {
+ Assert.notEqual(
+ PromiseDebugging,
+ undefined,
+ "PromiseDebugging is available."
+ );
+});
+
+add_task(async function test_promiseID() {
+ let p1 = new Promise(resolve => {});
+ let p2 = new Promise(resolve => {});
+ let p3 = p2.catch(null);
+ let promise = [p1, p2, p3];
+
+ let identifiers = promise.map(PromiseDebugging.getPromiseID);
+ info("Identifiers: " + JSON.stringify(identifiers));
+ let idSet = new Set(identifiers);
+ Assert.equal(
+ idSet.size,
+ identifiers.length,
+ "PromiseDebugging.getPromiseID returns a distinct id per promise"
+ );
+
+ let identifiers2 = promise.map(PromiseDebugging.getPromiseID);
+ Assert.equal(
+ JSON.stringify(identifiers),
+ JSON.stringify(identifiers2),
+ "Successive calls to PromiseDebugging.getPromiseID return the same id for the same promise"
+ );
+});
+
+add_task(async function test_observe_uncaught() {
+ // The names of Promise instances
+ let names = new Map();
+
+ // The results for UncaughtPromiseObserver callbacks.
+ let CallbackResults = function (name) {
+ this.name = name;
+ this.expected = new Set();
+ this.observed = new Set();
+ this.blocker = new Promise(resolve => (this.resolve = resolve));
+ };
+ CallbackResults.prototype = {
+ observe(promise) {
+ info(this.name + " observing Promise " + names.get(promise));
+ Assert.equal(
+ PromiseDebugging.getState(promise).state,
+ "rejected",
+ this.name + " observed a rejected Promise"
+ );
+ if (!this.expected.has(promise)) {
+ Assert.ok(
+ false,
+ this.name +
+ " observed a Promise that it expected to observe, " +
+ names.get(promise) +
+ " (" +
+ PromiseDebugging.getPromiseID(promise) +
+ ", " +
+ PromiseDebugging.getAllocationStack(promise) +
+ ")"
+ );
+ }
+ Assert.ok(
+ this.expected.delete(promise),
+ this.name +
+ " observed a Promise that it expected to observe, " +
+ names.get(promise) +
+ " (" +
+ PromiseDebugging.getPromiseID(promise) +
+ ")"
+ );
+ Assert.ok(
+ !this.observed.has(promise),
+ this.name + " observed a Promise that it has not observed yet"
+ );
+ this.observed.add(promise);
+ if (this.expected.size == 0) {
+ this.resolve();
+ } else {
+ info(
+ this.name +
+ " is still waiting for " +
+ this.expected.size +
+ " observations:"
+ );
+ info(
+ JSON.stringify(Array.from(this.expected.values(), x => names.get(x)))
+ );
+ }
+ },
+ };
+
+ let onLeftUncaught = new CallbackResults("onLeftUncaught");
+ let onConsumed = new CallbackResults("onConsumed");
+
+ let observer = {
+ onLeftUncaught(promise, data) {
+ onLeftUncaught.observe(promise);
+ },
+ onConsumed(promise) {
+ onConsumed.observe(promise);
+ },
+ };
+
+ let resolveLater = function (delay = 20) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ return new Promise((resolve, reject) => setTimeout(resolve, delay));
+ };
+ let rejectLater = function (delay = 20) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ return new Promise((resolve, reject) => setTimeout(reject, delay));
+ };
+ let makeSamples = function* () {
+ yield {
+ promise: Promise.resolve(0),
+ name: "Promise.resolve",
+ };
+ yield {
+ promise: Promise.resolve(resolve => resolve(0)),
+ name: "Resolution callback",
+ };
+ yield {
+ promise: Promise.resolve(0).catch(null),
+ name: "`catch(null)`",
+ };
+ yield {
+ promise: Promise.reject(0).catch(() => {}),
+ name: "Reject and catch immediately",
+ };
+ yield {
+ promise: resolveLater(),
+ name: "Resolve later",
+ };
+ yield {
+ promise: Promise.reject("Simple rejection"),
+ leftUncaught: true,
+ consumed: false,
+ name: "Promise.reject",
+ };
+
+ // Reject a promise now, consume it later.
+ let p = Promise.reject("Reject now, consume later");
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(
+ () =>
+ p.catch(() => {
+ info("Consumed promise");
+ }),
+ 200
+ );
+ yield {
+ promise: p,
+ leftUncaught: true,
+ consumed: true,
+ name: "Reject now, consume later",
+ };
+
+ yield {
+ promise: Promise.all([Promise.resolve("Promise.all"), rejectLater()]),
+ leftUncaught: true,
+ name: "Rejecting through Promise.all",
+ };
+ yield {
+ promise: Promise.race([resolveLater(500), Promise.reject()]),
+ leftUncaught: true, // The rejection wins the race.
+ name: "Rejecting through Promise.race",
+ };
+ yield {
+ promise: Promise.race([Promise.resolve(), rejectLater(500)]),
+ leftUncaught: false, // The resolution wins the race.
+ name: "Resolving through Promise.race",
+ };
+
+ let boom = new Error("`throw` in the constructor");
+ yield {
+ promise: new Promise(() => {
+ throw boom;
+ }),
+ leftUncaught: true,
+ name: "Throwing in the constructor",
+ };
+
+ let rejection = Promise.reject("`reject` during resolution");
+ yield {
+ promise: rejection,
+ leftUncaught: false,
+ consumed: false, // `rejection` is consumed immediately (see below)
+ name: "Promise.reject, again",
+ };
+
+ yield {
+ promise: new Promise(resolve => resolve(rejection)),
+ leftUncaught: true,
+ consumed: false,
+ name: "Resolving with a rejected promise",
+ };
+
+ yield {
+ promise: Promise.resolve(0).then(() => rejection),
+ leftUncaught: true,
+ consumed: false,
+ name: "Returning a rejected promise from success handler",
+ };
+
+ yield {
+ promise: Promise.resolve(0).then(() => {
+ throw new Error();
+ }),
+ leftUncaught: true,
+ consumed: false,
+ name: "Throwing during the call to the success callback",
+ };
+ };
+ let samples = [];
+ for (let s of makeSamples()) {
+ samples.push(s);
+ info(
+ "Promise '" +
+ s.name +
+ "' has id " +
+ PromiseDebugging.getPromiseID(s.promise)
+ );
+ }
+
+ PromiseDebugging.addUncaughtRejectionObserver(observer);
+
+ for (let s of samples) {
+ names.set(s.promise, s.name);
+ if (s.leftUncaught || false) {
+ onLeftUncaught.expected.add(s.promise);
+ }
+ if (s.consumed || false) {
+ onConsumed.expected.add(s.promise);
+ }
+ }
+
+ info("Test setup, waiting for callbacks.");
+ await onLeftUncaught.blocker;
+
+ info("All calls to onLeftUncaught are complete.");
+ if (onConsumed.expected.size != 0) {
+ info("onConsumed is still waiting for the following Promise:");
+ info(
+ JSON.stringify(
+ Array.from(onConsumed.expected.values(), x => names.get(x))
+ )
+ );
+ await onConsumed.blocker;
+ }
+
+ info("All calls to onConsumed are complete.");
+ let removed = PromiseDebugging.removeUncaughtRejectionObserver(observer);
+ Assert.ok(removed, "removeUncaughtRejectionObserver succeeded");
+ removed = PromiseDebugging.removeUncaughtRejectionObserver(observer);
+ Assert.ok(
+ !removed,
+ "second call to removeUncaughtRejectionObserver didn't remove anything"
+ );
+});
+
+add_task(async function test_uninstall_observer() {
+ let Observer = function () {
+ this.blocker = new Promise(resolve => (this.resolve = resolve));
+ this.active = true;
+ };
+ Observer.prototype = {
+ set active(x) {
+ this._active = x;
+ if (x) {
+ PromiseDebugging.addUncaughtRejectionObserver(this);
+ } else {
+ PromiseDebugging.removeUncaughtRejectionObserver(this);
+ }
+ },
+ onLeftUncaught() {
+ Assert.ok(this._active, "This observer is active.");
+ this.resolve();
+ },
+ onConsumed() {
+ Assert.ok(false, "We should not consume any Promise.");
+ },
+ };
+
+ info("Adding an observer.");
+ let deactivate = new Observer();
+ Promise.reject("I am an uncaught rejection.");
+ await deactivate.blocker;
+ Assert.ok(true, "The observer has observed an uncaught Promise.");
+ deactivate.active = false;
+ info(
+ "Removing the observer, it should not observe any further uncaught Promise."
+ );
+
+ info(
+ "Rejecting a Promise and waiting a little to give a chance to observers."
+ );
+ let wait = new Observer();
+ Promise.reject("I am another uncaught rejection.");
+ await wait.blocker;
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 100));
+ // Normally, `deactivate` should not be notified of the uncaught rejection.
+ wait.active = false;
+});
diff --git a/dom/promise/tests/unit/test_promise_job_across_sandbox.js b/dom/promise/tests/unit/test_promise_job_across_sandbox.js
new file mode 100644
index 0000000000..ff1d1575e3
--- /dev/null
+++ b/dom/promise/tests/unit/test_promise_job_across_sandbox.js
@@ -0,0 +1,221 @@
+function createSandbox() {
+ const uri = Services.io.newURI("https://example.com");
+ const principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ return new Cu.Sandbox(principal, {});
+}
+
+add_task(async function testReactionJob() {
+ const sandbox = createSandbox();
+
+ sandbox.eval(`
+var testPromise = Promise.resolve(10);
+`);
+
+ // Calling `Promise.prototype.then` from sandbox performs GetFunctionRealm
+ // on wrapped `resolve` in sandbox realm, and it fails to unwrap the security
+ // wrapper. The reaction job should be created with sandbox realm.
+ const p = new Promise(resolve => {
+ sandbox.resolve = resolve;
+
+ sandbox.eval(`
+testPromise.then(resolve);
+`);
+ });
+
+ const result = await p;
+
+ equal(result, 10);
+});
+
+add_task(async function testReactionJobNuked() {
+ const sandbox = createSandbox();
+
+ sandbox.eval(`
+var testPromise = Promise.resolve(10);
+`);
+
+ // Calling `Promise.prototype.then` from sandbox performs GetFunctionRealm
+ // on wrapped `resolve` in sandbox realm, and it fails to unwrap the security
+ // wrapper. The reaction job should be created with sandbox realm.
+ const p1 = new Promise(resolve => {
+ sandbox.resolve = resolve;
+
+ sandbox.eval(`
+testPromise.then(resolve);
+`);
+
+ // Given the reaction job is created with the sandbox realm, nuking the
+ // sandbox prevents the job gets executed.
+ Cu.nukeSandbox(sandbox);
+ });
+
+ const p2 = Promise.resolve(11);
+
+ // Given the p1 doesn't get resolved, p2 should win.
+ const result = await Promise.race([p1, p2]);
+
+ equal(result, 11);
+});
+
+add_task(async function testReactionJobWithXray() {
+ const sandbox = createSandbox();
+
+ sandbox.eval(`
+var testPromise = Promise.resolve(10);
+`);
+
+ // Calling `Promise.prototype.then` from privileged realm via Xray uses
+ // privileged `Promise.prototype.then` function, and GetFunctionRealm
+ // performed there successfully gets top-level realm. The reaction job
+ // should be created with top-level realm.
+ const result = await new Promise(resolve => {
+ sandbox.testPromise.then(resolve);
+
+ // Given the reaction job is created with the top-level realm, nuking the
+ // sandbox doesn't affect the reaction job.
+ Cu.nukeSandbox(sandbox);
+ });
+
+ equal(result, 10);
+});
+
+add_task(async function testBoundReactionJob() {
+ const sandbox = createSandbox();
+
+ sandbox.eval(`
+var resolve = undefined;
+var callbackPromise = new Promise(r => { resolve = r; });
+var callback = function (v) { resolve(v + 1); };
+`);
+
+ // Create a bound function where its realm is privileged realm, and
+ // its target is from sandbox realm.
+ sandbox.bound_callback = Function.prototype.bind.call(
+ sandbox.callback,
+ sandbox
+ );
+
+ // Calling `Promise.prototype.then` from sandbox performs GetFunctionRealm
+ // and it fails. The reaction job should be created with sandbox realm.
+ sandbox.eval(`
+Promise.resolve(10).then(bound_callback);
+`);
+
+ const result = await sandbox.callbackPromise;
+ equal(result, 11);
+});
+
+add_task(async function testThenableJob() {
+ const sandbox = createSandbox();
+
+ const p = new Promise(resolve => {
+ // Create a bound function where its realm is privileged realm, and
+ // its target is from sandbox realm.
+ sandbox.then = function (onFulfilled, onRejected) {
+ resolve(10);
+ };
+ });
+
+ // Creating a promise thenable job in the following `Promise.resolve` performs
+ // GetFunctionRealm on the bound thenable.then and fails. The reaction job
+ // should be created with sandbox realm.
+ sandbox.eval(`
+var thenable = {
+ then: then,
+};
+
+Promise.resolve(thenable);
+`);
+
+ const result = await p;
+ equal(result, 10);
+});
+
+add_task(async function testThenableJobNuked() {
+ const sandbox = createSandbox();
+
+ let called = false;
+ sandbox.then = function (onFulfilled, onRejected) {
+ called = true;
+ };
+
+ // Creating a promise thenable job in the following `Promise.resolve` performs
+ // GetFunctionRealm on the bound thenable.then and fails. The reaction job
+ // should be created with sandbox realm.
+ sandbox.eval(`
+var thenable = {
+ then: then,
+};
+
+Promise.resolve(thenable);
+`);
+
+ Cu.nukeSandbox(sandbox);
+
+ // Drain the job queue, to make sure we hit dead object error inside the
+ // thenable job.
+ await Promise.resolve(10);
+
+ equal(
+ Services.console.getMessageArray().find(x => {
+ return x.toString().includes("can't access dead object");
+ }) !== undefined,
+ true
+ );
+ equal(called, false);
+});
+
+add_task(async function testThenableJobAccessError() {
+ const sandbox = createSandbox();
+
+ let accessed = false;
+ sandbox.thenable = {
+ get then() {
+ accessed = true;
+ },
+ };
+
+ // The following operation silently fails when accessing `then` property.
+ sandbox.eval(`
+var x = typeof thenable.then;
+
+Promise.resolve(thenable);
+`);
+
+ equal(accessed, false);
+});
+
+add_task(async function testBoundThenableJob() {
+ const sandbox = createSandbox();
+
+ sandbox.eval(`
+var resolve = undefined;
+var callbackPromise = new Promise(r => { resolve = r; });
+var callback = function (v) { resolve(v + 1); };
+
+var then = function(onFulfilled, onRejected) {
+ onFulfilled(10);
+};
+`);
+
+ // Create a bound function where its realm is privileged realm, and
+ // its target is from sandbox realm.
+ sandbox.bound_then = Function.prototype.bind.call(sandbox.then, sandbox);
+
+ // Creating a promise thenable job in the following `Promise.resolve` performs
+ // GetFunctionRealm on the bound thenable.then and fails. The reaction job
+ // should be created with sandbox realm.
+ sandbox.eval(`
+var thenable = {
+ then: bound_then,
+};
+
+Promise.resolve(thenable).then(callback);
+`);
+
+ const result = await sandbox.callbackPromise;
+ equal(result, 11);
+});
diff --git a/dom/promise/tests/unit/test_promise_unhandled_rejection.js b/dom/promise/tests/unit/test_promise_unhandled_rejection.js
new file mode 100644
index 0000000000..68471569ec
--- /dev/null
+++ b/dom/promise/tests/unit/test_promise_unhandled_rejection.js
@@ -0,0 +1,139 @@
+"use strict";
+
+// Tests that unhandled promise rejections generate the appropriate
+// console messages.
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+
+PromiseTestUtils.expectUncaughtRejection(/could not be cloned/);
+PromiseTestUtils.expectUncaughtRejection(/An exception was thrown/);
+PromiseTestUtils.expectUncaughtRejection(/Bleah/);
+
+const filename = "resource://foo/Bar.jsm";
+
+async function getSandboxMessages(sandbox, code) {
+ let { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
+ Cu.evalInSandbox(code, sandbox, null, filename, 1);
+
+ // We need two trips through the event loop for this error to be reported.
+ await new Promise(executeSoon);
+ await new Promise(executeSoon);
+ });
+
+ // xpcshell tests on OS-X sometimes include an extra warning, which we
+ // unfortunately need to ignore:
+ return messages.filter(
+ msg =>
+ !msg.message.includes(
+ "No chrome package registered for chrome://branding/locale/brand.properties"
+ )
+ );
+}
+
+add_task(async function test_unhandled_dom_exception() {
+ let sandbox = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal());
+ sandbox.StructuredCloneHolder = StructuredCloneHolder;
+
+ let messages = await getSandboxMessages(
+ sandbox,
+ `new Promise(() => {
+ new StructuredCloneHolder("", "", () => {});
+ });`
+ );
+
+ equal(messages.length, 1, "Got one console message");
+
+ let [msg] = messages;
+ ok(msg instanceof Ci.nsIScriptError, "Message is a script error");
+ equal(msg.sourceName, filename, "Got expected filename");
+ equal(msg.lineNumber, 2, "Got expected line number");
+ equal(
+ msg.errorMessage,
+ "DataCloneError: Function object could not be cloned.",
+ "Got expected error message"
+ );
+});
+
+add_task(async function test_unhandled_dom_exception_wrapped() {
+ let sandbox = Cu.Sandbox(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com/"
+ )
+ );
+ Cu.exportFunction(
+ function frick() {
+ throw new Components.Exception(
+ "Bleah.",
+ Cr.NS_ERROR_FAILURE,
+ Components.stack.caller
+ );
+ },
+ sandbox,
+ { defineAs: "frick" }
+ );
+
+ let messages = await getSandboxMessages(
+ sandbox,
+ `new Promise(() => {
+ frick();
+ });`
+ );
+
+ equal(messages.length, 2, "Got two console messages");
+
+ let [msg1, msg2] = messages;
+ ok(msg1 instanceof Ci.nsIScriptError, "Message is a script error");
+ equal(msg1.sourceName, filename, "Got expected filename");
+ equal(msg1.lineNumber, 2, "Got expected line number");
+ equal(
+ msg1.errorMessage,
+ "NS_ERROR_FAILURE: Bleah.",
+ "Got expected error message"
+ );
+
+ ok(msg2 instanceof Ci.nsIScriptError, "Message is a script error");
+ equal(msg2.sourceName, filename, "Got expected filename");
+ equal(msg2.lineNumber, 2, "Got expected line number");
+ equal(
+ msg2.errorMessage,
+ "InvalidStateError: An exception was thrown",
+ "Got expected error message"
+ );
+});
+
+add_task(async function test_unhandled_dom_exception_from_sandbox() {
+ let sandbox = Cu.Sandbox(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com/"
+ ),
+ { wantGlobalProperties: ["DOMException"] }
+ );
+ let ctor = Cu.evalInSandbox("DOMException", sandbox);
+ Cu.exportFunction(
+ function frick() {
+ throw new ctor("Bleah.");
+ },
+ sandbox,
+ { defineAs: "frick" }
+ );
+
+ let messages = await getSandboxMessages(
+ sandbox,
+ `new Promise(() => {
+ frick();
+ });`
+ );
+
+ equal(messages.length, 1, "Got one console messages");
+
+ let [msg] = messages;
+ ok(msg instanceof Ci.nsIScriptError, "Message is a script error");
+ equal(msg.sourceName, filename, "Got expected filename");
+ equal(msg.lineNumber, 2, "Got expected line number");
+ equal(msg.errorMessage, "Error: Bleah.", "Got expected error message");
+});
diff --git a/dom/promise/tests/unit/xpcshell.ini b/dom/promise/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..4da333a9b2
--- /dev/null
+++ b/dom/promise/tests/unit/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head =
+
+[test_monitor_uncaught.js]
+[test_promise_unhandled_rejection.js]
+[test_promise_job_across_sandbox.js]