diff options
Diffstat (limited to 'dom/promise')
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 Binary files differnew file mode 100644 index 0000000000..787e19a5df --- /dev/null +++ b/dom/promise/tests/test_webassembly_compile_sample.wasm 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] |