/* 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 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& aArgs, ErrorResult& aRv) { uint32_t argsCount = aArgs.Length(); JS::Rooted expectedVal( aCx, argsCount > 0 ? aArgs[0] : JS::UndefinedValue()); JS::Rooted actualVal( aCx, argsCount > 1 ? aArgs[1] : JS::UndefinedValue()); JS::Rooted 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 expectedJSString(aCx, JS::ToString(aCx, expectedVal)); JS::Rooted actualJSString(aCx, JS::ToString(aCx, actualVal)); JS::Rooted 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 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 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 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 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 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 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 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 messageVal(aCx); if (aActualValue.isObject()) { JS::Rooted 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 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 assertTrueArgs; JS::Rooted arg0(aCx); JS::Rooted 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 ignoredRetval(aCx); aFunction.Call({}, &ignoredRetval, erv, "ExtensionTest::AssertThrows", dom::Function::eRethrowExceptions); bool didThrow = false; JS::Rooted 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 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 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))) { 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 aExpectedMatchValue, const nsAString& 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, ErrorResult& aRv) 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 (!mMessageStr.IsEmpty()) { message.AppendPrintf(": %s", NS_ConvertUTF16toUTF8(mMessageStr).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, ErrorResult& aRv) 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, 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 aExpectedMatchValue, const nsAString& aMessage, UniquePtr&& 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 mOutPromise; RefPtr mExtensionTest; JS::Heap mExpectedMatchValue; 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 nsAString& 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; } } void ExtensionTest::AssertRejects( JSContext* aCx, dom::Promise& aPromise, const JS::HandleValue aExpectedError, const dom::Optional>& aCallback, JS::MutableHandle aRetval, ErrorResult& aRv) { AssertRejects(aCx, aPromise, aExpectedError, EmptyString(), aCallback, aRetval, aRv); } } // namespace extensions } // namespace mozilla