diff options
Diffstat (limited to 'widget/windows/tests')
-rw-r--r-- | widget/windows/tests/TestUriValidation.cpp | 135 | ||||
-rw-r--r-- | widget/windows/tests/TestUrisToValidate.h | 471 | ||||
-rw-r--r-- | widget/windows/tests/gtest/TestJumpListBuilder.cpp | 823 | ||||
-rw-r--r-- | widget/windows/tests/gtest/TestWinDND.cpp | 728 | ||||
-rw-r--r-- | widget/windows/tests/gtest/moz.build | 19 | ||||
-rw-r--r-- | widget/windows/tests/moz.build | 33 | ||||
-rw-r--r-- | widget/windows/tests/unit/test_windows_alert_service.js | 667 | ||||
-rw-r--r-- | widget/windows/tests/unit/xpcshell.toml | 3 |
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"ing?%41%31%00.txt",
+ L"fdaction:%.txt",
+ L"fdaction:%00.txt",
+ L"fdaction:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"feed:%.txt",
+ L"feed:%00.txt",
+ L"feed:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"feeds:%.txt",
+ L"feeds:%00.txt",
+ L"feeds:%41%2D%31%5Ftest"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"ing?%41%31%00.txt",
+ L"firefoxurl:%.txt",
+ L"firefoxurl:%00.txt",
+ L"firefoxurl:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ftp:%.txt",
+ L"ftp:%00.txt",
+ L"ftp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"gopher:%.txt",
+ L"gopher:%00.txt",
+ L"gopher:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"gtalk:%.txt",
+ L"gtalk:%00.txt",
+ L"gtalk:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"HTTP:%.txt",
+ L"HTTP:%00.txt",
+ L"HTTP:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"http:%.txt",
+ L"http:%00.txt",
+ L"http:%41%2D%31%5Ftest"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"ing?%41%31%00.txt",
+ L"ie.http:%.txt",
+ L"ie.http:%00.txt",
+ L"ie.http:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ie.https:%.txt",
+ L"ie.https:%00.txt",
+ L"ie.https:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"irc:%.txt",
+ L"irc:%00.txt",
+ L"irc:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ircs:%.txt",
+ L"ircs:%00.txt",
+ L"ircs:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itms:%.txt",
+ L"itms:%00.txt",
+ L"itms:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itmss:%.txt",
+ L"itmss:%00.txt",
+ L"itmss:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itpc:%.txt",
+ L"itpc:%00.txt",
+ L"itpc:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itunes.assocprotocol.itms:%.txt",
+ L"itunes.assocprotocol.itms:%00.txt",
+ L"itunes.assocprotocol.itms:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itunes.assocprotocol.itmss:%.txt",
+ L"itunes.assocprotocol.itmss:%00.txt",
+ L"itunes.assocprotocol.itmss:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itunes.assocprotocol.itpc:%.txt",
+ L"itunes.assocprotocol.itpc:%00.txt",
+ L"itunes.assocprotocol.itpc:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ldap:%.txt",
+ L"ldap:%00.txt",
+ L"ldap:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mailto:%.txt",
+ L"mailto:%00.txt",
+ L"mailto:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mms:%.txt",
+ L"mms:%00.txt",
+ L"mms:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mmst:%.txt",
+ L"mmst:%00.txt",
+ L"mmst:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mmst:%.txt",
+ L"mmst:%00.txt",
+ L"mmst:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mmsu:%.txt",
+ L"mmsu:%00.txt",
+ L"mmsu:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mmsu:%.txt",
+ L"mmsu:%00.txt",
+ L"mmsu:%41%2D%31%5Ftest"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"ing?%41%31%00.txt",
+ L"news:%.txt",
+ L"news:%00.txt",
+ L"news:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"nntp:%.txt",
+ L"nntp:%00.txt",
+ L"nntp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"oms:%.txt",
+ L"oms:%00.txt",
+ L"oms:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"outlook:%.txt",
+ L"outlook:%00.txt",
+ L"outlook:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"outlook.url.feed:%.txt",
+ L"outlook.url.feed:%00.txt",
+ L"outlook.url.feed:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"outlook.url.mailto:%.txt",
+ L"outlook.url.mailto:%00.txt",
+ L"outlook.url.mailto:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"outlook.url.webcal:%.txt",
+ L"outlook.url.webcal:%00.txt",
+ L"outlook.url.webcal:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"outlookfeed:%.txt",
+ L"outlookfeed:%00.txt",
+ L"outlookfeed:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"outlookfeeds:%.txt",
+ L"outlookfeeds:%00.txt",
+ L"outlookfeeds:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"pnm:%.txt",
+ L"pnm:%00.txt",
+ L"pnm:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"prls.intappfile.ftp:%.txt",
+ L"prls.intappfile.ftp:%00.txt",
+ L"prls.intappfile.ftp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"prls.intappfile.http:%.txt",
+ L"prls.intappfile.http:%00.txt",
+ L"prls.intappfile.http:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"prls.intappfile.https:%.txt",
+ L"prls.intappfile.https:%00.txt",
+ L"prls.intappfile.https:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"prls.intappfile.mailto:%.txt",
+ L"prls.intappfile.mailto:%00.txt",
+ L"prls.intappfile.mailto:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"rlogin:%.txt",
+ L"rlogin:%00.txt",
+ L"rlogin:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"rtsp:%.txt",
+ L"rtsp:%00.txt",
+ L"rtsp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"scp:%.txt",
+ L"scp:%00.txt",
+ L"scp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"sftp:%.txt",
+ L"sftp:%00.txt",
+ L"sftp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"sip:%.txt",
+ L"sip:%00.txt",
+ L"sip:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"skype:%.txt",
+ L"skype:%00.txt",
+ L"skype:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"snews:%.txt",
+ L"snews:%00.txt",
+ L"snews:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"telnet:%.txt",
+ L"telnet:%00.txt",
+ L"telnet:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"thunderbird.url.mailto:%.txt",
+ L"thunderbird.url.mailto:%00.txt",
+ L"thunderbird.url.mailto:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"thunderbird.url.news:%.txt",
+ L"thunderbird.url.news:%00.txt",
+ L"thunderbird.url.news:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"tn3270:%.txt",
+ L"tn3270:%00.txt",
+ L"tn3270:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"tscrec4:%.txt",
+ L"tscrec4:%00.txt",
+ L"tscrec4:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"webcal:%.txt",
+ L"webcal:%00.txt",
+ L"webcal:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"webcal:%.txt",
+ L"webcal:%00.txt",
+ L"webcal:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"webcals:%.txt",
+ L"webcals:%00.txt",
+ L"webcals:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"windowscalendar.urlwebcal.1:%.txt",
+ L"windowscalendar.urlwebcal.1:%00.txt",
+ L"windowscalendar.urlwebcal.1:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"windowsmail.url.mailto:%.txt",
+ L"windowsmail.url.mailto:%00.txt",
+ L"windowsmail.url.mailto:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"windowsmail.url.news:%.txt",
+ L"windowsmail.url.news:%00.txt",
+ L"windowsmail.url.news:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"windowsmail.url.nntp:%.txt",
+ L"windowsmail.url.nntp:%00.txt",
+ L"windowsmail.url.nntp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"windowsmail.url.snews:%.txt",
+ L"windowsmail.url.snews:%00.txt",
+ L"windowsmail.url.snews:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"wmp11.assocprotocol.mms:%.txt",
+ L"wmp11.assocprotocol.mms:%00.txt",
+ L"wmp11.assocprotocol.mms:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"wpc:%.txt",
+ L"wpc:%00.txt",
+ L"wpc:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ymsgr:%.txt",
+ L"ymsgr:%00.txt",
+ L"ymsgr:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"acrobat:%.txt",
+ L"acrobat:%00.txt",
+ L"acrobat:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"acsui:%.txt",
+ L"acsui:%00.txt",
+ L"acsui:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"aim:%.txt",
+ L"aim:%00.txt",
+ L"aim:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"aim:%.txt",
+ L"aim:%00.txt",
+ L"aim:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"allc8.commands.2:%.txt",
+ L"allc8.commands.2:%00.txt",
+ L"allc8.commands.2:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"allholdem.commands.2:%.txt",
+ L"allholdem.commands.2:%00.txt",
+ L"allholdem.commands.2:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"allpoker.commands.2:%.txt",
+ L"allpoker.commands.2:%00.txt",
+ L"allpoker.commands.2:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"aolautofix:%.txt",
+ L"aolautofix:%00.txt",
+ L"aolautofix:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"aolds:%.txt",
+ L"aolds:%00.txt",
+ L"aolds:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"bc:%.txt",
+ L"bc:%00.txt",
+ L"bc:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"bctp:%.txt",
+ L"bctp:%00.txt",
+ L"bctp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"bittorrent:%.txt",
+ L"bittorrent:%00.txt",
+ L"bittorrent:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"camfrog:%.txt",
+ L"camfrog:%00.txt",
+ L"camfrog:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"csi:%.txt",
+ L"csi:%00.txt",
+ L"csi:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"cvs:%.txt",
+ L"cvs:%00.txt",
+ L"cvs:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"daap:%.txt",
+ L"daap:%00.txt",
+ L"daap:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ed2k:%.txt",
+ L"ed2k:%00.txt",
+ L"ed2k:%41%2D%31%5Ftest"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"ing?%41%31%00.txt",
+ L"gizmoproject:%.txt",
+ L"gizmoproject:%00.txt",
+ L"gizmoproject:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"gnet:%.txt",
+ L"gnet:%00.txt",
+ L"gnet:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"gnutella:%.txt",
+ L"gnutella:%00.txt",
+ L"gnutella:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"gsarcade:%.txt",
+ L"gsarcade:%00.txt",
+ L"gsarcade:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"hcp:%.txt",
+ L"hcp:%00.txt",
+ L"hcp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"icquser:%.txt",
+ L"icquser:%00.txt",
+ L"icquser:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"icy:%.txt",
+ L"icy:%00.txt",
+ L"icy:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"imesync:%.txt",
+ L"imesync:%00.txt",
+ L"imesync:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itunes.assocprotocol.daap:%.txt",
+ L"itunes.assocprotocol.daap:%00.txt",
+ L"itunes.assocprotocol.daap:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itunes.assocprotocol.pcast:%.txt",
+ L"itunes.assocprotocol.pcast:%00.txt",
+ L"itunes.assocprotocol.pcast:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"joost:%.txt",
+ L"joost:%00.txt",
+ L"joost:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"m4macdrive:%.txt",
+ L"m4macdrive:%00.txt",
+ L"m4macdrive:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"magnet:%.txt",
+ L"magnet:%00.txt",
+ L"magnet:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mapi:%.txt",
+ L"mapi:%00.txt",
+ L"mapi:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mc12:%.txt",
+ L"mc12:%00.txt",
+ L"mc12:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mediajukebox:%.txt",
+ L"mediajukebox:%00.txt",
+ L"mediajukebox:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"morpheus:%.txt",
+ L"morpheus:%00.txt",
+ L"morpheus:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mp2p:%.txt",
+ L"mp2p:%00.txt",
+ L"mp2p:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mpodcast:%.txt",
+ L"mpodcast:%00.txt",
+ L"mpodcast:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"msbd:%.txt",
+ L"msbd:%00.txt",
+ L"msbd:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"msbd:%.txt",
+ L"msbd:%00.txt",
+ L"msbd:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"msdigitallocker:%.txt",
+ L"msdigitallocker:%00.txt",
+ L"msdigitallocker:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"outlook.url.stssync:%.txt",
+ L"outlook.url.stssync:%00.txt",
+ L"outlook.url.stssync:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"p2p:%.txt",
+ L"p2p:%00.txt",
+ L"p2p:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"pando:%.txt",
+ L"pando:%00.txt",
+ L"pando:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"pcast:%.txt",
+ L"pcast:%00.txt",
+ L"pcast:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"picasa:%.txt",
+ L"picasa:%00.txt",
+ L"picasa:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"plaxo:%.txt",
+ L"plaxo:%00.txt",
+ L"plaxo:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"play:%.txt",
+ L"play:%00.txt",
+ L"play:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"podcast:%.txt",
+ L"podcast:%00.txt",
+ L"podcast:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ppmate:%.txt",
+ L"ppmate:%00.txt",
+ L"ppmate:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ppmates:%.txt",
+ L"ppmates:%00.txt",
+ L"ppmates:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ppstream:%.txt",
+ L"ppstream:%00.txt",
+ L"ppstream:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"quicktime:%.txt",
+ L"quicktime:%00.txt",
+ L"quicktime:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"realplayer.autoplay.6:%.txt",
+ L"realplayer.autoplay.6:%00.txt",
+ L"realplayer.autoplay.6:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"realplayer.cdburn.6:%.txt",
+ L"realplayer.cdburn.6:%00.txt",
+ L"realplayer.cdburn.6:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"rhap:%.txt",
+ L"rhap:%00.txt",
+ L"rhap:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"sc:%.txt",
+ L"sc:%00.txt",
+ L"sc:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"search-ms:%.txt",
+ L"search-ms:%00.txt",
+ L"search-ms:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"shareaza:%.txt",
+ L"shareaza:%00.txt",
+ L"shareaza:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"shell:%.txt",
+ L"shell:%00.txt",
+ L"shell:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"shout:%.txt",
+ L"shout:%00.txt",
+ L"shout:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"sig2dat:%.txt",
+ L"sig2dat:%00.txt",
+ L"sig2dat:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"sop:%.txt",
+ L"sop:%00.txt",
+ L"sop:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"steam:%.txt",
+ L"steam:%00.txt",
+ L"steam:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"stssync:%.txt",
+ L"stssync:%00.txt",
+ L"stssync:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"svn:%.txt",
+ L"svn:%00.txt",
+ L"svn:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"svn+ssh:%.txt",
+ L"svn+ssh:%00.txt",
+ L"svn+ssh:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"synacast:%.txt",
+ L"synacast:%00.txt",
+ L"synacast:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"torrent:%.txt",
+ L"torrent:%00.txt",
+ L"torrent:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"tsvn:%.txt",
+ L"tsvn:%00.txt",
+ L"tsvn:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"tvants:%.txt",
+ L"tvants:%00.txt",
+ L"tvants:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"tvu:%.txt",
+ L"tvu:%00.txt",
+ L"tvu:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"unsv:%.txt",
+ L"unsv:%00.txt",
+ L"unsv:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"uvox:%.txt",
+ L"uvox:%00.txt",
+ L"uvox:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ventrilo:%.txt",
+ L"ventrilo:%00.txt",
+ L"ventrilo:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"vs:%.txt",
+ L"vs:%00.txt",
+ L"vs:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"zune:%.txt",
+ L"zune:%00.txt",
+ L"zune:%41%2D%31%5Ftest"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 `"` 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, "&") + .replace(/"/g, """) + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/\n/g, "
"); +} + +function unescape(s) { + return s + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/
/g, "\n"); +} + +function testAlert(when, { serverEnabled, profD, isBackgroundTaskMode } = {}) { + let argumentString = action => { + // 
 is "\n". + let s = ``; + if (serverEnabled) { + s += `program
${AppConstants.MOZ_APP_NAME}`; + } else { + s += `invalid key
invalid value`; + } + if (serverEnabled && profD) { + s += `
profile
${profD.path}`; + } + if (serverEnabled) { + s += "
windowsTag
"; + } + if (action) { + s += `
action
${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"] |