diff --git a/dom/webidl/ExtensionTest.webidl b/dom/webidl/ExtensionTest.webidl index 4dc3fbeddf11..baf7e3ff05d5 100644 --- a/dom/webidl/ExtensionTest.webidl +++ b/dom/webidl/ExtensionTest.webidl @@ -55,7 +55,7 @@ interface ExtensionTest { [Throws, WebExtensionStub="AssertEq"] void assertEq(any... args); - [Throws, WebExtensionStub="NotImplementedAsync"] + [Throws] any assertRejects(Promise promise, any expectedError, optional DOMString message, optional Function callback); [Throws] diff --git a/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_request_handler.js b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_request_handler.js index c505c2ff34a6..0cc46a27897c 100644 --- a/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_request_handler.js +++ b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_request_handler.js @@ -36,7 +36,7 @@ add_task(async function test_sw_api_request_handling_local_process_api() { files: { "page.html": "", "sw.js": async function() { - browser.test.onMessage.addListener(msg => { + browser.test.onMessage.addListener(async msg => { browser.test.succeed("call to test.succeed"); browser.test.assertTrue(true, "call to test.assertTrue"); browser.test.assertFalse(false, "call to test.assertFalse"); @@ -72,6 +72,17 @@ add_task(async function test_sw_api_request_handling_local_process_api() { ); } + browser.test.log("run assertRejects smoke tests"); + + const rejectedPromise = Promise.reject(errorObject); + for (const [msg, expected] of errorMatchingTestCases) { + await browser.test.assertRejects( + rejectedPromise, + expected, + `call to assertRejects with ${msg}` + ); + } + browser.test.notifyPass("test-completed"); }); browser.test.sendMessage("bgsw-ready"); diff --git a/toolkit/components/extensions/webidl-api/ExtensionTest.cpp b/toolkit/components/extensions/webidl-api/ExtensionTest.cpp index 3c34da4b2ad6..a9e108c3af00 100644 --- a/toolkit/components/extensions/webidl-api/ExtensionTest.cpp +++ b/toolkit/components/extensions/webidl-api/ExtensionTest.cpp @@ -336,5 +336,185 @@ ExtensionEventManager* ExtensionTest::OnMessage() { return mOnMessageEventMgr; } +#define ASSERT_REJECT_UNKNOWN_FAIL_STR "Failed to complete assertRejects call" + +class AssertRejectsHandler final : public dom::PromiseNativeHandler { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AssertRejectsHandler) + + static void Create(ExtensionTest* aExtensionTest, dom::Promise* aPromise, + dom::Promise* outPromise, + JS::HandleValue aExpectedMatchValue, + const dom::Optional& aMessage, + UniquePtr&& aCallerStack) { + MOZ_ASSERT(aPromise); + MOZ_ASSERT(outPromise); + MOZ_ASSERT(aExtensionTest); + + RefPtr handler = new AssertRejectsHandler( + aExtensionTest, outPromise, aExpectedMatchValue, aMessage, + std::move(aCallerStack)); + + aPromise->AppendNativeHandler(handler); + } + + MOZ_CAN_RUN_SCRIPT void ResolvedCallback( + JSContext* aCx, JS::Handle aValue) override { + nsAutoJSString expectedErrorSource; + JS::Rooted rootedExpectedMatchValue(aCx, mExpectedMatchValue); + JS::Rooted expectedErrorToSource( + aCx, JS_ValueToSource(aCx, rootedExpectedMatchValue)); + if (NS_WARN_IF(!expectedErrorToSource || + !expectedErrorSource.init(aCx, expectedErrorToSource))) { + mOutPromise->MaybeRejectWithUnknownError(ASSERT_REJECT_UNKNOWN_FAIL_STR); + return; + } + + nsString message; + message.AppendPrintf("Promise resolved, expect rejection '%s'", + NS_ConvertUTF16toUTF8(expectedErrorSource).get()); + + if (mMessage.WasPassed()) { + message.AppendPrintf(": %s", + NS_ConvertUTF16toUTF8(mMessage.Value()).get()); + } + + dom::Sequence assertTrueArgs; + JS::Rooted arg0(aCx); + JS::Rooted arg1(aCx); + if (NS_WARN_IF(!dom::ToJSValue(aCx, false, &arg0) || + !dom::ToJSValue(aCx, message, &arg1) || + !assertTrueArgs.AppendElement(arg0, fallible) || + !assertTrueArgs.AppendElement(arg1, fallible))) { + mOutPromise->MaybeRejectWithUnknownError(ASSERT_REJECT_UNKNOWN_FAIL_STR); + return; + } + + IgnoredErrorResult erv; + auto request = mExtensionTest->CallFunctionNoReturn(u"assertTrue"_ns); + request->SetSerializedCallerStack(std::move(mCallerStack)); + request->Run(mExtensionTest->GetGlobalObject(), aCx, assertTrueArgs, erv); + if (NS_WARN_IF(erv.Failed())) { + mOutPromise->MaybeRejectWithUnknownError(ASSERT_REJECT_UNKNOWN_FAIL_STR); + return; + } + mOutPromise->MaybeResolve(JS::UndefinedValue()); + } + + MOZ_CAN_RUN_SCRIPT void RejectedCallback( + JSContext* aCx, JS::Handle aValue) override { + JS::Rooted expectedMatchRooted(aCx, mExpectedMatchValue); + ErrorResult erv; + + if (NS_WARN_IF(!MOZ_KnownLive(mExtensionTest) + ->AssertMatchInternal( + aCx, aValue, expectedMatchRooted, + u"Promise rejected, expected rejection"_ns, + mMessage, std::move(mCallerStack), erv))) { + // Reject for other unknown errors. + mOutPromise->MaybeRejectWithUnknownError(ASSERT_REJECT_UNKNOWN_FAIL_STR); + return; + } + + // Reject with the matcher function exception. + erv.WouldReportJSException(); + if (erv.Failed()) { + mOutPromise->MaybeReject(std::move(erv)); + return; + } + mExpectedMatchValue.setUndefined(); + mOutPromise->MaybeResolveWithUndefined(); + } + + private: + AssertRejectsHandler(ExtensionTest* aExtensionTest, dom::Promise* mOutPromise, + JS::HandleValue aExpectedMatchValue, + const dom::Optional& aMessage, + UniquePtr&& aCallerStack) + : mOutPromise(mOutPromise), mExtensionTest(aExtensionTest) { + MOZ_ASSERT(mOutPromise); + MOZ_ASSERT(mExtensionTest); + mozilla::HoldJSObjects(this); + mExpectedMatchValue.set(aExpectedMatchValue); + mCallerStack = std::move(aCallerStack); + if (aMessage.WasPassed()) { + mMessageStr = aMessage.Value(); + mMessage = &mMessageStr; + } + } + + ~AssertRejectsHandler() { + mOutPromise = nullptr; + mExtensionTest = nullptr; + mExpectedMatchValue.setUndefined(); + mozilla::DropJSObjects(this); + }; + + RefPtr mOutPromise; + RefPtr mExtensionTest; + JS::Heap mExpectedMatchValue; + dom::Optional mMessage; + UniquePtr mCallerStack; + nsString mMessageStr; +}; + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AssertRejectsHandler) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_CLASS(AssertRejectsHandler) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AssertRejectsHandler) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AssertRejectsHandler) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AssertRejectsHandler) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExtensionTest) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutPromise) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(AssertRejectsHandler) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mExpectedMatchValue) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AssertRejectsHandler) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mExtensionTest) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutPromise) + tmp->mExpectedMatchValue.setUndefined(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +void ExtensionTest::AssertRejects( + JSContext* aCx, dom::Promise& aPromise, + const JS::HandleValue aExpectedError, + const dom::Optional& aMessage, + const dom::Optional>& aCallback, + JS::MutableHandle aRetval, ErrorResult& aRv) { + auto* global = GetGlobalObject(); + + IgnoredErrorResult erv; + RefPtr outPromise = dom::Promise::Create(global, erv); + if (NS_WARN_IF(erv.Failed())) { + ThrowUnexpectedError(aCx, aRv); + return; + } + MOZ_ASSERT(outPromise); + + AssertRejectsHandler::Create(this, &aPromise, outPromise, aExpectedError, + aMessage, dom::GetCurrentStack(aCx)); + + if (aCallback.WasPassed()) { + // In theory we could also support the callback-based behavior, but we + // only use this in tests and so we don't really need to support it + // for Chrome-compatibility reasons. + aRv.ThrowNotSupportedError("assertRejects does not support a callback"); + return; + } + + if (NS_WARN_IF(!ToJSValue(aCx, outPromise, aRetval))) { + ThrowUnexpectedError(aCx, aRv); + return; + } +} + } // namespace extensions } // namespace mozilla diff --git a/toolkit/components/extensions/webidl-api/ExtensionTest.h b/toolkit/components/extensions/webidl-api/ExtensionTest.h index bfc7b05ef7ff..635eb23e5ce2 100644 --- a/toolkit/components/extensions/webidl-api/ExtensionTest.h +++ b/toolkit/components/extensions/webidl-api/ExtensionTest.h @@ -75,6 +75,13 @@ class ExtensionTest final : public nsISupports, const dom::Optional& aMessage, ErrorResult& aRv); + void AssertRejects( + JSContext* aCx, dom::Promise& aPromise, + const JS::HandleValue aExpectedError, + const dom::Optional& aMessage, + const dom::Optional>& aCallback, + JS::MutableHandle aRetval, ErrorResult& aRv); + ExtensionEventManager* OnMessage(); NS_DECL_CYCLE_COLLECTING_ISUPPORTS diff --git a/toolkit/components/extensions/webidl-api/ExtensionWebIDL.conf b/toolkit/components/extensions/webidl-api/ExtensionWebIDL.conf index b2bb1c0845aa..c07d0e2c7fb1 100644 --- a/toolkit/components/extensions/webidl-api/ExtensionWebIDL.conf +++ b/toolkit/components/extensions/webidl-api/ExtensionWebIDL.conf @@ -52,7 +52,7 @@ WEBEXT_STUBS_MAPPING = { "runtime.connectNative": "ReturnsPort", "runtime.getURL": "ReturnsString", "test.assertEq": "AssertEq", - "test.assertRejects": "NotImplementedAsync", + "test.assertRejects": False, # No WebExtensionStub attribute. "test.assertThrows": False, # No WebExtensionStub attribute. "test.withHandlingUserInput": "NotImplementedNoReturn", }