diff options
Diffstat (limited to 'toolkit/components/extensions/webidl-api/ExtensionTest.cpp')
-rw-r--r-- | toolkit/components/extensions/webidl-api/ExtensionTest.cpp | 527 |
1 files changed, 527 insertions, 0 deletions
diff --git a/toolkit/components/extensions/webidl-api/ExtensionTest.cpp b/toolkit/components/extensions/webidl-api/ExtensionTest.cpp new file mode 100644 index 0000000000..e9cde7ccad --- /dev/null +++ b/toolkit/components/extensions/webidl-api/ExtensionTest.cpp @@ -0,0 +1,527 @@ +/* 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 "ExtensionTest.h" +#include "ExtensionEventManager.h" +#include "ExtensionAPICallFunctionNoReturn.h" + +#include "js/Equality.h" // JS::StrictlyEqual +#include "js/PropertyAndElement.h" // JS_GetProperty +#include "mozilla/dom/ExtensionTestBinding.h" +#include "nsIGlobalObject.h" +#include "js/RegExp.h" +#include "mozilla/dom/WorkerScope.h" +#include "prenv.h" + +namespace mozilla { +namespace extensions { + +bool IsInAutomation(JSContext* aCx, JSObject* aGlobal) { + return NS_IsMainThread() + ? xpc::IsInAutomation() + : dom::WorkerGlobalScope::IsInAutomation(aCx, aGlobal); +} + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionTest); +NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionTest) +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ExtensionTest, mGlobal, mExtensionBrowser, + mOnMessageEventMgr); + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionTest) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_WEBEXT_EVENTMGR(ExtensionTest, u"onMessage"_ns, OnMessage) + +ExtensionTest::ExtensionTest(nsIGlobalObject* aGlobal, + ExtensionBrowser* aExtensionBrowser) + : mGlobal(aGlobal), mExtensionBrowser(aExtensionBrowser) { + MOZ_DIAGNOSTIC_ASSERT(mGlobal); + MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser); +} + +/* static */ +bool ExtensionTest::IsAllowed(JSContext* aCx, JSObject* aGlobal) { + // Allow browser.test API namespace while running in xpcshell tests. + if (PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR")) { + return true; + } + + return IsInAutomation(aCx, aGlobal); +} + +JSObject* ExtensionTest::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return dom::ExtensionTest_Binding::Wrap(aCx, this, aGivenProto); +} + +nsIGlobalObject* ExtensionTest::GetParentObject() const { return mGlobal; } + +void ExtensionTest::CallWebExtMethodAssertEq( + JSContext* aCx, const nsAString& aApiMethod, + const dom::Sequence<JS::Value>& aArgs, ErrorResult& aRv) { + uint32_t argsCount = aArgs.Length(); + + JS::Rooted<JS::Value> expectedVal( + aCx, argsCount > 0 ? aArgs[0] : JS::UndefinedValue()); + JS::Rooted<JS::Value> actualVal( + aCx, argsCount > 1 ? aArgs[1] : JS::UndefinedValue()); + JS::Rooted<JS::Value> messageVal( + aCx, argsCount > 2 ? aArgs[2] : JS::UndefinedValue()); + + bool isEqual; + if (NS_WARN_IF(!JS::StrictlyEqual(aCx, actualVal, expectedVal, &isEqual))) { + ThrowUnexpectedError(aCx, aRv); + return; + } + + JS::Rooted<JSString*> expectedJSString(aCx, JS::ToString(aCx, expectedVal)); + JS::Rooted<JSString*> actualJSString(aCx, JS::ToString(aCx, actualVal)); + JS::Rooted<JSString*> messageJSString(aCx, JS::ToString(aCx, messageVal)); + + nsString expected; + nsString actual; + nsString message; + + if (NS_WARN_IF(!AssignJSString(aCx, expected, expectedJSString) || + !AssignJSString(aCx, actual, actualJSString) || + !AssignJSString(aCx, message, messageJSString))) { + ThrowUnexpectedError(aCx, aRv); + return; + } + + if (!isEqual && actual.Equals(expected)) { + actual.AppendLiteral(" (different)"); + } + + if (NS_WARN_IF(!dom::ToJSValue(aCx, expected, &expectedVal) || + !dom::ToJSValue(aCx, actual, &actualVal) || + !dom::ToJSValue(aCx, message, &messageVal))) { + ThrowUnexpectedError(aCx, aRv); + return; + } + + dom::Sequence<JS::Value> args; + if (NS_WARN_IF(!args.AppendElement(expectedVal, fallible) || + !args.AppendElement(actualVal, fallible) || + !args.AppendElement(messageVal, fallible))) { + ThrowUnexpectedError(aCx, aRv); + return; + } + + CallWebExtMethodNoReturn(aCx, aApiMethod, args, aRv); +} + +MOZ_CAN_RUN_SCRIPT bool ExtensionTest::AssertMatchInternal( + JSContext* aCx, const JS::HandleValue aActualValue, + const JS::HandleValue aExpectedMatchValue, const nsAString& aMessagePre, + const nsAString& aMessage, + UniquePtr<dom::SerializedStackHolder> aSerializedCallerStack, + ErrorResult& aRv) { + // Stringify the actual value, if the expected value is a regexp or a string + // then it will be used as part of the matching assertion, otherwise it is + // still interpolated in the assertion message. + JS::Rooted<JSString*> actualToString(aCx, JS::ToString(aCx, aActualValue)); + NS_ENSURE_TRUE(actualToString, false); + nsAutoJSString actualString; + NS_ENSURE_TRUE(actualString.init(aCx, actualToString), false); + + bool matched = false; + + if (aExpectedMatchValue.isObject()) { + JS::Rooted<JSObject*> expectedMatchObj(aCx, + &aExpectedMatchValue.toObject()); + + bool isRegexp; + NS_ENSURE_TRUE(JS::ObjectIsRegExp(aCx, expectedMatchObj, &isRegexp), false); + + if (isRegexp) { + // Expected value is a regexp, test if the stringified actual value does + // match. + nsString input(actualString); + size_t index = 0; + JS::Rooted<JS::Value> rxResult(aCx); + NS_ENSURE_TRUE(JS::ExecuteRegExpNoStatics( + aCx, expectedMatchObj, input.BeginWriting(), + actualString.Length(), &index, true, &rxResult), + false); + matched = !rxResult.isNull(); + } else if (JS::IsCallable(expectedMatchObj) && + !JS::IsConstructor(expectedMatchObj)) { + // Expected value is a matcher function, execute it with the value as a + // parameter: + // + // - if the matcher function throws, steal the exception to re-raise it + // to the extension code that called the assertion method, but + // continue to still report the assertion as failed to the WebExtensions + // internals. + // + // - if the function return a falsey value, the assertion should fail and + // no exception is raised to the extension code that called the + // assertion + JS::Rooted<JS::Value> retval(aCx); + aRv.MightThrowJSException(); + if (!JS::Call(aCx, JS::UndefinedHandleValue, expectedMatchObj, + JS::HandleValueArray(aActualValue), &retval)) { + aRv.StealExceptionFromJSContext(aCx); + matched = false; + } else { + matched = JS::ToBoolean(retval); + } + } else if (JS::IsConstructor(expectedMatchObj)) { + // Expected value is a constructor, test if the actual value is an + // instanceof the expected constructor. + NS_ENSURE_TRUE( + JS_HasInstance(aCx, expectedMatchObj, aActualValue, &matched), false); + } else { + // Fallback to strict equal for any other js object type we don't expect. + NS_ENSURE_TRUE( + JS::StrictlyEqual(aCx, aActualValue, aExpectedMatchValue, &matched), + false); + } + } else if (aExpectedMatchValue.isString()) { + // Expected value is a string, assertion should fail if the expected string + // isn't equal to the stringified actual value. + JS::Rooted<JSString*> expectedToString( + aCx, JS::ToString(aCx, aExpectedMatchValue)); + NS_ENSURE_TRUE(expectedToString, false); + + nsAutoJSString expectedString; + NS_ENSURE_TRUE(expectedString.init(aCx, expectedToString), false); + + // If actual is an object and it has a message property that is a string, + // then we want to use that message string as the string to compare the + // expected one with. + // + // This is needed mainly to match the current JS implementation. + // + // TODO(Bug 1731094): as a low priority follow up, we may want to reconsider + // and compare the entire stringified error (which is also often a common + // behavior in many third party JS test frameworks). + JS::Rooted<JS::Value> messageVal(aCx); + if (aActualValue.isObject()) { + JS::Rooted<JSObject*> actualValueObj(aCx, &aActualValue.toObject()); + + if (!JS_GetProperty(aCx, actualValueObj, "message", &messageVal)) { + // GetProperty may raise an exception, in that case we steal the + // exception to re-raise it to the caller, but continue to still report + // the assertion as failed to the WebExtensions internals. + aRv.StealExceptionFromJSContext(aCx); + matched = false; + } + + if (messageVal.isString()) { + actualToString.set(messageVal.toString()); + NS_ENSURE_TRUE(actualString.init(aCx, actualToString), false); + } + } + matched = expectedString.Equals(actualString); + } else { + // Fallback to strict equal for any other js value type we don't expect. + NS_ENSURE_TRUE( + JS::StrictlyEqual(aCx, aActualValue, aExpectedMatchValue, &matched), + false); + } + + // Convert the expected value to a source string, to be interpolated + // in the assertion message. + JS::Rooted<JSString*> expectedToSource( + aCx, JS_ValueToSource(aCx, aExpectedMatchValue)); + NS_ENSURE_TRUE(expectedToSource, false); + nsAutoJSString expectedSource; + NS_ENSURE_TRUE(expectedSource.init(aCx, expectedToSource), false); + + nsString message; + message.AppendPrintf("%s to match '%s', got '%s'", + NS_ConvertUTF16toUTF8(aMessagePre).get(), + NS_ConvertUTF16toUTF8(expectedSource).get(), + NS_ConvertUTF16toUTF8(actualString).get()); + if (!aMessage.IsEmpty()) { + message.AppendPrintf(": %s", NS_ConvertUTF16toUTF8(aMessage).get()); + } + + // Complete the assertion by forwarding the boolean result and the + // interpolated assertion message to the test.assertTrue API method on the + // main thread. + dom::Sequence<JS::Value> assertTrueArgs; + JS::Rooted<JS::Value> arg0(aCx); + JS::Rooted<JS::Value> arg1(aCx); + NS_ENSURE_FALSE(!dom::ToJSValue(aCx, matched, &arg0) || + !dom::ToJSValue(aCx, message, &arg1) || + !assertTrueArgs.AppendElement(arg0, fallible) || + !assertTrueArgs.AppendElement(arg1, fallible), + false); + + auto request = CallFunctionNoReturn(u"assertTrue"_ns); + IgnoredErrorResult erv; + if (aSerializedCallerStack) { + request->SetSerializedCallerStack(std::move(aSerializedCallerStack)); + } + request->Run(GetGlobalObject(), aCx, assertTrueArgs, erv); + NS_ENSURE_FALSE(erv.Failed(), false); + return true; +} + +MOZ_CAN_RUN_SCRIPT void ExtensionTest::AssertThrows( + JSContext* aCx, dom::Function& aFunction, + const JS::HandleValue aExpectedError, const nsAString& aMessage, + ErrorResult& aRv) { + // Call the function that is expected to throw, then get the pending exception + // to pass it to the AssertMatchInternal. + ErrorResult erv; + erv.MightThrowJSException(); + JS::Rooted<JS::Value> ignoredRetval(aCx); + aFunction.Call({}, &ignoredRetval, erv, "ExtensionTest::AssertThrows", + dom::Function::eRethrowExceptions); + + bool didThrow = false; + JS::Rooted<JS::Value> exn(aCx); + + if (erv.MaybeSetPendingException(aCx) && JS_GetPendingException(aCx, &exn)) { + JS_ClearPendingException(aCx); + didThrow = true; + } + + // If the function did not throw, then the assertion is failed + // and the result should be forwarded to assertTrue on the main thread. + if (!didThrow) { + JS::Rooted<JSString*> expectedErrorToSource( + aCx, JS_ValueToSource(aCx, aExpectedError)); + if (NS_WARN_IF(!expectedErrorToSource)) { + ThrowUnexpectedError(aCx, aRv); + return; + } + nsAutoJSString expectedErrorSource; + if (NS_WARN_IF(!expectedErrorSource.init(aCx, expectedErrorToSource))) { + ThrowUnexpectedError(aCx, aRv); + return; + } + + nsString message; + message.AppendPrintf("Function did not throw, expected error '%s'", + NS_ConvertUTF16toUTF8(expectedErrorSource).get()); + if (!aMessage.IsEmpty()) { + message.AppendPrintf(": %s", NS_ConvertUTF16toUTF8(aMessage).get()); + } + + dom::Sequence<JS::Value> assertTrueArgs; + JS::Rooted<JS::Value> arg0(aCx); + JS::Rooted<JS::Value> arg1(aCx); + if (NS_WARN_IF(!dom::ToJSValue(aCx, false, &arg0) || + !dom::ToJSValue(aCx, message, &arg1) || + !assertTrueArgs.AppendElement(arg0, fallible) || + !assertTrueArgs.AppendElement(arg1, fallible))) { + ThrowUnexpectedError(aCx, aRv); + return; + } + + CallWebExtMethodNoReturn(aCx, u"assertTrue"_ns, assertTrueArgs, aRv); + if (NS_WARN_IF(aRv.Failed())) { + ThrowUnexpectedError(aCx, aRv); + } + return; + } + + if (NS_WARN_IF(!AssertMatchInternal(aCx, exn, aExpectedError, + u"Function threw, expecting error"_ns, + aMessage, nullptr, aRv))) { + ThrowUnexpectedError(aCx, aRv); + } +} + +MOZ_CAN_RUN_SCRIPT void ExtensionTest::AssertThrows( + JSContext* aCx, dom::Function& aFunction, + const JS::HandleValue aExpectedError, ErrorResult& aRv) { + AssertThrows(aCx, aFunction, aExpectedError, EmptyString(), aRv); +} + +#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::Handle<JS::Value> aExpectedMatchValue, + const nsAString& aMessage, + UniquePtr<dom::SerializedStackHolder>&& aCallerStack) { + MOZ_ASSERT(aPromise); + MOZ_ASSERT(outPromise); + MOZ_ASSERT(aExtensionTest); + + RefPtr<AssertRejectsHandler> handler = new AssertRejectsHandler( + aExtensionTest, outPromise, aExpectedMatchValue, aMessage, + std::move(aCallerStack)); + + aPromise->AppendNativeHandler(handler); + } + + MOZ_CAN_RUN_SCRIPT void ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { + nsAutoJSString expectedErrorSource; + JS::Rooted<JS::Value> rootedExpectedMatchValue(aCx, mExpectedMatchValue); + JS::Rooted<JSString*> 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 (!mMessageStr.IsEmpty()) { + message.AppendPrintf(": %s", NS_ConvertUTF16toUTF8(mMessageStr).get()); + } + + dom::Sequence<JS::Value> assertTrueArgs; + JS::Rooted<JS::Value> arg0(aCx); + JS::Rooted<JS::Value> 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<JS::Value> aValue, + ErrorResult& aRv) override { + JS::Rooted<JS::Value> expectedMatchRooted(aCx, mExpectedMatchValue); + ErrorResult erv; + + if (NS_WARN_IF(!MOZ_KnownLive(mExtensionTest) + ->AssertMatchInternal( + aCx, aValue, expectedMatchRooted, + u"Promise rejected, expected rejection"_ns, + mMessageStr, 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::Handle<JS::Value> aExpectedMatchValue, + const nsAString& aMessage, + UniquePtr<dom::SerializedStackHolder>&& aCallerStack) + : mOutPromise(mOutPromise), mExtensionTest(aExtensionTest) { + MOZ_ASSERT(mOutPromise); + MOZ_ASSERT(mExtensionTest); + mozilla::HoldJSObjects(this); + mExpectedMatchValue.set(aExpectedMatchValue); + mCallerStack = std::move(aCallerStack); + mMessageStr = aMessage; + } + + ~AssertRejectsHandler() { + mOutPromise = nullptr; + mExtensionTest = nullptr; + mExpectedMatchValue.setUndefined(); + mozilla::DropJSObjects(this); + }; + + RefPtr<dom::Promise> mOutPromise; + RefPtr<ExtensionTest> mExtensionTest; + JS::Heap<JS::Value> mExpectedMatchValue; + UniquePtr<dom::SerializedStackHolder> 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 nsAString& aMessage, + const dom::Optional<OwningNonNull<dom::Function>>& aCallback, + JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv) { + auto* global = GetGlobalObject(); + + IgnoredErrorResult erv; + RefPtr<dom::Promise> 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; + } +} + +void ExtensionTest::AssertRejects( + JSContext* aCx, dom::Promise& aPromise, + const JS::HandleValue aExpectedError, + const dom::Optional<OwningNonNull<dom::Function>>& aCallback, + JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv) { + AssertRejects(aCx, aPromise, aExpectedError, EmptyString(), aCallback, + aRetval, aRv); +} + +} // namespace extensions +} // namespace mozilla |