/* 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 #include #include #include #include #include #ifdef __MINGW32__ // MinGW-w64 headers are missing PropVariantToString. # include PSSTDAPI PropVariantToString(REFPROPVARIANT propvar, PWSTR psz, UINT cch); #endif #include "gtest/gtest.h" #include "gmock/gmock.h" #include "mozilla/dom/BindingDeclarations.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/PromiseNativeHandler.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/ToJSValue.h" #include "mozilla/dom/WindowsJumpListShortcutDescriptionBinding.h" #include "mozilla/SpinEventLoopUntil.h" #include "JumpListBuilder.h" using namespace mozilla; using namespace testing; using mozilla::dom::AutoJSAPI; using mozilla::dom::Promise; using mozilla::dom::PromiseNativeHandler; using mozilla::dom::ToJSValue; using mozilla::dom::WindowsJumpListShortcutDescription; using mozilla::widget::JumpListBackend; using mozilla::widget::JumpListBuilder; /** * GMock matcher that ensures that two LPCWSTRs match. */ MATCHER_P(LPCWSTREq, value, "The equivalent of StrEq for LPCWSTRs") { return (wcscmp(arg, value)) == 0; } /** * GMock matcher that ensures that a IObjectArray* contains nsIShellLinkW's * that match an equivalent set of nsTArray */ MATCHER_P(ShellLinksEq, descs, "Comparing generated IShellLinkW with " "WindowsJumpListShortcutDescription definitions") { uint32_t count = 0; HRESULT hr = arg->GetCount(&count); if (FAILED(hr) || count != descs->Length()) { return false; } for (uint32_t i = 0; i < descs->Length(); ++i) { RefPtr link; if (FAILED(arg->GetAt(i, IID_IShellLinkW, static_cast(getter_AddRefs(link))))) { return false; } if (!link) { return false; } const WindowsJumpListShortcutDescription& desc = descs->ElementAt(i); // We'll now compare each member of the WindowsJumpListShortcutDescription // with what is stored in the IShellLink. // WindowsJumpListShortcutDescription.title IPropertyStore* propStore = nullptr; hr = link->QueryInterface(IID_IPropertyStore, (LPVOID*)&propStore); if (FAILED(hr)) { return false; } PROPVARIANT pv; hr = propStore->GetValue(PKEY_Title, &pv); if (FAILED(hr)) { return false; } wchar_t title[PKEYSTR_MAX]; hr = PropVariantToString(pv, title, PKEYSTR_MAX); if (FAILED(hr)) { return false; } if (!desc.mTitle.Equals(title)) { return false; } // WindowsJumpListShortcutDescription.path wchar_t pathBuf[MAX_PATH]; hr = link->GetPath(pathBuf, MAX_PATH, nullptr, SLGP_SHORTPATH); if (FAILED(hr)) { return false; } if (!desc.mPath.Equals(pathBuf)) { return false; } // WindowsJumpListShortcutDescription.arguments (optional) wchar_t argsBuf[MAX_PATH]; hr = link->GetArguments(argsBuf, MAX_PATH); if (FAILED(hr)) { return false; } if (desc.mArguments.WasPassed()) { if (!desc.mArguments.Value().Equals(argsBuf)) { return false; } } else { // Otherwise, the arguments should be empty. if (wcsnlen(argsBuf, MAX_PATH) != 0) { return false; } } // WindowsJumpListShortcutDescription.description wchar_t descBuf[MAX_PATH]; hr = link->GetDescription(descBuf, MAX_PATH); if (FAILED(hr)) { return false; } if (!desc.mDescription.Equals(descBuf)) { return false; } // WindowsJumpListShortcutDescription.iconPath and // WindowsJumpListShortcutDescription.fallbackIconIndex int iconIdx = 0; wchar_t iconPathBuf[MAX_PATH]; hr = link->GetIconLocation(iconPathBuf, MAX_PATH, &iconIdx); if (FAILED(hr)) { return false; } if (desc.mIconPath.WasPassed() && !desc.mIconPath.Value().IsEmpty()) { // If the WindowsJumpListShortcutDescription supplied an iconPath, // then it should match iconPathBuf and have an icon index of 0. if (!desc.mIconPath.Value().Equals(iconPathBuf) || iconIdx != 0) { return false; } } else { // Otherwise, the iconPathBuf should equal the // WindowsJumpListShortcutDescription path, and the iconIdx should match // the fallbackIconIndex. if (!desc.mPath.Equals(iconPathBuf) || desc.mFallbackIconIndex != iconIdx) { return false; } } } return true; } /** * This is a helper class that allows our tests to wait for a native DOM Promise * to resolve, and get the JS::Value that the Promise resolves with. This is * expected to run on the main thread. */ class WaitForResolver : public PromiseNativeHandler { public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WaitForResolver, override) NS_IMETHODIMP QueryInterface(REFNSIID aIID, void** aInstancePtr) override { nsresult rv = NS_ERROR_UNEXPECTED; NS_INTERFACE_TABLE0(WaitForResolver) return rv; } WaitForResolver() : mIsDone(false) {} void ResolvedCallback(JSContext* aCx, JS::Handle aValue, ErrorResult& aError) override { mResult = aValue; mIsDone = true; } void RejectedCallback(JSContext* aCx, JS::Handle aValue, ErrorResult& aError) override { ASSERT_TRUE(false); // Should never reach here. } /** * Spins a nested event loop and blocks until the Promise has resolved. */ void SpinUntilResolved() { SpinEventLoopUntil("WaitForResolver::SpinUntilResolved"_ns, [&]() { return mIsDone; }); } /** * Spins a nested event loop and blocks until the Promise has resolved, * after which the JS::Value that the Promise resolves with is returned via * the aRetval outparam. * * @param {JS::MutableHandle} aRetval * The outparam for the JS::Value that the Promise resolves with. */ void SpinUntilResolvedWithResult(JS::MutableHandle aRetval) { SpinEventLoopUntil("WaitForResolver::SpinUntilResolved"_ns, [&]() { return mIsDone; }); aRetval.set(mResult); } private: virtual ~WaitForResolver() = default; JS::Heap mResult; bool mIsDone; }; /** * An implementation of JumpListBackend that is instrumented using the GMock * framework to record calls. Unlike the NativeJumpListBackend, this backend * is expected to be instantiated on the main thread and passed as an argument * to the JumpListBuilder's worker thread. Testers should wait for the methods * that call these functions to resolve their Promises before checking the * recorded values. */ class TestingJumpListBackend : public JumpListBackend { NS_INLINE_DECL_THREADSAFE_REFCOUNTING(JumpListBackend, override) TestingJumpListBackend() : mMonitor("TestingJumpListBackend::mMonitor") {} virtual bool IsAvailable() override { return true; } MOCK_METHOD(HRESULT, SetAppID, (LPCWSTR)); MOCK_METHOD(HRESULT, BeginList, (UINT*, REFIID, void**)); MOCK_METHOD(HRESULT, AddUserTasks, (IObjectArray*)); MOCK_METHOD(HRESULT, AppendCategory, (LPCWSTR, IObjectArray*)); MOCK_METHOD(HRESULT, CommitList, ()); MOCK_METHOD(HRESULT, AbortList, ()); MOCK_METHOD(HRESULT, DeleteList, (LPCWSTR)); virtual HRESULT AppendKnownCategory(KNOWNDESTCATEGORY category) override { return 0; } // In one case (construction), an operation occurs off of the main thread that // we must wait for without an associated Promise. Monitor& GetMonitor() { return mMonitor; } protected: virtual ~TestingJumpListBackend() override{}; private: Monitor mMonitor; }; /** * A helper function that creates some fake WindowsJumpListShortcutDescription * objects as well as JS::Value representations of those objects. These are * returned to the caller through outparams. * * @param {JSContext*} aCx * The current JSContext in the execution environment. * @param {uint32_t} howMany * The number of WindowsJumpListShortcutDescriptions to generate. * @param {boolean} longDescription * True if the description should be greater than MAX_PATH (260 characters). * @param {nsTArray&} aArray * The outparam for the array of generated * WindowsJumpListShortcutDescriptions. * @param {nsTArray&} aJSValArray * The outparam for the array of JS::Value's representing the generated * WindowsJumpListShortcutDescriptions. */ void GenerateWindowsJumpListShortcutDescriptions( JSContext* aCx, uint32_t howMany, bool longDescription, nsTArray& aArray, nsTArray& aJSValArray) { for (uint32_t i = 0; i < howMany; ++i) { WindowsJumpListShortcutDescription desc; nsAutoString title(u"Test Task #"); title.AppendInt(i); desc.mTitle = title; nsAutoString path(u"C:\\Some\\Test\\Path.exe"); desc.mPath = path; nsAutoString description; if (longDescription) { description.AppendPrintf( "For item #%i, this is a very very very very VERY VERY very very " "very very very very very very very very very very VERY VERY very " "very very very very very very very very very very very VERY VERY " "very very very very very very very very very very very very VERY " "VERY very very very very very very very very very very very very " "VERY VERY very very very very very very very very very very very " "very VERY VERY very very very very very very very very long test " "description for an item", i); } else { description.AppendPrintf("This is a test description for an item #%i", i); } desc.mDescription = description; desc.mFallbackIconIndex = 0; if (!(i % 2)) { nsAutoString arguments(u"-arg1 -arg2 -arg3"); desc.mArguments.Construct(arguments); nsAutoString iconPath(u"C:\\Some\\icon.png"); desc.mIconPath.Construct(iconPath); } aArray.AppendElement(desc); JS::Rooted descJSValue(aCx); ASSERT_TRUE(ToJSValue(aCx, desc, &descJSValue)); aJSValArray.AppendElement(std::move(descJSValue)); } } /** * Tests construction and that the application ID is properly passed to the * backend. */ TEST(JumpListBuilder, Construction) { RefPtr> testBackend = new StrictMock(); ASSERT_TRUE(testBackend); nsAutoString aumid(u"TestApplicationID"); LPCWSTR passedID = aumid.get(); // Construction of our class (or any class of that matter) does not return a // Promise that we can wait on to ensure that the background thread got the // right information. We therefore use a monitor on the testing backend as // well as an EXPECT_CALL to block execution of the test until the background // work has completed. Monitor& mon = testBackend->GetMonitor(); MonitorAutoLock lock(mon); EXPECT_CALL(*testBackend, SetAppID(LPCWSTREq(passedID))).WillOnce([&mon] { MonitorAutoLock lock(mon); mon.Notify(); return S_OK; }); nsCOMPtr builder = new JumpListBuilder(aumid, testBackend); ASSERT_TRUE(builder); // This is the amount of time that we will wait for the background thread to // respond before considering it a timeout failure. const int kWaitTimeoutMs = 5000; ASSERT_TRUE(mon.Wait(TimeDuration::FromMilliseconds(kWaitTimeoutMs)) != CVStatus::Timeout); } /** * Tests calling CheckForRemovals and receiving a series of removed jump list * entries. Calling CheckForRemovals should call the following methods on the * backend, in order: * * - SetAppID * - AbortList * - BeginList * - AbortList */ TEST(JumpListBuilder, CheckForRemovals) { RefPtr> testBackend = new StrictMock(); nsAutoString aumid(u"TestApplicationID"); // We set up this expectation here because SetAppID will be called soon // after construction of the JumpListBuilder via the background thread. EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); nsCOMPtr builder = new JumpListBuilder(aumid, testBackend); ASSERT_TRUE(builder); EXPECT_CALL(*testBackend, AbortList()).Times(2); // Let's prepare BeginList to return a two entry collection of IShellLinks. // The first IShellLink will have the URL be https://example.com, the second // will have the URL be https://mozilla.org. EXPECT_CALL(*testBackend, BeginList) .WillOnce([](UINT* pcMinSlots, REFIID riid, void** ppv) { RefPtr collection; DebugOnly hr = CoCreateInstance( CLSID_EnumerableObjectCollection, nullptr, CLSCTX_INPROC_SERVER, IID_IObjectCollection, getter_AddRefs(collection)); MOZ_ASSERT(SUCCEEDED(hr)); RefPtr link; hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLinkW, getter_AddRefs(link)); MOZ_ASSERT(SUCCEEDED(hr)); nsAutoString firstLinkHref(u"https://example.com"_ns); link->SetArguments(firstLinkHref.get()); nsAutoString appPath(u"C:\\Tmp\\firefox.exe"_ns); link->SetIconLocation(appPath.get(), 0); collection->AddObject(link); // Let's re-use the same IShellLink, but change the URL to add our // second entry. The values of the IShellLink are ultimately copied // over to the items being added to the collection. nsAutoString secondLinkHref(u"https://mozilla.org"_ns); link->SetArguments(secondLinkHref.get()); collection->AddObject(link); RefPtr pArray; hr = collection->QueryInterface(IID_IObjectArray, getter_AddRefs(pArray)); MOZ_ASSERT(SUCCEEDED(hr)); *ppv = static_cast(pArray); (static_cast(*ppv))->AddRef(); // This is the default value to return, according to // https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-icustomdestinationlist-beginlist *pcMinSlots = 10; return S_OK; }); AutoJSAPI jsapi; MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); JSContext* cx = jsapi.cx(); RefPtr promise; nsresult rv = builder->CheckForRemovals(cx, getter_AddRefs(promise)); ASSERT_TRUE(NS_SUCCEEDED(rv)); ASSERT_TRUE(promise); RefPtr resolver = new WaitForResolver(); promise->AppendNativeHandler(resolver); JS::Rooted result(cx); resolver->SpinUntilResolvedWithResult(&result); ASSERT_TRUE(result.isObject()); JS::Rooted obj(cx, result.toObjectOrNull()); bool isArray; ASSERT_TRUE(JS::IsArrayObject(cx, obj, &isArray)); ASSERT_TRUE(isArray); // We should expect to see 2 URL strings returned in the array. uint32_t length = 0; ASSERT_TRUE(JS::GetArrayLength(cx, obj, &length)); ASSERT_EQ(length, 2U); // The first one should be https://example.com JS::Rooted firstURLValue(cx); ASSERT_TRUE(JS_GetElement(cx, obj, 0, &firstURLValue)); JS::Rooted firstURLJSString(cx, firstURLValue.toString()); nsAutoJSString firstURLAutoString; ASSERT_TRUE(firstURLAutoString.init(cx, firstURLJSString)); ASSERT_TRUE(firstURLAutoString.EqualsLiteral("https://example.com")); // The second one should be https://mozilla.org JS::Rooted secondURLValue(cx); ASSERT_TRUE(JS_GetElement(cx, obj, 1, &secondURLValue)); JS::Rooted secondURLJSString(cx, secondURLValue.toString()); nsAutoJSString secondURLAutoString; ASSERT_TRUE(secondURLAutoString.init(cx, secondURLJSString)); ASSERT_TRUE(secondURLAutoString.EqualsLiteral("https://mozilla.org")); } /** * Tests calling PopulateJumpList with empty arguments, which should call the * following methods on the backend, in order: * * - SetAppID * - AbortList * - BeginList * - CommitList * * This should result in an empty jump list for the user. */ TEST(JumpListBuilder, PopulateJumpListEmpty) { RefPtr> testBackend = new StrictMock(); nsAutoString aumid(u"TestApplicationID"); // We set up this expectation here because SetAppID will be called soon // after construction of the JumpListBuilder via the background thread. EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); nsCOMPtr builder = new JumpListBuilder(aumid, testBackend); ASSERT_TRUE(builder); AutoJSAPI jsapi; MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); JSContext* cx = jsapi.cx(); RefPtr promise; nsTArray taskDescJSVals; nsAutoString customTitle(u""); nsTArray customDescJSVals; EXPECT_CALL(*testBackend, AbortList()).Times(1); EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); EXPECT_CALL(*testBackend, CommitList()).Times(1); EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); nsresult rv = builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, cx, getter_AddRefs(promise)); ASSERT_TRUE(NS_SUCCEEDED(rv)); ASSERT_TRUE(promise); RefPtr resolver = new WaitForResolver(); promise->AppendNativeHandler(resolver); JS::Rooted result(cx); resolver->SpinUntilResolved(); } /** * Tests calling PopulateJumpList with only tasks, and no custom items. * This should call the following methods on the backend, in order: * * - SetAppID * - AbortList * - BeginList * - AddUserTasks * - CommitList * * This should result in a jump list with just tasks shown to the user, and * no custom section. */ TEST(JumpListBuilder, PopulateJumpListOnlyTasks) { RefPtr> testBackend = new StrictMock(); nsAutoString aumid(u"TestApplicationID"); // We set up this expectation here because SetAppID will be called soon // after construction of the JumpListBuilder via the background thread. EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); nsCOMPtr builder = new JumpListBuilder(aumid, testBackend); ASSERT_TRUE(builder); AutoJSAPI jsapi; MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); JSContext* cx = jsapi.cx(); RefPtr promise; nsTArray taskDescJSVals; nsTArray taskDescs; GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, taskDescs, taskDescJSVals); nsAutoString customTitle(u""); nsTArray customDescJSVals; EXPECT_CALL(*testBackend, AbortList()).Times(1); EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1); EXPECT_CALL(*testBackend, AppendCategory(_, _)).Times(0); EXPECT_CALL(*testBackend, CommitList()).Times(1); EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); nsresult rv = builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, cx, getter_AddRefs(promise)); ASSERT_TRUE(NS_SUCCEEDED(rv)); ASSERT_TRUE(promise); RefPtr resolver = new WaitForResolver(); promise->AppendNativeHandler(resolver); JS::Rooted result(cx); resolver->SpinUntilResolved(); } /** * Tests calling PopulateJumpList with only custom items, and no tasks. * This should call the following methods on the backend, in order: * * - SetAppID * - AbortList * - BeginList * - AppendCategory * - CommitList * * This should result in a jump list with just custom items shown to the user, * and no tasks. */ TEST(JumpListBuilder, PopulateJumpListOnlyCustomItems) { RefPtr> testBackend = new StrictMock(); nsAutoString aumid(u"TestApplicationID"); // We set up this expectation here because SetAppID will be called soon // after construction of the JumpListBuilder via the background thread. EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); nsCOMPtr builder = new JumpListBuilder(aumid, testBackend); ASSERT_TRUE(builder); AutoJSAPI jsapi; MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); JSContext* cx = jsapi.cx(); RefPtr promise; nsTArray descs; nsTArray customDescJSVals; GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, descs, customDescJSVals); nsAutoString customTitle(u"My custom title"); nsTArray taskDescJSVals; EXPECT_CALL(*testBackend, AbortList()).Times(1); EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); EXPECT_CALL(*testBackend, AddUserTasks(_)).Times(0); EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()), ShellLinksEq(&descs))) .Times(1); EXPECT_CALL(*testBackend, CommitList()).Times(1); EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); nsresult rv = builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, cx, getter_AddRefs(promise)); ASSERT_TRUE(NS_SUCCEEDED(rv)); ASSERT_TRUE(promise); RefPtr resolver = new WaitForResolver(); promise->AppendNativeHandler(resolver); JS::Rooted result(cx); resolver->SpinUntilResolved(); } /** * Tests calling PopulateJumpList with tasks and custom items. * This should call the following methods on the backend, in order: * * - SetAppID * - AbortList * - BeginList * - AddUserTasks * - AppendCategory * - CommitList * * This should result in a jump list with both built-in tasks as well as * custom tasks with a custom label. */ TEST(JumpListBuilder, PopulateJumpList) { RefPtr> testBackend = new StrictMock(); nsAutoString aumid(u"TestApplicationID"); // We set up this expectation here because SetAppID will be called soon // after construction of the JumpListBuilder via the background thread. EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); nsCOMPtr builder = new JumpListBuilder(aumid, testBackend); ASSERT_TRUE(builder); AutoJSAPI jsapi; MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); JSContext* cx = jsapi.cx(); RefPtr promise; nsTArray taskDescs; nsTArray taskDescJSVals; GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, taskDescs, taskDescJSVals); nsTArray customDescs; nsTArray customDescJSVals; GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, customDescs, customDescJSVals); nsAutoString customTitle(u"My custom title"); EXPECT_CALL(*testBackend, AbortList()).Times(1); EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1); EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()), ShellLinksEq(&customDescs))) .Times(1); EXPECT_CALL(*testBackend, CommitList()).Times(1); EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); nsresult rv = builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, cx, getter_AddRefs(promise)); ASSERT_TRUE(NS_SUCCEEDED(rv)); ASSERT_TRUE(promise); RefPtr resolver = new WaitForResolver(); promise->AppendNativeHandler(resolver); JS::Rooted result(cx); resolver->SpinUntilResolved(); } /** * Tests calling ClearJumpList calls the following: * * - SetAppID * - DeleteList (passing the aumid) * * This results in an empty jump list for the user. */ TEST(JumpListBuilder, ClearJumpList) { RefPtr> testBackend = new StrictMock(); nsAutoString aumid(u"TestApplicationID"); // We set up this expectation here because SetAppID will be called soon // after construction of the JumpListBuilder via the background thread. EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); nsCOMPtr builder = new JumpListBuilder(aumid, testBackend); ASSERT_TRUE(builder); AutoJSAPI jsapi; MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); JSContext* cx = jsapi.cx(); RefPtr promise; EXPECT_CALL(*testBackend, AbortList()).Times(0); EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(0); EXPECT_CALL(*testBackend, AddUserTasks(_)).Times(0); EXPECT_CALL(*testBackend, AppendCategory(_, _)).Times(0); EXPECT_CALL(*testBackend, CommitList()).Times(0); EXPECT_CALL(*testBackend, DeleteList(LPCWSTREq(aumid.get()))).Times(1); nsresult rv = builder->ClearJumpList(cx, getter_AddRefs(promise)); ASSERT_TRUE(NS_SUCCEEDED(rv)); ASSERT_TRUE(promise); RefPtr resolver = new WaitForResolver(); promise->AppendNativeHandler(resolver); JS::Rooted result(cx); resolver->SpinUntilResolved(); } /** * Test that a WindowsJumpListShortcutDescription with a description * longer than MAX_PATH gets truncated to MAX_PATH. This is because a * description longer than MAX_PATH will cause CommitList to fail. */ TEST(JumpListBuilder, TruncateDescription) { RefPtr> testBackend = new StrictMock(); nsAutoString aumid(u"TestApplicationID"); // We set up this expectation here because SetAppID will be called soon // after construction of the JumpListBuilder via the background thread. EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); nsCOMPtr builder = new JumpListBuilder(aumid, testBackend); ASSERT_TRUE(builder); AutoJSAPI jsapi; MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); JSContext* cx = jsapi.cx(); RefPtr promise; nsTArray taskDescs; nsTArray taskDescJSVals; GenerateWindowsJumpListShortcutDescriptions(cx, 2, true, taskDescs, taskDescJSVals); nsTArray customDescs; nsTArray customDescJSVals; GenerateWindowsJumpListShortcutDescriptions(cx, 2, true, customDescs, customDescJSVals); // We expect the long descriptions to be truncated to 260 characters, so // we'll truncate the descriptions here ourselves. for (auto& taskDesc : taskDescs) { taskDesc.mDescription.SetLength(MAX_PATH - 1); } for (auto& customDesc : customDescs) { customDesc.mDescription.SetLength(MAX_PATH - 1); } nsAutoString customTitle(u"My custom title"); EXPECT_CALL(*testBackend, AbortList()).Times(1); EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1); EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()), ShellLinksEq(&customDescs))) .Times(1); EXPECT_CALL(*testBackend, CommitList()).Times(1); EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); nsresult rv = builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, cx, getter_AddRefs(promise)); ASSERT_TRUE(NS_SUCCEEDED(rv)); ASSERT_TRUE(promise); RefPtr resolver = new WaitForResolver(); promise->AppendNativeHandler(resolver); JS::Rooted result(cx); resolver->SpinUntilResolved(); }