summaryrefslogtreecommitdiffstats
path: root/widget/windows/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /widget/windows/tests
parentInitial commit. (diff)
downloadfirefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.tar.xz
firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/windows/tests')
-rw-r--r--widget/windows/tests/TestUriValidation.cpp135
-rw-r--r--widget/windows/tests/TestUrisToValidate.h471
-rw-r--r--widget/windows/tests/gtest/TestJumpListBuilder.cpp823
-rw-r--r--widget/windows/tests/gtest/TestWinDND.cpp728
-rw-r--r--widget/windows/tests/gtest/moz.build19
-rw-r--r--widget/windows/tests/moz.build33
-rw-r--r--widget/windows/tests/unit/test_windows_alert_service.js667
-rw-r--r--widget/windows/tests/unit/xpcshell.toml3
8 files changed, 2879 insertions, 0 deletions
diff --git a/widget/windows/tests/TestUriValidation.cpp b/widget/windows/tests/TestUriValidation.cpp
new file mode 100644
index 0000000000..d8a0ca09ce
--- /dev/null
+++ b/widget/windows/tests/TestUriValidation.cpp
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+#define MOZ_USE_LAUNCHER_ERROR
+
+#define UNICODE
+#include "mozilla/UrlmonHeaderOnlyUtils.h"
+#include "TestUrisToValidate.h"
+
+#include <urlmon.h>
+
+using namespace mozilla;
+
+static LauncherResult<_bstr_t> ShellValidateUri(const wchar_t* aUri) {
+ LauncherResult<UniqueAbsolutePidl> pidlResult = ShellParseDisplayName(aUri);
+ if (pidlResult.isErr()) {
+ return pidlResult.propagateErr();
+ }
+ UniqueAbsolutePidl pidl = pidlResult.unwrap();
+
+ // |pidl| is an absolute path. IShellFolder::GetDisplayNameOf requires a
+ // valid child ID, so the first thing we need to resolve is the IShellFolder
+ // for |pidl|'s parent, as well as the childId that represents |pidl|.
+ // Fortunately SHBindToParent does exactly that!
+ PCUITEMID_CHILD childId = nullptr;
+ RefPtr<IShellFolder> parentFolder;
+ HRESULT hr = SHBindToParent(pidl.get(), IID_IShellFolder,
+ getter_AddRefs(parentFolder), &childId);
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ // Now we retrieve the display name of |childId|, telling the shell that we
+ // plan to have the string parsed.
+ STRRET strret;
+ hr = parentFolder->GetDisplayNameOf(childId, SHGDN_FORPARSING, &strret);
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ // StrRetToBSTR automatically takes care of freeing any dynamically
+ // allocated memory in |strret|.
+ _bstr_t bstrUri;
+ hr = StrRetToBSTR(&strret, nullptr, bstrUri.GetAddress());
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ return bstrUri;
+}
+
+static LauncherResult<_bstr_t> GetFragment(const wchar_t* aUri) {
+ constexpr DWORD flags =
+ Uri_CREATE_NO_DECODE_EXTRA_INFO | Uri_CREATE_CANONICALIZE |
+ Uri_CREATE_CRACK_UNKNOWN_SCHEMES | Uri_CREATE_PRE_PROCESS_HTML_URI |
+ Uri_CREATE_IE_SETTINGS;
+ RefPtr<IUri> uri;
+ HRESULT hr = CreateUri(aUri, flags, 0, getter_AddRefs(uri));
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+
+ _bstr_t bstrFragment;
+ hr = uri->GetFragment(bstrFragment.GetAddress());
+ if (FAILED(hr)) {
+ return LAUNCHER_ERROR_FROM_HRESULT(hr);
+ }
+ return bstrFragment;
+}
+
+static bool RunSingleTest(const wchar_t* aUri) {
+ LauncherResult<_bstr_t> uriOld = ShellValidateUri(aUri),
+ uriNew = UrlmonValidateUri(aUri);
+ if (uriOld.isErr() != uriNew.isErr()) {
+ printf("TEST-FAILED | UriValidation | Validation result mismatch on %S\n",
+ aUri);
+ return false;
+ }
+
+ if (uriOld.isErr()) {
+ if (uriOld.unwrapErr().mError != uriNew.unwrapErr().mError) {
+ printf("TEST-FAILED | UriValidation | Error code mismatch on %S\n", aUri);
+ return false;
+ }
+ return true;
+ }
+
+ LauncherResult<_bstr_t> bstrFragment = GetFragment(aUri);
+ if (bstrFragment.isErr()) {
+ printf("TEST-FAILED | UriValidation | Failed to get a fragment from %S\n",
+ aUri);
+ return false;
+ }
+
+ // We validate a uri with two logics: the current one UrlmonValidateUri and
+ // the older one ShellValidateUri, to make sure the same validation result.
+ // We introduced UrlmonValidateUri because ShellValidateUri drops a fragment
+ // in a uri due to the design of Windows. To bypass the fragment issue, we
+ // extract a fragment and appends it into the validated string, and compare.
+ _bstr_t bstrUriOldCorrected = uriOld.unwrap() + bstrFragment.unwrap();
+ const _bstr_t& bstrUriNew = uriNew.unwrap();
+ if (bstrUriOldCorrected != bstrUriNew) {
+ printf("TEST-FAILED | UriValidation | %S %S %S\n", aUri,
+ static_cast<const wchar_t*>(bstrUriOldCorrected),
+ static_cast<const wchar_t*>(bstrUriNew));
+ return false;
+ }
+
+ return true;
+}
+
+int wmain(int argc, wchar_t* argv[]) {
+ HRESULT hr = CoInitialize(nullptr);
+ if (FAILED(hr)) {
+ return 1;
+ }
+
+ bool isOk = true;
+
+ if (argc == 2) {
+ isOk = RunSingleTest(argv[1]);
+ } else {
+ for (const wchar_t*& testUri : kTestUris) {
+ if (!RunSingleTest(testUri)) {
+ isOk = false;
+ }
+ }
+ }
+
+ CoUninitialize();
+ return isOk ? 0 : 1;
+}
diff --git a/widget/windows/tests/TestUrisToValidate.h b/widget/windows/tests/TestUrisToValidate.h
new file mode 100644
index 0000000000..cb00366d1e
--- /dev/null
+++ b/widget/windows/tests/TestUrisToValidate.h
@@ -0,0 +1,471 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_TestUrisToValidate_h
+#define mozilla_TestUrisToValidate_h
+
+const wchar_t* kTestUris[] = {
+ L"callto:%.txt",
+ L"callto:%00.txt",
+ L"callto:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"fdaction:%.txt",
+ L"fdaction:%00.txt",
+ L"fdaction:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"feed:%.txt",
+ L"feed:%00.txt",
+ L"feed:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"feeds:%.txt",
+ L"feeds:%00.txt",
+ L"feeds:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"file:///%.txt",
+ L"file:///%00.txt",
+ L"file:///%41%2D%31%5Ftest%22ing?%41%31%00.txt",
+ L"firefox.url:%.txt",
+ L"firefox.url:%00.txt",
+ L"firefox.url:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"firefoxurl:%.txt",
+ L"firefoxurl:%00.txt",
+ L"firefoxurl:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ftp:%.txt",
+ L"ftp:%00.txt",
+ L"ftp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"gopher:%.txt",
+ L"gopher:%00.txt",
+ L"gopher:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"gtalk:%.txt",
+ L"gtalk:%00.txt",
+ L"gtalk:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"HTTP:%.txt",
+ L"HTTP:%00.txt",
+ L"HTTP:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"http:%.txt",
+ L"http:%00.txt",
+ L"http:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"https://bug389580.bmoattachments.org/%.txt",
+ L"https://bug389580.bmoattachments.org/%00.txt",
+ L"https://bug389580.bmoattachments.org/"
+ L"%41%2D%31%5Ftest%22ing?%41%31%00.txt",
+ L"ie.ftp:%.txt",
+ L"ie.ftp:%00.txt",
+ L"ie.ftp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ie.http:%.txt",
+ L"ie.http:%00.txt",
+ L"ie.http:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ie.https:%.txt",
+ L"ie.https:%00.txt",
+ L"ie.https:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"irc:%.txt",
+ L"irc:%00.txt",
+ L"irc:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ircs:%.txt",
+ L"ircs:%00.txt",
+ L"ircs:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itms:%.txt",
+ L"itms:%00.txt",
+ L"itms:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itmss:%.txt",
+ L"itmss:%00.txt",
+ L"itmss:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itpc:%.txt",
+ L"itpc:%00.txt",
+ L"itpc:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itunes.assocprotocol.itms:%.txt",
+ L"itunes.assocprotocol.itms:%00.txt",
+ L"itunes.assocprotocol.itms:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itunes.assocprotocol.itmss:%.txt",
+ L"itunes.assocprotocol.itmss:%00.txt",
+ L"itunes.assocprotocol.itmss:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itunes.assocprotocol.itpc:%.txt",
+ L"itunes.assocprotocol.itpc:%00.txt",
+ L"itunes.assocprotocol.itpc:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ldap:%.txt",
+ L"ldap:%00.txt",
+ L"ldap:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mailto:%.txt",
+ L"mailto:%00.txt",
+ L"mailto:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mms:%.txt",
+ L"mms:%00.txt",
+ L"mms:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mmst:%.txt",
+ L"mmst:%00.txt",
+ L"mmst:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mmst:%.txt",
+ L"mmst:%00.txt",
+ L"mmst:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mmsu:%.txt",
+ L"mmsu:%00.txt",
+ L"mmsu:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mmsu:%.txt",
+ L"mmsu:%00.txt",
+ L"mmsu:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"https://bug389580.bmoattachments.org/"
+ L"Mozilla%20Thunderbird.Url.Mailto:%.txt",
+ L"https://bug389580.bmoattachments.org/"
+ L"Mozilla%20Thunderbird.Url.Mailto:%00.txt",
+ L"https://bug389580.bmoattachments.org/"
+ L"Mozilla%20Thunderbird.Url.Mailto:%41%2D%31%5Ftest%22ing?%41%31%00.txt",
+ L"navigatorurl:%.txt",
+ L"navigatorurl:%00.txt",
+ L"navigatorurl:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"news:%.txt",
+ L"news:%00.txt",
+ L"news:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"nntp:%.txt",
+ L"nntp:%00.txt",
+ L"nntp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"oms:%.txt",
+ L"oms:%00.txt",
+ L"oms:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"outlook:%.txt",
+ L"outlook:%00.txt",
+ L"outlook:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"outlook.url.feed:%.txt",
+ L"outlook.url.feed:%00.txt",
+ L"outlook.url.feed:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"outlook.url.mailto:%.txt",
+ L"outlook.url.mailto:%00.txt",
+ L"outlook.url.mailto:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"outlook.url.webcal:%.txt",
+ L"outlook.url.webcal:%00.txt",
+ L"outlook.url.webcal:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"outlookfeed:%.txt",
+ L"outlookfeed:%00.txt",
+ L"outlookfeed:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"outlookfeeds:%.txt",
+ L"outlookfeeds:%00.txt",
+ L"outlookfeeds:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"pnm:%.txt",
+ L"pnm:%00.txt",
+ L"pnm:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"prls.intappfile.ftp:%.txt",
+ L"prls.intappfile.ftp:%00.txt",
+ L"prls.intappfile.ftp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"prls.intappfile.http:%.txt",
+ L"prls.intappfile.http:%00.txt",
+ L"prls.intappfile.http:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"prls.intappfile.https:%.txt",
+ L"prls.intappfile.https:%00.txt",
+ L"prls.intappfile.https:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"prls.intappfile.mailto:%.txt",
+ L"prls.intappfile.mailto:%00.txt",
+ L"prls.intappfile.mailto:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"rlogin:%.txt",
+ L"rlogin:%00.txt",
+ L"rlogin:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"rtsp:%.txt",
+ L"rtsp:%00.txt",
+ L"rtsp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"scp:%.txt",
+ L"scp:%00.txt",
+ L"scp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"sftp:%.txt",
+ L"sftp:%00.txt",
+ L"sftp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"sip:%.txt",
+ L"sip:%00.txt",
+ L"sip:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"skype:%.txt",
+ L"skype:%00.txt",
+ L"skype:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"snews:%.txt",
+ L"snews:%00.txt",
+ L"snews:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"telnet:%.txt",
+ L"telnet:%00.txt",
+ L"telnet:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"thunderbird.url.mailto:%.txt",
+ L"thunderbird.url.mailto:%00.txt",
+ L"thunderbird.url.mailto:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"thunderbird.url.news:%.txt",
+ L"thunderbird.url.news:%00.txt",
+ L"thunderbird.url.news:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"tn3270:%.txt",
+ L"tn3270:%00.txt",
+ L"tn3270:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"tscrec4:%.txt",
+ L"tscrec4:%00.txt",
+ L"tscrec4:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"webcal:%.txt",
+ L"webcal:%00.txt",
+ L"webcal:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"webcal:%.txt",
+ L"webcal:%00.txt",
+ L"webcal:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"webcals:%.txt",
+ L"webcals:%00.txt",
+ L"webcals:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"windowscalendar.urlwebcal.1:%.txt",
+ L"windowscalendar.urlwebcal.1:%00.txt",
+ L"windowscalendar.urlwebcal.1:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"windowsmail.url.mailto:%.txt",
+ L"windowsmail.url.mailto:%00.txt",
+ L"windowsmail.url.mailto:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"windowsmail.url.news:%.txt",
+ L"windowsmail.url.news:%00.txt",
+ L"windowsmail.url.news:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"windowsmail.url.nntp:%.txt",
+ L"windowsmail.url.nntp:%00.txt",
+ L"windowsmail.url.nntp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"windowsmail.url.snews:%.txt",
+ L"windowsmail.url.snews:%00.txt",
+ L"windowsmail.url.snews:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"wmp11.assocprotocol.mms:%.txt",
+ L"wmp11.assocprotocol.mms:%00.txt",
+ L"wmp11.assocprotocol.mms:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"wpc:%.txt",
+ L"wpc:%00.txt",
+ L"wpc:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ymsgr:%.txt",
+ L"ymsgr:%00.txt",
+ L"ymsgr:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"acrobat:%.txt",
+ L"acrobat:%00.txt",
+ L"acrobat:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"acsui:%.txt",
+ L"acsui:%00.txt",
+ L"acsui:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"aim:%.txt",
+ L"aim:%00.txt",
+ L"aim:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"aim:%.txt",
+ L"aim:%00.txt",
+ L"aim:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"allc8.commands.2:%.txt",
+ L"allc8.commands.2:%00.txt",
+ L"allc8.commands.2:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"allholdem.commands.2:%.txt",
+ L"allholdem.commands.2:%00.txt",
+ L"allholdem.commands.2:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"allpoker.commands.2:%.txt",
+ L"allpoker.commands.2:%00.txt",
+ L"allpoker.commands.2:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"aolautofix:%.txt",
+ L"aolautofix:%00.txt",
+ L"aolautofix:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"aolds:%.txt",
+ L"aolds:%00.txt",
+ L"aolds:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"bc:%.txt",
+ L"bc:%00.txt",
+ L"bc:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"bctp:%.txt",
+ L"bctp:%00.txt",
+ L"bctp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"bittorrent:%.txt",
+ L"bittorrent:%00.txt",
+ L"bittorrent:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"camfrog:%.txt",
+ L"camfrog:%00.txt",
+ L"camfrog:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"csi:%.txt",
+ L"csi:%00.txt",
+ L"csi:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"cvs:%.txt",
+ L"cvs:%00.txt",
+ L"cvs:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"daap:%.txt",
+ L"daap:%00.txt",
+ L"daap:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ed2k:%.txt",
+ L"ed2k:%00.txt",
+ L"ed2k:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"explorer.assocprotocol.search-ms:%.txt",
+ L"explorer.assocprotocol.search-ms:%00.txt",
+ L"explorer.assocprotocol.search-ms:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"gizmoproject:%.txt",
+ L"gizmoproject:%00.txt",
+ L"gizmoproject:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"gnet:%.txt",
+ L"gnet:%00.txt",
+ L"gnet:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"gnutella:%.txt",
+ L"gnutella:%00.txt",
+ L"gnutella:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"gsarcade:%.txt",
+ L"gsarcade:%00.txt",
+ L"gsarcade:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"hcp:%.txt",
+ L"hcp:%00.txt",
+ L"hcp:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"icquser:%.txt",
+ L"icquser:%00.txt",
+ L"icquser:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"icy:%.txt",
+ L"icy:%00.txt",
+ L"icy:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"imesync:%.txt",
+ L"imesync:%00.txt",
+ L"imesync:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itunes.assocprotocol.daap:%.txt",
+ L"itunes.assocprotocol.daap:%00.txt",
+ L"itunes.assocprotocol.daap:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"itunes.assocprotocol.pcast:%.txt",
+ L"itunes.assocprotocol.pcast:%00.txt",
+ L"itunes.assocprotocol.pcast:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"joost:%.txt",
+ L"joost:%00.txt",
+ L"joost:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"m4macdrive:%.txt",
+ L"m4macdrive:%00.txt",
+ L"m4macdrive:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"magnet:%.txt",
+ L"magnet:%00.txt",
+ L"magnet:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mapi:%.txt",
+ L"mapi:%00.txt",
+ L"mapi:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mc12:%.txt",
+ L"mc12:%00.txt",
+ L"mc12:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mediajukebox:%.txt",
+ L"mediajukebox:%00.txt",
+ L"mediajukebox:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"morpheus:%.txt",
+ L"morpheus:%00.txt",
+ L"morpheus:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mp2p:%.txt",
+ L"mp2p:%00.txt",
+ L"mp2p:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"mpodcast:%.txt",
+ L"mpodcast:%00.txt",
+ L"mpodcast:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"msbd:%.txt",
+ L"msbd:%00.txt",
+ L"msbd:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"msbd:%.txt",
+ L"msbd:%00.txt",
+ L"msbd:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"msdigitallocker:%.txt",
+ L"msdigitallocker:%00.txt",
+ L"msdigitallocker:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"outlook.url.stssync:%.txt",
+ L"outlook.url.stssync:%00.txt",
+ L"outlook.url.stssync:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"p2p:%.txt",
+ L"p2p:%00.txt",
+ L"p2p:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"pando:%.txt",
+ L"pando:%00.txt",
+ L"pando:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"pcast:%.txt",
+ L"pcast:%00.txt",
+ L"pcast:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"picasa:%.txt",
+ L"picasa:%00.txt",
+ L"picasa:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"plaxo:%.txt",
+ L"plaxo:%00.txt",
+ L"plaxo:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"play:%.txt",
+ L"play:%00.txt",
+ L"play:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"podcast:%.txt",
+ L"podcast:%00.txt",
+ L"podcast:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ppmate:%.txt",
+ L"ppmate:%00.txt",
+ L"ppmate:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ppmates:%.txt",
+ L"ppmates:%00.txt",
+ L"ppmates:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ppstream:%.txt",
+ L"ppstream:%00.txt",
+ L"ppstream:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"quicktime:%.txt",
+ L"quicktime:%00.txt",
+ L"quicktime:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"realplayer.autoplay.6:%.txt",
+ L"realplayer.autoplay.6:%00.txt",
+ L"realplayer.autoplay.6:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"realplayer.cdburn.6:%.txt",
+ L"realplayer.cdburn.6:%00.txt",
+ L"realplayer.cdburn.6:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"rhap:%.txt",
+ L"rhap:%00.txt",
+ L"rhap:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"sc:%.txt",
+ L"sc:%00.txt",
+ L"sc:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"search-ms:%.txt",
+ L"search-ms:%00.txt",
+ L"search-ms:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"shareaza:%.txt",
+ L"shareaza:%00.txt",
+ L"shareaza:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"shell:%.txt",
+ L"shell:%00.txt",
+ L"shell:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"shout:%.txt",
+ L"shout:%00.txt",
+ L"shout:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"sig2dat:%.txt",
+ L"sig2dat:%00.txt",
+ L"sig2dat:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"sop:%.txt",
+ L"sop:%00.txt",
+ L"sop:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"steam:%.txt",
+ L"steam:%00.txt",
+ L"steam:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"stssync:%.txt",
+ L"stssync:%00.txt",
+ L"stssync:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"svn:%.txt",
+ L"svn:%00.txt",
+ L"svn:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"svn+ssh:%.txt",
+ L"svn+ssh:%00.txt",
+ L"svn+ssh:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"synacast:%.txt",
+ L"synacast:%00.txt",
+ L"synacast:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"torrent:%.txt",
+ L"torrent:%00.txt",
+ L"torrent:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"tsvn:%.txt",
+ L"tsvn:%00.txt",
+ L"tsvn:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"tvants:%.txt",
+ L"tvants:%00.txt",
+ L"tvants:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"tvu:%.txt",
+ L"tvu:%00.txt",
+ L"tvu:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"unsv:%.txt",
+ L"unsv:%00.txt",
+ L"unsv:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"uvox:%.txt",
+ L"uvox:%00.txt",
+ L"uvox:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"ventrilo:%.txt",
+ L"ventrilo:%00.txt",
+ L"ventrilo:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"vs:%.txt",
+ L"vs:%00.txt",
+ L"vs:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"zune:%.txt",
+ L"zune:%00.txt",
+ L"zune:%41%2D%31%5Ftest&quot;ing?%41%31%00.txt",
+ L"https://example.com/?a=123&b=456",
+ L"https://example.com/#123?a=123&b=456",
+ L"https://example.com/?#123a=123&b=456",
+ L"https://example.com/?a=123&b=456#123",
+ L"mailto:%41%42%23%31",
+ L"mailto:%41%42%23%31#fragment",
+ L"news:%41%42%23%31",
+ L"news:%41%42%23%31#fragment",
+ L"microsoft-edge:%41%42%23%31",
+ L"microsoft-edge:%41%42%23%31#fragment",
+ L"microsoft-edge:%41%42%23%31#fragment#",
+ L"microsoft-edge:%41%42%23%31####",
+ L"something-unknown:",
+ L"something-unknown:x=123",
+ L"something-unknown:?=123",
+ L"something-unknown:#code=0123456789%200123456789&x=01234567890123456789",
+};
+
+#endif // mozilla_TestUrisToValidate_h
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();
+}
diff --git a/widget/windows/tests/gtest/TestWinDND.cpp b/widget/windows/tests/gtest/TestWinDND.cpp
new file mode 100644
index 0000000000..fb7849fd79
--- /dev/null
+++ b/widget/windows/tests/gtest/TestWinDND.cpp
@@ -0,0 +1,728 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 <windows.h>
+#include <ole2.h>
+#include <shlobj.h>
+
+#include "nsArray.h"
+#include "nsArrayUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIFile.h"
+#include "nsNetUtil.h"
+#include "nsISupportsPrimitives.h"
+#include "nsITransferable.h"
+
+#include "nsClipboard.h"
+#include "nsDataObjCollection.h"
+
+#include "gtest/gtest.h"
+
+// shims for conversion from cppunittest to gtest
+template <size_t N>
+void fail(const char (&msg)[N]) {
+ ADD_FAILURE() << "TEST-UNEXPECTED-FAIL | " << msg;
+}
+template <size_t N>
+void passed(const char (&msg)[N]) {
+ GTEST_SUCCEED() << "TEST-PASS | " << msg;
+}
+
+nsIFile* xferFile;
+
+nsresult CheckValidHDROP(STGMEDIUM* pSTG) {
+ if (pSTG->tymed != TYMED_HGLOBAL) {
+ fail("Received data is not in an HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ HGLOBAL hGlobal = pSTG->hGlobal;
+ DROPFILES* pDropFiles;
+ pDropFiles = (DROPFILES*)GlobalLock(hGlobal);
+ if (!pDropFiles) {
+ fail("There is no data at the given HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (pDropFiles->pFiles != sizeof(DROPFILES)) {
+ fail("DROPFILES struct has wrong size");
+ }
+
+ if (!pDropFiles->fWide) {
+ fail("Received data is not Unicode");
+ return NS_ERROR_UNEXPECTED;
+ }
+ nsString s;
+ unsigned long offset = 0;
+ while (true) {
+ s = (char16_t*)((char*)pDropFiles + pDropFiles->pFiles + offset);
+ if (s.IsEmpty()) break;
+ nsresult rv;
+ nsCOMPtr<nsIFile> localFile(
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ rv = localFile->InitWithPath(s);
+ if (NS_FAILED(rv)) {
+ fail("File could not be opened");
+ return NS_ERROR_UNEXPECTED;
+ }
+ offset += sizeof(char16_t) * (s.Length() + 1);
+ }
+ return NS_OK;
+}
+
+nsresult CheckValidTEXT(STGMEDIUM* pSTG) {
+ if (pSTG->tymed != TYMED_HGLOBAL) {
+ fail("Received data is not in an HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ HGLOBAL hGlobal = pSTG->hGlobal;
+ char* pText;
+ pText = (char*)GlobalLock(hGlobal);
+ if (!pText) {
+ fail("There is no data at the given HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsCString string;
+ string = pText;
+
+ if (!string.EqualsLiteral("Mozilla can drag and drop")) {
+ fail("Text passed through drop object wrong");
+ return NS_ERROR_UNEXPECTED;
+ }
+ return NS_OK;
+}
+
+nsresult CheckValidTEXTTwo(STGMEDIUM* pSTG) {
+ if (pSTG->tymed != TYMED_HGLOBAL) {
+ fail("Received data is not in an HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ HGLOBAL hGlobal = pSTG->hGlobal;
+ char* pText;
+ pText = (char*)GlobalLock(hGlobal);
+ if (!pText) {
+ fail("There is no data at the given HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsCString string;
+ string = pText;
+
+ if (!string.EqualsLiteral("Mozilla can drag and drop twice over")) {
+ fail("Text passed through drop object wrong");
+ return NS_ERROR_UNEXPECTED;
+ }
+ return NS_OK;
+}
+
+nsresult CheckValidUNICODE(STGMEDIUM* pSTG) {
+ if (pSTG->tymed != TYMED_HGLOBAL) {
+ fail("Received data is not in an HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ HGLOBAL hGlobal = pSTG->hGlobal;
+ char16_t* pText;
+ pText = (char16_t*)GlobalLock(hGlobal);
+ if (!pText) {
+ fail("There is no data at the given HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsString string;
+ string = pText;
+
+ if (!string.EqualsLiteral("Mozilla can drag and drop")) {
+ fail("Text passed through drop object wrong");
+ return NS_ERROR_UNEXPECTED;
+ }
+ return NS_OK;
+}
+
+nsresult CheckValidUNICODETwo(STGMEDIUM* pSTG) {
+ if (pSTG->tymed != TYMED_HGLOBAL) {
+ fail("Received data is not in an HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ HGLOBAL hGlobal = pSTG->hGlobal;
+ char16_t* pText;
+ pText = (char16_t*)GlobalLock(hGlobal);
+ if (!pText) {
+ fail("There is no data at the given HGLOBAL");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsString string;
+ string = pText;
+
+ if (!string.EqualsLiteral("Mozilla can drag and drop twice over")) {
+ fail("Text passed through drop object wrong");
+ return NS_ERROR_UNEXPECTED;
+ }
+ return NS_OK;
+}
+
+nsresult GetTransferableFile(nsCOMPtr<nsITransferable>& pTransferable) {
+ nsresult rv;
+
+ nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferFile);
+
+ pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1");
+ pTransferable->Init(nullptr);
+ rv = pTransferable->SetTransferData("application/x-moz-file", genericWrapper);
+ return rv;
+}
+
+nsresult GetTransferableText(nsCOMPtr<nsITransferable>& pTransferable) {
+ nsresult rv;
+ constexpr auto mozString = u"Mozilla can drag and drop"_ns;
+ nsCOMPtr<nsISupportsString> xferString =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID);
+ rv = xferString->SetData(mozString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferString);
+
+ pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1");
+ pTransferable->Init(nullptr);
+ rv = pTransferable->SetTransferData("text/plain", genericWrapper);
+ return rv;
+}
+
+nsresult GetTransferableTextTwo(nsCOMPtr<nsITransferable>& pTransferable) {
+ nsresult rv;
+ constexpr auto mozString = u" twice over"_ns;
+ nsCOMPtr<nsISupportsString> xferString =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID);
+ rv = xferString->SetData(mozString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferString);
+
+ pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1");
+ pTransferable->Init(nullptr);
+ rv = pTransferable->SetTransferData("text/plain", genericWrapper);
+ return rv;
+}
+
+nsresult GetTransferableURI(nsCOMPtr<nsITransferable>& pTransferable) {
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> xferURI;
+
+ rv = NS_NewURI(getter_AddRefs(xferURI), "http://www.mozilla.org");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferURI);
+
+ pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1");
+ pTransferable->Init(nullptr);
+ rv = pTransferable->SetTransferData("text/x-moz-url", genericWrapper);
+ return rv;
+}
+
+nsresult MakeDataObject(nsIArray* transferableArray,
+ RefPtr<IDataObject>& itemToDrag) {
+ nsresult rv;
+ uint32_t itemCount = 0;
+
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), "http://www.mozilla.org");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transferableArray->GetLength(&itemCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Copied more or less exactly from nsDragService::InvokeDragSession
+ // This is what lets us play fake Drag Service for the test
+ if (itemCount > 1) {
+ nsDataObjCollection* dataObjCollection = new nsDataObjCollection();
+ if (!dataObjCollection) return NS_ERROR_OUT_OF_MEMORY;
+ itemToDrag = dataObjCollection;
+ for (uint32_t i = 0; i < itemCount; ++i) {
+ nsCOMPtr<nsITransferable> trans = do_QueryElementAt(transferableArray, i);
+ if (trans) {
+ RefPtr<IDataObject> dataObj;
+ rv = nsClipboard::CreateNativeDataObject(trans, getter_AddRefs(dataObj),
+ uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Add the flavors to the collection object too
+ rv = nsClipboard::SetupNativeDataObject(trans, dataObjCollection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ dataObjCollection->AddDataObject(dataObj);
+ }
+ }
+ } // if dragging multiple items
+ else {
+ nsCOMPtr<nsITransferable> trans = do_QueryElementAt(transferableArray, 0);
+ if (trans) {
+ rv = nsClipboard::CreateNativeDataObject(trans,
+ getter_AddRefs(itemToDrag), uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } // else dragging a single object
+ return rv;
+}
+
+nsresult Do_CheckOneFile() {
+ nsresult rv;
+ nsCOMPtr<nsITransferable> transferable;
+ nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
+ nsCOMPtr<nsISupports> genericWrapper;
+ RefPtr<IDataObject> dataObj;
+
+ rv = GetTransferableFile(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+
+ rv = MakeDataObject(transferableArray, dataObj);
+ if (NS_FAILED(rv)) {
+ fail("Could not create data object");
+ return rv;
+ }
+
+ FORMATETC fe;
+ SET_FORMATETC(fe, CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (dataObj->QueryGetData(&fe) != S_OK) {
+ fail("File data object does not support the file data type!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ STGMEDIUM* stg;
+ stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
+ if (dataObj->GetData(&fe, stg) != S_OK) {
+ fail("File data object did not provide data on request");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = CheckValidHDROP(stg);
+ if (NS_FAILED(rv)) {
+ fail("HDROP was invalid");
+ return rv;
+ }
+
+ ReleaseStgMedium(stg);
+
+ return NS_OK;
+}
+
+nsresult Do_CheckTwoFiles() {
+ nsresult rv;
+ nsCOMPtr<nsITransferable> transferable;
+ nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
+ nsCOMPtr<nsISupports> genericWrapper;
+ RefPtr<IDataObject> dataObj;
+
+ rv = GetTransferableFile(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+
+ rv = GetTransferableFile(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+
+ rv = MakeDataObject(transferableArray, dataObj);
+ if (NS_FAILED(rv)) {
+ fail("Could not create data object");
+ return rv;
+ }
+
+ FORMATETC fe;
+ SET_FORMATETC(fe, CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (dataObj->QueryGetData(&fe) != S_OK) {
+ fail("File data object does not support the file data type!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ STGMEDIUM* stg;
+ stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
+ if (dataObj->GetData(&fe, stg) != S_OK) {
+ fail("File data object did not provide data on request");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = CheckValidHDROP(stg);
+ if (NS_FAILED(rv)) {
+ fail("HDROP was invalid");
+ return rv;
+ }
+
+ ReleaseStgMedium(stg);
+
+ return NS_OK;
+}
+
+nsresult Do_CheckOneString() {
+ nsresult rv;
+ nsCOMPtr<nsITransferable> transferable;
+ nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
+ nsCOMPtr<nsISupports> genericWrapper;
+ RefPtr<IDataObject> dataObj;
+
+ rv = GetTransferableText(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+
+ rv = MakeDataObject(transferableArray, dataObj);
+ if (NS_FAILED(rv)) {
+ fail("Could not create data object");
+ return rv;
+ }
+
+ FORMATETC fe;
+ SET_FORMATETC(fe, CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (dataObj->QueryGetData(&fe) != S_OK) {
+ fail("String data object does not support the ASCII text data type!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ STGMEDIUM* stg;
+ stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
+ if (dataObj->GetData(&fe, stg) != S_OK) {
+ fail("String data object did not provide ASCII data on request");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = CheckValidTEXT(stg);
+ if (NS_FAILED(rv)) {
+ fail("TEXT was invalid");
+ return rv;
+ }
+
+ ReleaseStgMedium(stg);
+
+ SET_FORMATETC(fe, CF_UNICODETEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (dataObj->QueryGetData(&fe) != S_OK) {
+ fail("String data object does not support the wide text data type!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (dataObj->GetData(&fe, stg) != S_OK) {
+ fail("String data object did not provide wide data on request");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = CheckValidUNICODE(stg);
+ if (NS_FAILED(rv)) {
+ fail("UNICODE was invalid");
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult Do_CheckTwoStrings() {
+ nsresult rv;
+ nsCOMPtr<nsITransferable> transferable;
+ nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
+ nsCOMPtr<nsISupports> genericWrapper;
+ RefPtr<IDataObject> dataObj;
+
+ rv = GetTransferableText(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+
+ rv = GetTransferableTextTwo(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+
+ rv = MakeDataObject(transferableArray, dataObj);
+ if (NS_FAILED(rv)) {
+ fail("Could not create data object");
+ return rv;
+ }
+
+ FORMATETC fe;
+ SET_FORMATETC(fe, CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (dataObj->QueryGetData(&fe) != S_OK) {
+ fail("String data object does not support the ASCII text data type!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ STGMEDIUM* stg;
+ stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
+ if (dataObj->GetData(&fe, stg) != S_OK) {
+ fail("String data object did not provide ASCII data on request");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = CheckValidTEXTTwo(stg);
+ if (NS_FAILED(rv)) {
+ fail("TEXT was invalid");
+ return rv;
+ }
+
+ ReleaseStgMedium(stg);
+
+ SET_FORMATETC(fe, CF_UNICODETEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+ if (dataObj->QueryGetData(&fe) != S_OK) {
+ fail("String data object does not support the wide text data type!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (dataObj->GetData(&fe, stg) != S_OK) {
+ fail("String data object did not provide wide data on request");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = CheckValidUNICODETwo(stg);
+ if (NS_FAILED(rv)) {
+ fail("UNICODE was invalid");
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult Do_CheckSetArbitraryData(bool aMultiple) {
+ nsresult rv;
+ nsCOMPtr<nsITransferable> transferable;
+ nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
+ nsCOMPtr<nsISupports> genericWrapper;
+ RefPtr<IDataObject> dataObj;
+
+ rv = GetTransferableText(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+
+ if (aMultiple) {
+ rv = GetTransferableText(transferable);
+ if (NS_FAILED(rv)) {
+ fail("Could not create the proper nsITransferable!");
+ return rv;
+ }
+ genericWrapper = do_QueryInterface(transferable);
+ rv = transferableArray->AppendElement(genericWrapper);
+ if (NS_FAILED(rv)) {
+ fail("Could not append element to transferable array");
+ return rv;
+ }
+ }
+
+ rv = MakeDataObject(transferableArray, dataObj);
+ if (NS_FAILED(rv)) {
+ fail("Could not create data object");
+ return rv;
+ }
+
+ static CLIPFORMAT mozArbitraryFormat =
+ ::RegisterClipboardFormatW(L"MozillaTestFormat");
+ FORMATETC fe;
+ STGMEDIUM stg;
+ SET_FORMATETC(fe, mozArbitraryFormat, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
+
+ HGLOBAL hg = GlobalAlloc(GPTR, 1024);
+ stg.tymed = TYMED_HGLOBAL;
+ stg.hGlobal = hg;
+ stg.pUnkForRelease = nullptr;
+
+ if (dataObj->SetData(&fe, &stg, true) != S_OK) {
+ if (aMultiple) {
+ fail("Unable to set arbitrary data type on data object collection!");
+ } else {
+ fail("Unable to set arbitrary data type on data object!");
+ }
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (dataObj->QueryGetData(&fe) != S_OK) {
+ fail("Arbitrary data set on data object is not advertised!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ STGMEDIUM* stg2;
+ stg2 = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
+ if (dataObj->GetData(&fe, stg2) != S_OK) {
+ fail("Data object did not provide arbitrary data upon request!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (stg2->hGlobal != hg) {
+ fail("Arbitrary data was not returned properly!");
+ return rv;
+ }
+ ReleaseStgMedium(stg2);
+
+ return NS_OK;
+}
+
+// This function performs basic drop tests, testing a data object consisting
+// of one transferable
+nsresult Do_Test1() {
+ nsresult rv = NS_OK;
+ nsresult workingrv;
+
+ workingrv = Do_CheckOneFile();
+ if (NS_FAILED(workingrv)) {
+ fail("Drag object tests failed on a single file");
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ passed("Successfully created a working file drag object!");
+ }
+
+ workingrv = Do_CheckOneString();
+ if (NS_FAILED(workingrv)) {
+ fail("Drag object tests failed on a single string");
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ passed("Successfully created a working string drag object!");
+ }
+
+ workingrv = Do_CheckSetArbitraryData(false);
+ if (NS_FAILED(workingrv)) {
+ fail("Drag object tests failed on setting arbitrary data");
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ passed("Successfully set arbitrary data on a drag object");
+ }
+
+ return rv;
+}
+
+// This function performs basic drop tests, testing a data object consisting of
+// two transferables.
+nsresult Do_Test2() {
+ nsresult rv = NS_OK;
+ nsresult workingrv;
+
+ workingrv = Do_CheckTwoFiles();
+ if (NS_FAILED(workingrv)) {
+ fail("Drag object tests failed on multiple files");
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ passed("Successfully created a working multiple file drag object!");
+ }
+
+ workingrv = Do_CheckTwoStrings();
+ if (NS_FAILED(workingrv)) {
+ fail("Drag object tests failed on multiple strings");
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ passed("Successfully created a working multiple string drag object!");
+ }
+
+ workingrv = Do_CheckSetArbitraryData(true);
+ if (NS_FAILED(workingrv)) {
+ fail("Drag object tests failed on setting arbitrary data");
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ passed("Successfully set arbitrary data on a drag object");
+ }
+
+ return rv;
+}
+
+// This function performs advanced drag and drop tests, testing a data object
+// consisting of multiple transferables that have different data types
+nsresult Do_Test3() {
+ nsresult rv = NS_OK;
+ // nsresult workingrv;
+
+ // XXX TODO Write more advanced tests in Bug 535860
+ return rv;
+}
+
+nsCOMPtr<nsIFile> GetTemporaryDirectory() {
+ nsCOMPtr<nsIFile> tmpdir;
+
+#define ENSURE(expr) NS_ENSURE_SUCCESS(expr, nullptr);
+
+ ENSURE(NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpdir)));
+ MOZ_ASSERT(tmpdir);
+
+ ENSURE(tmpdir->AppendNative("TestWinDND"_ns));
+ ENSURE(tmpdir->CreateUnique(nsIFile::DIRECTORY_TYPE, 0777));
+
+#undef ENSURE
+
+ return tmpdir;
+}
+
+TEST(TestWinDND, All)
+{
+ nsCOMPtr<nsIFile> file = GetTemporaryDirectory();
+ if (!file) {
+ fail("could not create temporary directory!");
+ return;
+ }
+ xferFile = file;
+
+ if (NS_SUCCEEDED(Do_Test1())) {
+ passed(
+ "Basic Drag and Drop data type tests (single transferable) succeeded!");
+ }
+
+ if (NS_SUCCEEDED(Do_Test2())) {
+ passed(
+ "Basic Drag and Drop data type tests (multiple transferables) "
+ "succeeded!");
+ }
+
+ // if (NS_SUCCEEDED(Do_Test3()))
+ // passed("Advanced Drag and Drop data type tests succeeded!");
+}
diff --git a/widget/windows/tests/gtest/moz.build b/widget/windows/tests/gtest/moz.build
new file mode 100644
index 0000000000..4057b7ae57
--- /dev/null
+++ b/widget/windows/tests/gtest/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES += [
+ "TestJumpListBuilder.cpp",
+ "TestWinDND.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/widget",
+ "/widget/windows",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/widget/windows/tests/moz.build b/widget/windows/tests/moz.build
new file mode 100644
index 0000000000..2c7d200571
--- /dev/null
+++ b/widget/windows/tests/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+GeckoCppUnitTests(
+ [
+ "TestUriValidation",
+ ],
+ linkage=None,
+)
+
+DIRS = ["gtest"]
+
+LOCAL_INCLUDES += []
+
+OS_LIBS += [
+ "oleaut32",
+ "ole32",
+ "shell32",
+ "shlwapi",
+ "urlmon",
+ "uuid",
+]
+
+if CONFIG["OS_TARGET"] == "WINNT" and CONFIG["CC_TYPE"] in ("gcc", "clang"):
+ # This allows us to use wmain as the entry point on mingw
+ LDFLAGS += [
+ "-municode",
+ ]
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.toml"]
diff --git a/widget/windows/tests/unit/test_windows_alert_service.js b/widget/windows/tests/unit/test_windows_alert_service.js
new file mode 100644
index 0000000000..0ba0d2a4d4
--- /dev/null
+++ b/widget/windows/tests/unit/test_windows_alert_service.js
@@ -0,0 +1,667 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that Windows alert notifications generate expected XML.
+ */
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+let gProfD = do_get_profile();
+
+// Setup that allows to use the profile service in xpcshell tests,
+// lifted from `toolkit/profile/xpcshell/head.js`.
+function setupProfileService() {
+ let gDataHome = gProfD.clone();
+ gDataHome.append("data");
+ gDataHome.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ let gDataHomeLocal = gProfD.clone();
+ gDataHomeLocal.append("local");
+ gDataHomeLocal.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+ let xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].getService(
+ Ci.nsIXREDirProvider
+ );
+ xreDirProvider.setUserDataDirectory(gDataHome, false);
+ xreDirProvider.setUserDataDirectory(gDataHomeLocal, true);
+}
+
+add_setup(setupProfileService);
+
+function makeAlert(options) {
+ var alert = Cc["@mozilla.org/alert-notification;1"].createInstance(
+ Ci.nsIAlertNotification
+ );
+ alert.init(
+ options.name,
+ options.imageURL,
+ options.title,
+ options.text,
+ options.textClickable,
+ options.cookie,
+ options.dir,
+ options.lang,
+ options.data,
+ options.principal,
+ options.inPrivateBrowsing,
+ options.requireInteraction,
+ options.silent,
+ options.vibrate || []
+ );
+ if (options.actions) {
+ alert.actions = options.actions;
+ }
+ if (options.opaqueRelaunchData) {
+ alert.opaqueRelaunchData = options.opaqueRelaunchData;
+ }
+ return alert;
+}
+
+/**
+ * Take a `key1\nvalue1\n...` string encoding as used by the Windows native
+ * notification server DLL, and split it into an object, keeping `action\n...`
+ * intact.
+ *
+ * @param {string} t string encoding.
+ * @returns {object} an object with keys and values.
+ */
+function parseOneEncoded(t) {
+ var launch = {};
+
+ var lines = t.split("\n");
+ while (lines.length) {
+ var key = lines.shift();
+ var value;
+ if (key === "action") {
+ value = lines.join("\n");
+ lines = [];
+ } else {
+ value = lines.shift();
+ }
+ launch[key] = value;
+ }
+
+ return launch;
+}
+
+/**
+ * This complicated-looking function takes a (XML) string representation of a
+ * Windows alert (toast notification), parses it into XML, extracts and further
+ * parses internal data, and returns a simplified XML representation together
+ * with the parsed internals.
+ *
+ * Doing this lets us compare JSON objects rather than stringified-JSON further
+ * encoded as XML strings, which have lots of slashes and `&quot;` characters to
+ * contend with.
+ *
+ * @param {string} s XML string for Windows alert.
+
+ * @returns {Array} a pair of a simplified XML string and an object with
+ * `launch` and `actions` keys.
+ */
+function parseLaunchAndActions(s) {
+ var document = new DOMParser().parseFromString(s, "text/xml");
+ var root = document.documentElement;
+
+ var launchString = root.getAttribute("launch");
+ root.setAttribute("launch", "launch");
+ var launch = parseOneEncoded(launchString);
+
+ // `actions` is keyed by "content" attribute.
+ let actions = {};
+ for (var actionElement of root.querySelectorAll("action")) {
+ // `activationType="system"` is special. Leave them alone.
+ let systemActivationType =
+ actionElement.getAttribute("activationType") === "system";
+
+ let action = {};
+ let names = [...actionElement.attributes].map(attribute => attribute.name);
+
+ for (var name of names) {
+ let value = actionElement.getAttribute(name);
+
+ // Here is where we parse stringified-JSON to simplify comparisons.
+ if (value.startsWith("{")) {
+ value = JSON.parse(value);
+ if ("opaqueRelaunchData" in value) {
+ value.opaqueRelaunchData = JSON.parse(value.opaqueRelaunchData);
+ }
+ }
+
+ if (name == "arguments" && !systemActivationType) {
+ action[name] = parseOneEncoded(value);
+ } else {
+ action[name] = value;
+ }
+
+ if (name != "content" && !systemActivationType) {
+ actionElement.removeAttribute(name);
+ }
+ }
+
+ let actionName = actionElement.getAttribute("content");
+ actions[actionName] = action;
+ }
+
+ return [new XMLSerializer().serializeToString(document), { launch, actions }];
+}
+
+function escape(s) {
+ return s
+ .replace(/&/g, "&amp;")
+ .replace(/"/g, "&quot;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;")
+ .replace(/\n/g, "&#xA;");
+}
+
+function unescape(s) {
+ return s
+ .replace(/&amp;/g, "&")
+ .replace(/&quot;/g, '"')
+ .replace(/&lt;/g, "<")
+ .replace(/&gt;/g, ">")
+ .replace(/&#xA;/g, "\n");
+}
+
+function testAlert(when, { serverEnabled, profD, isBackgroundTaskMode } = {}) {
+ let argumentString = action => {
+ // &#xA; is "\n".
+ let s = ``;
+ if (serverEnabled) {
+ s += `program&#xA;${AppConstants.MOZ_APP_NAME}`;
+ } else {
+ s += `invalid key&#xA;invalid value`;
+ }
+ if (serverEnabled && profD) {
+ s += `&#xA;profile&#xA;${profD.path}`;
+ }
+ if (serverEnabled) {
+ s += "&#xA;windowsTag&#xA;";
+ }
+ if (action) {
+ s += `&#xA;action&#xA;${escape(JSON.stringify(action))}`;
+ }
+
+ return s;
+ };
+
+ let parsedArgumentString = action =>
+ parseOneEncoded(unescape(argumentString(action)));
+
+ let settingsAction = isBackgroundTaskMode
+ ? ""
+ : `<action content="Notification settings"/>`;
+
+ let parsedSettingsAction = hostport => {
+ if (isBackgroundTaskMode) {
+ return [];
+ }
+ let content = "Notification settings";
+ return [
+ content,
+ {
+ content,
+ arguments: parsedArgumentString(
+ Object.assign(
+ {
+ action: "settings",
+ },
+ hostport && {
+ launchUrl: hostport,
+ }
+ )
+ ),
+ placement: "contextmenu",
+ },
+ ];
+ };
+
+ let parsedSnoozeAction = hostport => {
+ let content = `Disable notifications from ${hostport}`;
+ return [
+ content,
+ {
+ content,
+ arguments: parsedArgumentString(
+ Object.assign(
+ {
+ action: "snooze",
+ },
+ hostport && {
+ launchUrl: hostport,
+ }
+ )
+ ),
+ placement: "contextmenu",
+ },
+ ];
+ };
+
+ let alertsService = Cc["@mozilla.org/system-alerts-service;1"]
+ .getService(Ci.nsIAlertsService)
+ .QueryInterface(Ci.nsIWindowsAlertsService);
+
+ let name = "name";
+ let title = "title";
+ let text = "text";
+ let imageURL = "file:///image.png";
+ let actions = [
+ { action: "action1", title: "title1", iconURL: "file:///iconURL1.png" },
+ { action: "action2", title: "title2", iconURL: "file:///iconURL2.png" },
+ ];
+ let opaqueRelaunchData = { foo: 1, bar: "two" };
+
+ let alert = makeAlert({ name, title, text });
+ let expected = `<toast launch="launch"><visual><binding template="ToastText03"><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}</actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({ action: "" }),
+ actions: Object.fromEntries(
+ [parsedSettingsAction()].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ alert = makeAlert({ name, title, text, imageURL });
+ expected = `<toast launch="launch"><visual><binding template="ToastImageAndText03"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}</actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({ action: "" }),
+ actions: Object.fromEntries(
+ [parsedSettingsAction()].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ alert = makeAlert({ name, title, text, imageURL, requireInteraction: true });
+ expected = `<toast scenario="reminder" launch="launch"><visual><binding template="ToastImageAndText03"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="Dismiss" arguments="dismiss" activationType="system"/></actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({ action: "" }),
+ actions: Object.fromEntries(
+ [
+ parsedSettingsAction(),
+ [
+ "Dismiss",
+ {
+ content: "Dismiss",
+ arguments: "dismiss",
+ activationType: "system",
+ },
+ ],
+ ].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ alert = makeAlert({ name, title, text, imageURL, actions });
+ expected = `<toast launch="launch"><visual><binding template="ToastImageAndText03"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="title1"/><action content="title2"/></actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({ action: "" }),
+ actions: Object.fromEntries(
+ [
+ parsedSettingsAction(),
+ [
+ "title1",
+ {
+ content: "title1",
+ arguments: parsedArgumentString({ action: "action1" }),
+ },
+ ],
+ [
+ "title2",
+ {
+ content: "title2",
+ arguments: parsedArgumentString({ action: "action2" }),
+ },
+ ],
+ ].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ // Chrome privileged alerts can use `windowsSystemActivationType`.
+ let systemActions = [
+ {
+ action: "dismiss",
+ title: "dismissTitle",
+ windowsSystemActivationType: true,
+ },
+ {
+ action: "snooze",
+ title: "snoozeTitle",
+ windowsSystemActivationType: true,
+ },
+ ];
+ let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ alert = makeAlert({
+ name,
+ title,
+ text,
+ imageURL,
+ principal: systemPrincipal,
+ actions: systemActions,
+ });
+ let parsedSettingsActionWithPrivilegedName = isBackgroundTaskMode
+ ? []
+ : [
+ "Notification settings",
+ {
+ content: "Notification settings",
+ arguments: parsedArgumentString({
+ action: "settings",
+ privilegedName: name,
+ }),
+ placement: "contextmenu",
+ },
+ ];
+
+ expected = `<toast launch="launch"><visual><binding template="ToastGeneric"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="dismissTitle" arguments="dismiss" activationType="system"/><action content="snoozeTitle" arguments="snooze" activationType="system"/></actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({ action: "", privilegedName: name }),
+ actions: Object.fromEntries(
+ [
+ parsedSettingsActionWithPrivilegedName,
+ [
+ "dismissTitle",
+ {
+ content: "dismissTitle",
+ arguments: "dismiss",
+ activationType: "system",
+ },
+ ],
+ [
+ "snoozeTitle",
+ {
+ content: "snoozeTitle",
+ arguments: "snooze",
+ activationType: "system",
+ },
+ ],
+ ].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ // But content unprivileged alerts can't use `windowsSystemActivationType`.
+ let launchUrl = "https://example.com/foo/bar.html";
+ const principaluri = Services.io.newURI(launchUrl);
+ const principal = Services.scriptSecurityManager.createContentPrincipal(
+ principaluri,
+ {}
+ );
+
+ alert = makeAlert({
+ name,
+ title,
+ text,
+ imageURL,
+ actions: systemActions,
+ principal,
+ });
+ expected = `<toast launch="launch"><visual><binding template="ToastImageAndText04"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text><text id="3" placement="attribution">via example.com</text></binding></visual><actions><action content="Disable notifications from example.com"/>${settingsAction}<action content="dismissTitle"/><action content="snoozeTitle"/></actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({
+ action: "",
+ launchUrl: principaluri.hostPort,
+ }),
+ actions: Object.fromEntries(
+ [
+ parsedSnoozeAction(principaluri.hostPort),
+ parsedSettingsAction(principaluri.hostPort),
+ [
+ "dismissTitle",
+ {
+ content: "dismissTitle",
+ arguments: parsedArgumentString({
+ action: "dismiss",
+ launchUrl: principaluri.hostPort,
+ }),
+ },
+ ],
+ [
+ "snoozeTitle",
+ {
+ content: "snoozeTitle",
+ arguments: parsedArgumentString({
+ action: "snooze",
+ launchUrl: principaluri.hostPort,
+ }),
+ },
+ ],
+ ].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ // Chrome privileged alerts can set `opaqueRelaunchData`.
+ alert = makeAlert({
+ name,
+ title,
+ text,
+ imageURL,
+ principal: systemPrincipal,
+ opaqueRelaunchData: JSON.stringify(opaqueRelaunchData),
+ });
+ expected = `<toast launch="launch"><visual><binding template="ToastGeneric"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}</actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({
+ action: "",
+ opaqueRelaunchData: JSON.stringify(opaqueRelaunchData),
+ privilegedName: name,
+ }),
+ actions: Object.fromEntries(
+ [parsedSettingsActionWithPrivilegedName].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ // But content unprivileged alerts can't set `opaqueRelaunchData`.
+ alert = makeAlert({
+ name,
+ title,
+ text,
+ imageURL,
+ principal,
+ opaqueRelaunchData: JSON.stringify(opaqueRelaunchData),
+ });
+ expected = `<toast launch="launch"><visual><binding template="ToastImageAndText04"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text><text id="3" placement="attribution">via example.com</text></binding></visual><actions><action content="Disable notifications from example.com"/>${settingsAction}</actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({
+ action: "",
+ launchUrl: principaluri.hostPort,
+ }),
+ actions: Object.fromEntries(
+ [
+ parsedSnoozeAction(principaluri.hostPort),
+ parsedSettingsAction(principaluri.hostPort),
+ ].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+
+ // Chrome privileged alerts can set action-specific relaunch parameters.
+ let systemRelaunchActions = [
+ {
+ action: "action1",
+ title: "title1",
+ opaqueRelaunchData: JSON.stringify({ json: "data1" }),
+ },
+ {
+ action: "action2",
+ title: "title2",
+ opaqueRelaunchData: JSON.stringify({ json: "data2" }),
+ },
+ ];
+ systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ alert = makeAlert({
+ name,
+ title,
+ text,
+ imageURL,
+ principal: systemPrincipal,
+ actions: systemRelaunchActions,
+ });
+ expected = `<toast launch="launch"><visual><binding template="ToastGeneric"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="title1"/><action content="title2"/></actions></toast>`;
+ Assert.deepEqual(
+ [
+ expected.replace("<actions></actions>", "<actions/>"),
+ {
+ launch: parsedArgumentString({ action: "", privilegedName: name }),
+ actions: Object.fromEntries(
+ [
+ parsedSettingsActionWithPrivilegedName,
+ [
+ "title1",
+ {
+ content: "title1",
+ arguments: parsedArgumentString(
+ {
+ action: "action1",
+ opaqueRelaunchData: JSON.stringify({ json: "data1" }),
+ privilegedName: name,
+ },
+ null,
+ name
+ ),
+ },
+ ],
+
+ [
+ "title2",
+ {
+ content: "title2",
+ arguments: parsedArgumentString(
+ {
+ action: "action2",
+ opaqueRelaunchData: JSON.stringify({ json: "data2" }),
+ privilegedName: name,
+ },
+ null,
+ name
+ ),
+ },
+ ],
+ ].filter(x => x.length)
+ ),
+ },
+ ],
+ parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)),
+ when
+ );
+}
+
+add_task(async () => {
+ Services.prefs.deleteBranch(
+ "alerts.useSystemBackend.windows.notificationserver.enabled"
+ );
+ testAlert("when notification server pref is unset", {
+ profD: gProfD,
+ });
+
+ Services.prefs.setBoolPref(
+ "alerts.useSystemBackend.windows.notificationserver.enabled",
+ false
+ );
+ testAlert("when notification server pref is false", { profD: gProfD });
+
+ Services.prefs.setBoolPref(
+ "alerts.useSystemBackend.windows.notificationserver.enabled",
+ true
+ );
+ testAlert("when notification server pref is true", {
+ serverEnabled: true,
+ profD: gProfD,
+ });
+});
+
+let condition = {
+ skip_if: () => !AppConstants.MOZ_BACKGROUNDTASKS,
+};
+
+add_task(condition, async () => {
+ const bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
+ Ci.nsIBackgroundTasks
+ );
+
+ // Pretend that this is a background task.
+ bts.overrideBackgroundTaskNameForTesting("taskname");
+
+ Services.prefs.setBoolPref(
+ "alerts.useSystemBackend.windows.notificationserver.enabled",
+ true
+ );
+ testAlert(
+ "when notification server pref is true in background task, no default profile",
+ { serverEnabled: true, isBackgroundTaskMode: true }
+ );
+
+ let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
+ Ci.nsIToolkitProfileService
+ );
+
+ let profilePath = do_get_profile();
+ profilePath.append(`test_windows_alert_service`);
+ let profile = profileService.createUniqueProfile(
+ profilePath,
+ "test_windows_alert_service"
+ );
+
+ profileService.defaultProfile = profile;
+
+ testAlert(
+ "when notification server pref is true in background task, default profile",
+ { serverEnabled: true, isBackgroundTaskMode: true, profD: profilePath }
+ );
+
+ // No longer a background task,
+ bts.overrideBackgroundTaskNameForTesting("");
+});
diff --git a/widget/windows/tests/unit/xpcshell.toml b/widget/windows/tests/unit/xpcshell.toml
new file mode 100644
index 0000000000..9943d5510e
--- /dev/null
+++ b/widget/windows/tests/unit/xpcshell.toml
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+["test_windows_alert_service.js"]