summaryrefslogtreecommitdiffstats
path: root/widget/windows/tests/gtest/TestJumpListBuilder.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'widget/windows/tests/gtest/TestJumpListBuilder.cpp')
-rw-r--r--widget/windows/tests/gtest/TestJumpListBuilder.cpp823
1 files changed, 823 insertions, 0 deletions
diff --git a/widget/windows/tests/gtest/TestJumpListBuilder.cpp b/widget/windows/tests/gtest/TestJumpListBuilder.cpp
new file mode 100644
index 0000000000..5494c42d37
--- /dev/null
+++ b/widget/windows/tests/gtest/TestJumpListBuilder.cpp
@@ -0,0 +1,823 @@
+/* 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 <objectarray.h>
+#include <shobjidl.h>
+#include <windows.h>
+#include <string.h>
+#include <propvarutil.h>
+#include <propkey.h>
+
+#ifdef __MINGW32__
+// MinGW-w64 headers are missing PropVariantToString.
+# include <propsys.h>
+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<WindowsJumpListShortcutDescriptions>
+ */
+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<IShellLinkW> link;
+ if (FAILED(arg->GetAt(i, IID_IShellLinkW,
+ static_cast<void**>(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<JS::Value> aValue,
+ ErrorResult& aError) override {
+ mResult = aValue;
+ mIsDone = true;
+ }
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> 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<JS::Value>} aRetval
+ * The outparam for the JS::Value that the Promise resolves with.
+ */
+ void SpinUntilResolvedWithResult(JS::MutableHandle<JS::Value> aRetval) {
+ SpinEventLoopUntil("WaitForResolver::SpinUntilResolved"_ns,
+ [&]() { return mIsDone; });
+ aRetval.set(mResult);
+ }
+
+ private:
+ virtual ~WaitForResolver() = default;
+
+ JS::Heap<JS::Value> 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<WindowsJumpListShortcutDescription>&} aArray
+ * The outparam for the array of generated
+ * WindowsJumpListShortcutDescriptions.
+ * @param {nsTArray<JS::Value>&} aJSValArray
+ * The outparam for the array of JS::Value's representing the generated
+ * WindowsJumpListShortcutDescriptions.
+ */
+void GenerateWindowsJumpListShortcutDescriptions(
+ JSContext* aCx, uint32_t howMany, bool longDescription,
+ nsTArray<WindowsJumpListShortcutDescription>& aArray,
+ nsTArray<JS::Value>& 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<JS::Value> 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<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ 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<nsIJumpListBuilder> 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<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ 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<nsIJumpListBuilder> 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<IObjectCollection> collection;
+ DebugOnly<HRESULT> hr = CoCreateInstance(
+ CLSID_EnumerableObjectCollection, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IObjectCollection, getter_AddRefs(collection));
+ MOZ_ASSERT(SUCCEEDED(hr));
+
+ RefPtr<IShellLinkW> 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<IObjectArray> pArray;
+ hr = collection->QueryInterface(IID_IObjectArray,
+ getter_AddRefs(pArray));
+ MOZ_ASSERT(SUCCEEDED(hr));
+
+ *ppv = static_cast<IObjectArray*>(pArray);
+ (static_cast<IUnknown*>(*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> promise;
+ nsresult rv = builder->CheckForRemovals(cx, getter_AddRefs(promise));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_TRUE(promise);
+
+ RefPtr<WaitForResolver> resolver = new WaitForResolver();
+ promise->AppendNativeHandler(resolver);
+ JS::Rooted<JS::Value> result(cx);
+ resolver->SpinUntilResolvedWithResult(&result);
+
+ ASSERT_TRUE(result.isObject());
+ JS::Rooted<JSObject*> 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<JS::Value> firstURLValue(cx);
+ ASSERT_TRUE(JS_GetElement(cx, obj, 0, &firstURLValue));
+ JS::Rooted<JSString*> 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<JS::Value> secondURLValue(cx);
+ ASSERT_TRUE(JS_GetElement(cx, obj, 1, &secondURLValue));
+ JS::Rooted<JSString*> 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<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ 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<nsIJumpListBuilder> builder =
+ new JumpListBuilder(aumid, testBackend);
+ ASSERT_TRUE(builder);
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ RefPtr<Promise> promise;
+
+ nsTArray<JS::Value> taskDescJSVals;
+ nsAutoString customTitle(u"");
+ nsTArray<JS::Value> 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<WaitForResolver> resolver = new WaitForResolver();
+ promise->AppendNativeHandler(resolver);
+ JS::Rooted<JS::Value> 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<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ 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<nsIJumpListBuilder> builder =
+ new JumpListBuilder(aumid, testBackend);
+ ASSERT_TRUE(builder);
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ RefPtr<Promise> promise;
+
+ nsTArray<JS::Value> taskDescJSVals;
+ nsTArray<WindowsJumpListShortcutDescription> taskDescs;
+ GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, taskDescs,
+ taskDescJSVals);
+
+ nsAutoString customTitle(u"");
+ nsTArray<JS::Value> 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<WaitForResolver> resolver = new WaitForResolver();
+ promise->AppendNativeHandler(resolver);
+ JS::Rooted<JS::Value> 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<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ 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<nsIJumpListBuilder> builder =
+ new JumpListBuilder(aumid, testBackend);
+ ASSERT_TRUE(builder);
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ RefPtr<Promise> promise;
+
+ nsTArray<WindowsJumpListShortcutDescription> descs;
+ nsTArray<JS::Value> customDescJSVals;
+ GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, descs,
+ customDescJSVals);
+
+ nsAutoString customTitle(u"My custom title");
+ nsTArray<JS::Value> 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<WaitForResolver> resolver = new WaitForResolver();
+ promise->AppendNativeHandler(resolver);
+ JS::Rooted<JS::Value> 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<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ 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<nsIJumpListBuilder> builder =
+ new JumpListBuilder(aumid, testBackend);
+ ASSERT_TRUE(builder);
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ RefPtr<Promise> promise;
+
+ nsTArray<WindowsJumpListShortcutDescription> taskDescs;
+ nsTArray<JS::Value> taskDescJSVals;
+ GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, taskDescs,
+ taskDescJSVals);
+
+ nsTArray<WindowsJumpListShortcutDescription> customDescs;
+ nsTArray<JS::Value> 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<WaitForResolver> resolver = new WaitForResolver();
+ promise->AppendNativeHandler(resolver);
+ JS::Rooted<JS::Value> 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<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ 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<nsIJumpListBuilder> builder =
+ new JumpListBuilder(aumid, testBackend);
+ ASSERT_TRUE(builder);
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ RefPtr<Promise> 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<WaitForResolver> resolver = new WaitForResolver();
+ promise->AppendNativeHandler(resolver);
+ JS::Rooted<JS::Value> 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<StrictMock<TestingJumpListBackend>> testBackend =
+ new StrictMock<TestingJumpListBackend>();
+ 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<nsIJumpListBuilder> builder =
+ new JumpListBuilder(aumid, testBackend);
+ ASSERT_TRUE(builder);
+
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+ JSContext* cx = jsapi.cx();
+ RefPtr<Promise> promise;
+
+ nsTArray<WindowsJumpListShortcutDescription> taskDescs;
+ nsTArray<JS::Value> taskDescJSVals;
+ GenerateWindowsJumpListShortcutDescriptions(cx, 2, true, taskDescs,
+ taskDescJSVals);
+
+ nsTArray<WindowsJumpListShortcutDescription> customDescs;
+ nsTArray<JS::Value> 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<WaitForResolver> resolver = new WaitForResolver();
+ promise->AppendNativeHandler(resolver);
+ JS::Rooted<JS::Value> result(cx);
+ resolver->SpinUntilResolved();
+}