summaryrefslogtreecommitdiffstats
path: root/toolkit/components/aboutthirdparty/tests
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/aboutthirdparty/tests')
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/Factory.cpp74
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/Icon.cpp109
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.cpp148
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.h52
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/Resource.h12
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.cpp68
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.def10
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.rc39
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/dinosaur.icobin0 -> 1406 bytes
-rw-r--r--toolkit/components/aboutthirdparty/tests/TestShellEx/moz.build33
-rw-r--r--toolkit/components/aboutthirdparty/tests/browser/browser.toml6
-rw-r--r--toolkit/components/aboutthirdparty/tests/browser/browser_aboutthirdparty.js317
-rw-r--r--toolkit/components/aboutthirdparty/tests/browser/head.js144
-rw-r--r--toolkit/components/aboutthirdparty/tests/browser/hello.zzz1
-rw-r--r--toolkit/components/aboutthirdparty/tests/gtest/TestAboutThirdParty.cpp125
-rw-r--r--toolkit/components/aboutthirdparty/tests/gtest/moz.build13
-rw-r--r--toolkit/components/aboutthirdparty/tests/xpcshell/head.js10
-rw-r--r--toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js65
-rw-r--r--toolkit/components/aboutthirdparty/tests/xpcshell/xpcshell.toml4
19 files changed, 1230 insertions, 0 deletions
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/Factory.cpp b/toolkit/components/aboutthirdparty/tests/TestShellEx/Factory.cpp
new file mode 100644
index 0000000000..a341c27b73
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/Factory.cpp
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * 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 "mozilla/Atomics.h"
+#include "mozilla/RefPtr.h"
+
+#include <windows.h>
+#include <shlobj.h>
+
+already_AddRefed<IExtractIconW> CreateIconExtension();
+
+class ClassFactory final : public IClassFactory {
+ mozilla::Atomic<uint32_t> mRefCnt;
+
+ ~ClassFactory() = default;
+
+ public:
+ ClassFactory() : mRefCnt(0) {}
+
+ // IUnknown
+
+ STDMETHODIMP QueryInterface(REFIID aRefIID, void** aResult) {
+ if (!aResult) {
+ return E_INVALIDARG;
+ }
+
+ if (aRefIID == IID_IClassFactory) {
+ RefPtr ref(static_cast<IClassFactory*>(this));
+ ref.forget(aResult);
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+ }
+
+ STDMETHODIMP_(ULONG) AddRef() { return ++mRefCnt; }
+
+ STDMETHODIMP_(ULONG) Release() {
+ ULONG result = --mRefCnt;
+ if (!result) {
+ delete this;
+ }
+ return result;
+ }
+
+ // IClassFactory
+
+ STDMETHODIMP CreateInstance(IUnknown* aOuter, REFIID aRefIID,
+ void** aResult) {
+ if (aOuter) {
+ return CLASS_E_NOAGGREGATION;
+ }
+
+ RefPtr<IUnknown> instance;
+ if (IsEqualCLSID(aRefIID, IID_IExtractIconA) ||
+ IsEqualCLSID(aRefIID, IID_IExtractIconW)) {
+ instance = CreateIconExtension();
+ } else {
+ return E_NOINTERFACE;
+ }
+
+ return instance ? instance->QueryInterface(aRefIID, aResult)
+ : E_OUTOFMEMORY;
+ }
+
+ STDMETHODIMP LockServer(BOOL) { return S_OK; }
+};
+
+already_AddRefed<IClassFactory> CreateFactory() {
+ return mozilla::MakeAndAddRef<ClassFactory>();
+}
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/Icon.cpp b/toolkit/components/aboutthirdparty/tests/TestShellEx/Icon.cpp
new file mode 100644
index 0000000000..116c0e03a1
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/Icon.cpp
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * 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 "mozilla/Atomics.h"
+#include "mozilla/RefPtr.h"
+#include "Resource.h"
+
+#include <windows.h>
+#include <shlobj.h>
+
+#include <string>
+
+extern std::wstring gDllPath;
+extern GUID CLSID_TestShellEx;
+
+class IconExtension final : public IPersistFile,
+ public IExtractIconA,
+ public IExtractIconW {
+ mozilla::Atomic<uint32_t> mRefCnt;
+
+ ~IconExtension() = default;
+
+ public:
+ IconExtension() : mRefCnt(0) {}
+
+ // IUnknown
+
+ STDMETHODIMP QueryInterface(REFIID aRefIID, void** aResult) {
+ if (!aResult) {
+ return E_INVALIDARG;
+ }
+
+ if (aRefIID == IID_IPersist) {
+ RefPtr ref(static_cast<IPersist*>(this));
+ ref.forget(aResult);
+ return S_OK;
+ } else if (aRefIID == IID_IPersistFile) {
+ RefPtr ref(static_cast<IPersistFile*>(this));
+ ref.forget(aResult);
+ return S_OK;
+ } else if (aRefIID == IID_IExtractIconA) {
+ RefPtr ref(static_cast<IExtractIconA*>(this));
+ ref.forget(aResult);
+ return S_OK;
+ } else if (aRefIID == IID_IExtractIconW) {
+ RefPtr ref(static_cast<IExtractIconW*>(this));
+ ref.forget(aResult);
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+ }
+
+ STDMETHODIMP_(ULONG) AddRef() { return ++mRefCnt; }
+
+ STDMETHODIMP_(ULONG) Release() {
+ ULONG result = --mRefCnt;
+ if (!result) {
+ delete this;
+ }
+ return result;
+ }
+
+ // IPersist
+
+ STDMETHODIMP GetClassID(CLSID* aClassID) {
+ *aClassID = CLSID_TestShellEx;
+ return S_OK;
+ }
+
+ // IPersistFile
+
+ STDMETHODIMP GetCurFile(LPOLESTR*) { return E_NOTIMPL; }
+ STDMETHODIMP IsDirty() { return S_FALSE; }
+ STDMETHODIMP Load(LPCOLESTR, DWORD) { return S_OK; }
+ STDMETHODIMP Save(LPCOLESTR, BOOL) { return E_NOTIMPL; }
+ STDMETHODIMP SaveCompleted(LPCOLESTR) { return E_NOTIMPL; }
+
+ // IExtractIconA
+
+ STDMETHODIMP Extract(PCSTR, UINT, HICON*, HICON*, UINT) { return E_NOTIMPL; }
+ STDMETHODIMP GetIconLocation(UINT, PSTR, UINT, int*, UINT*) {
+ return E_NOTIMPL;
+ }
+
+ // IExtractIconW
+
+ STDMETHODIMP Extract(PCWSTR, UINT, HICON*, HICON*, UINT) { return S_FALSE; }
+
+ STDMETHODIMP GetIconLocation(UINT, PWSTR aIconFile, UINT aCchMax, int* aIndex,
+ UINT* aOutFlags) {
+ if (aCchMax <= gDllPath.size()) {
+ return E_NOT_SUFFICIENT_BUFFER;
+ }
+
+ gDllPath.copy(aIconFile, gDllPath.size());
+ aIconFile[gDllPath.size()] = 0;
+ *aOutFlags = GIL_DONTCACHE;
+ *aIndex = -IDI_ICON1;
+ return S_OK;
+ }
+};
+
+already_AddRefed<IExtractIconW> CreateIconExtension() {
+ return mozilla::MakeAndAddRef<IconExtension>();
+}
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.cpp b/toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.cpp
new file mode 100644
index 0000000000..33b2feb12a
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.cpp
@@ -0,0 +1,148 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * 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 "mozilla/UniquePtr.h"
+#include "RegUtils.h"
+
+#include <windows.h>
+#include <strsafe.h>
+
+extern std::wstring gDllPath;
+
+const wchar_t kClsIdPrefix[] = L"CLSID\\";
+const wchar_t* kExtensionSubkeys[] = {
+ L".zzz\\shellex\\IconHandler",
+};
+
+bool RegKey::SetStringInternal(const wchar_t* aValueName,
+ const wchar_t* aValueData,
+ DWORD aValueDataLength) {
+ if (!mKey) {
+ return false;
+ }
+
+ return ::RegSetValueExW(mKey, aValueName, 0, REG_SZ,
+ reinterpret_cast<const BYTE*>(aValueData),
+ aValueDataLength) == ERROR_SUCCESS;
+}
+
+RegKey::RegKey(HKEY root, const wchar_t* aSubkey) : mKey(nullptr) {
+ ::RegCreateKeyExW(root, aSubkey, 0, nullptr, 0, KEY_ALL_ACCESS, nullptr,
+ &mKey, nullptr);
+}
+
+RegKey::~RegKey() {
+ if (mKey) {
+ ::RegCloseKey(mKey);
+ }
+}
+
+bool RegKey::SetString(const wchar_t* aValueName, const wchar_t* aValueData) {
+ return SetStringInternal(
+ aValueName, aValueData,
+ aValueData
+ ? static_cast<DWORD>((wcslen(aValueData) + 1) * sizeof(wchar_t))
+ : 0);
+}
+
+bool RegKey::SetString(const wchar_t* aValueName,
+ const std::wstring& aValueData) {
+ return SetStringInternal(
+ aValueName, aValueData.c_str(),
+ static_cast<DWORD>((aValueData.size() + 1) * sizeof(wchar_t)));
+}
+
+std::wstring RegKey::GetString(const wchar_t* aValueName) {
+ DWORD len = 0;
+ LSTATUS status = ::RegGetValueW(mKey, aValueName, nullptr, RRF_RT_REG_SZ,
+ nullptr, nullptr, &len);
+
+ mozilla::UniquePtr<uint8_t[]> buf = mozilla::MakeUnique<uint8_t[]>(len);
+ status = ::RegGetValueW(mKey, aValueName, nullptr, RRF_RT_REG_SZ, nullptr,
+ buf.get(), &len);
+ if (status != ERROR_SUCCESS) {
+ return L"";
+ }
+
+ return reinterpret_cast<wchar_t*>(buf.get());
+}
+
+ComRegisterer::ComRegisterer(const GUID& aClsId, const wchar_t* aFriendlyName)
+ : mClassRoot(HKEY_CURRENT_USER, L"Software\\Classes"),
+ mFriendlyName(aFriendlyName) {
+ wchar_t guidStr[64];
+ HRESULT hr = ::StringCbPrintfW(
+ guidStr, sizeof(guidStr),
+ L"{%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}", aClsId.Data1,
+ aClsId.Data2, aClsId.Data3, aClsId.Data4[0], aClsId.Data4[1],
+ aClsId.Data4[2], aClsId.Data4[3], aClsId.Data4[4], aClsId.Data4[5],
+ aClsId.Data4[6], aClsId.Data4[7]);
+ if (FAILED(hr)) {
+ return;
+ }
+
+ mClsId = guidStr;
+}
+
+bool ComRegisterer::UnregisterAll() {
+ bool isOk = true;
+ LSTATUS ls;
+
+ for (const wchar_t* subkey : kExtensionSubkeys) {
+ RegKey root(mClassRoot, subkey);
+
+ std::wstring currentHandler = root.GetString(nullptr);
+ if (currentHandler != mClsId) {
+ // If another extension is registered, don't overwrite it.
+ continue;
+ }
+
+ // Set an empty string instead of deleting the key.
+ if (!root.SetString(nullptr)) {
+ isOk = false;
+ }
+ }
+
+ std::wstring subkey(kClsIdPrefix);
+ subkey += mClsId;
+ ls = ::RegDeleteTreeW(mClassRoot, subkey.c_str());
+ if (ls != ERROR_SUCCESS && ls != ERROR_FILE_NOT_FOUND) {
+ isOk = false;
+ }
+
+ return isOk;
+}
+
+bool ComRegisterer::RegisterObject(const wchar_t* aThreadModel) {
+ std::wstring subkey(kClsIdPrefix);
+ subkey += mClsId;
+
+ RegKey root(mClassRoot, subkey.c_str());
+ if (!root || !root.SetString(nullptr, mFriendlyName)) {
+ return false;
+ }
+
+ RegKey inproc(root, L"InprocServer32");
+ return inproc && inproc.SetString(nullptr, gDllPath) &&
+ inproc.SetString(L"ThreadingModel", aThreadModel);
+}
+
+bool ComRegisterer::RegisterExtensions() {
+ for (const wchar_t* subkey : kExtensionSubkeys) {
+ RegKey root(mClassRoot, subkey);
+ std::wstring currentHandler = root.GetString(nullptr);
+ if (!currentHandler.empty()) {
+ // If another extension is registered, don't overwrite it.
+ continue;
+ }
+
+ if (!root.SetString(nullptr, mClsId)) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.h b/toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.h
new file mode 100644
index 0000000000..21a23fdf16
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/RegUtils.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * 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/. */
+
+#ifndef mozilla_TestShellEx_RegUtils_h
+#define mozilla_TestShellEx_RegUtils_h
+
+#include <windows.h>
+#include <string>
+
+class RegKey final {
+ HKEY mKey;
+
+ bool SetStringInternal(const wchar_t* aValueName, const wchar_t* aValueData,
+ DWORD aValueDataLength);
+
+ public:
+ RegKey() : mKey(nullptr) {}
+ RegKey(HKEY root, const wchar_t* aSubkey);
+ ~RegKey();
+
+ RegKey(RegKey&& aOther) = delete;
+ RegKey& operator=(RegKey&& aOther) = delete;
+ RegKey(const RegKey&) = delete;
+ RegKey& operator=(const RegKey&) = delete;
+
+ explicit operator bool() const { return !!mKey; }
+ operator HKEY() const { return mKey; }
+
+ bool SetString(const wchar_t* aValueName,
+ const wchar_t* aValueData = nullptr);
+ bool SetString(const wchar_t* aValueName, const std::wstring& aValueData);
+ std::wstring GetString(const wchar_t* aValueName);
+};
+
+class ComRegisterer final {
+ RegKey mClassRoot;
+ std::wstring mClsId;
+ std::wstring mFriendlyName;
+
+ public:
+ ComRegisterer(const GUID& aClsId, const wchar_t* aFriendlyName);
+ ~ComRegisterer() = default;
+
+ bool UnregisterAll();
+ bool RegisterObject(const wchar_t* aThreadModel);
+ bool RegisterExtensions();
+};
+
+#endif // mozilla_TestShellEx_RegUtils_h
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/Resource.h b/toolkit/components/aboutthirdparty/tests/TestShellEx/Resource.h
new file mode 100644
index 0000000000..fa037194d7
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/Resource.h
@@ -0,0 +1,12 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * 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/. */
+
+#ifndef mozilla_TestShellEx_Resource_h
+#define mozilla_TestShellEx_Resource_h
+
+#define IDI_ICON1 100
+
+#endif // mozilla_TestShellEx_Resource_h
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.cpp b/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.cpp
new file mode 100644
index 0000000000..dc0588483f
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.cpp
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * 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 "mozilla/RefPtr.h"
+#include "RegUtils.h"
+
+#include <windows.h>
+#include <objbase.h>
+
+already_AddRefed<IClassFactory> CreateFactory();
+
+// {10A9521E-0205-4CC7-93A1-62F30A9A54B3}
+GUID CLSID_TestShellEx = {
+ 0x10a9521e, 0x205, 0x4cc7, {0x93, 0xa1, 0x62, 0xf3, 0xa, 0x9a, 0x54, 0xb3}};
+wchar_t kFriendlyName[] = L"Minimum Shell Extension for Firefox testing";
+
+std::wstring gDllPath;
+
+BOOL APIENTRY DllMain(HMODULE aModule, DWORD aReason, LPVOID) {
+ wchar_t buf[MAX_PATH];
+ switch (aReason) {
+ case DLL_PROCESS_ATTACH:
+ if (!::GetModuleFileNameW(aModule, buf, ARRAYSIZE(buf))) {
+ return FALSE;
+ }
+ gDllPath = buf;
+ break;
+ case DLL_THREAD_ATTACH:
+ case DLL_THREAD_DETACH:
+ case DLL_PROCESS_DETACH:
+ break;
+ }
+ return TRUE;
+}
+
+STDAPI DllGetClassObject(REFCLSID aClsid, REFIID aIid, void** aResult) {
+ if (!IsEqualCLSID(aClsid, CLSID_TestShellEx) ||
+ !IsEqualCLSID(aIid, IID_IClassFactory)) {
+ return CLASS_E_CLASSNOTAVAILABLE;
+ }
+
+ RefPtr<IClassFactory> factory = CreateFactory();
+ return factory ? factory->QueryInterface(aIid, aResult) : E_OUTOFMEMORY;
+}
+
+STDAPI DllCanUnloadNow() { return S_OK; }
+
+// These functions enable us to run "regsvr32 [/u] TestShellEx.dll" manually.
+// (No admin privilege is needed because all access is under HKCU.)
+// We don't use these functions in the mochitest, but having these makes easier
+// to test the module manually.
+STDAPI DllRegisterServer() {
+ ComRegisterer reg(CLSID_TestShellEx, kFriendlyName);
+ if (!reg.RegisterObject(L"Apartment") || !reg.RegisterExtensions()) {
+ return E_ACCESSDENIED;
+ }
+ return S_OK;
+}
+STDAPI DllUnregisterServer() {
+ ComRegisterer reg(CLSID_TestShellEx, kFriendlyName);
+ if (!reg.UnregisterAll()) {
+ return E_ACCESSDENIED;
+ }
+ return S_OK;
+}
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.def b/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.def
new file mode 100644
index 0000000000..e83041771d
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.def
@@ -0,0 +1,10 @@
+;+# 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/.
+
+LIBRARY TestShellEx
+EXPORTS
+ DllGetClassObject PRIVATE
+ DllCanUnloadNow PRIVATE
+ DllRegisterServer PRIVATE
+ DllUnregisterServer PRIVATE
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.rc b/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.rc
new file mode 100644
index 0000000000..7500551f02
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/TestShellEx.rc
@@ -0,0 +1,39 @@
+/* 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 <winres.h>
+
+#include "Resource.h"
+
+IDI_ICON1 ICON DISCARDABLE "dinosaur.ico"
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION 1,2,3,4 // This field will be collected
+ PRODUCTVERSION 5,6,7,8
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS 0x1L
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_DLL
+ FILESUBTYPE 0x0L
+
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "Mozilla Corporation"
+ VALUE "OriginalFilename", "TestShellEx.dll"
+ VALUE "ProductName", "Sample Shell Extension"
+ END
+ END
+
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x0409, 1252
+ END
+END
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/dinosaur.ico b/toolkit/components/aboutthirdparty/tests/TestShellEx/dinosaur.ico
new file mode 100644
index 0000000000..d44438903b
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/dinosaur.ico
Binary files differ
diff --git a/toolkit/components/aboutthirdparty/tests/TestShellEx/moz.build b/toolkit/components/aboutthirdparty/tests/TestShellEx/moz.build
new file mode 100644
index 0000000000..44344a0e2e
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/TestShellEx/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+DIST_INSTALL = False
+
+SharedLibrary("TestShellEx")
+
+UNIFIED_SOURCES = [
+ "Factory.cpp",
+ "Icon.cpp",
+ "RegUtils.cpp",
+ "TestShellEx.cpp",
+]
+
+RCFILE = "TestShellEx.rc"
+DEFFILE = "TestShellEx.def"
+USE_LIBS += [
+ "mozglue",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ OS_LIBS += [
+ "advapi32",
+ "uuid",
+ ]
+
+if CONFIG["COMPILE_ENVIRONMENT"]:
+ shared_library = "!%sTestShellEx%s" % (CONFIG["DLL_PREFIX"], CONFIG["DLL_SUFFIX"])
+ TEST_HARNESS_FILES.testing.mochitest.browser.toolkit.components.aboutthirdparty.tests.browser += [
+ shared_library
+ ]
diff --git a/toolkit/components/aboutthirdparty/tests/browser/browser.toml b/toolkit/components/aboutthirdparty/tests/browser/browser.toml
new file mode 100644
index 0000000000..8a2d9db429
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/browser/browser.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+head = "head.js"
+
+["browser_aboutthirdparty.js"]
+support-files = ["hello.zzz"]
+skip-if = ["os == 'win'"] # Bug 1776048
diff --git a/toolkit/components/aboutthirdparty/tests/browser/browser_aboutthirdparty.js b/toolkit/components/aboutthirdparty/tests/browser/browser_aboutthirdparty.js
new file mode 100644
index 0000000000..0a53823a35
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/browser/browser_aboutthirdparty.js
@@ -0,0 +1,317 @@
+/* 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/. */
+
+// Return card containers matching a given name
+function getCardsByName(aContainer, aLeafName) {
+ const matchedCards = [];
+ const allCards = aContainer.querySelectorAll(".card");
+ for (const card of allCards) {
+ const nameLabel = card.querySelector(".module-name");
+ if (nameLabel.textContent == aLeafName) {
+ matchedCards.push(card);
+ }
+ }
+ return matchedCards;
+}
+
+function getDetailRow(aContainer, aLabel) {
+ return aContainer.querySelector(`[data-l10n-id=${aLabel}]`).parentElement;
+}
+
+function verifyClipboardData(aModuleJson) {
+ Assert.ok(
+ aModuleJson.hasOwnProperty("blocked"),
+ "Clipboard data should have blocked property."
+ );
+ const blocked = aModuleJson.blocked.filter(
+ x => x.name == kUserBlockedModuleName
+ );
+ Assert.equal(
+ blocked.length,
+ 1,
+ "Blocked array should contain the blocked module one time."
+ );
+ Assert.ok(
+ aModuleJson.hasOwnProperty("modules"),
+ "Clipboard data should have modules property"
+ );
+ let aModuleArray = aModuleJson.modules;
+ const filtered = aModuleArray.filter(x => x.name == kExtensionModuleName);
+ Assert.equal(filtered.length, 1, "No duplicate data for the module.");
+
+ const kDeletedPropertiesOfModule = [
+ "application",
+ "dllFile",
+ "loadingOnMain",
+ "trustFlags",
+ ];
+ const kDeletedPropertiesOfLoadingEvent = [
+ "baseAddress",
+ "isDependent",
+ "mainThread",
+ "moduleIndex",
+ "processUptimeMS",
+ ];
+
+ for (const module of aModuleArray) {
+ for (const deletedProperty of kDeletedPropertiesOfModule) {
+ Assert.ok(
+ !module.hasOwnProperty(deletedProperty),
+ `The property \`${deletedProperty}\` is deleted.`
+ );
+ }
+
+ Assert.ok(
+ !module.hasOwnProperty("typeFlags") || module.typeFlags != 0,
+ "typeFlags does not exist or is non-zero."
+ );
+
+ for (const event of module.events) {
+ for (const deletedProperty of kDeletedPropertiesOfLoadingEvent) {
+ Assert.ok(
+ !event.hasOwnProperty(deletedProperty),
+ `The property \`${deletedProperty}\` is deleted.`
+ );
+ }
+ }
+ }
+}
+
+function verifyModuleSorting(compareFunc) {
+ const uninteresting = {
+ typeFlags: 0,
+ isCrasher: false,
+ loadingOnMain: 0,
+ };
+ const crasherNotBlocked = { ...uninteresting, isCrasher: true };
+ const crasherBlocked = {
+ ...uninteresting,
+ isCrasher: true,
+ typeFlags: Ci.nsIAboutThirdParty.ModuleType_BlockedByUserAtLaunch,
+ };
+ const justBlocked = {
+ ...uninteresting,
+ typeFlags: Ci.nsIAboutThirdParty.ModuleType_BlockedByUserAtLaunch,
+ };
+ const uninterestingButSlow = {
+ ...uninteresting,
+ loadingOnMain: 10,
+ };
+ let modules = [
+ uninteresting,
+ uninterestingButSlow,
+ crasherNotBlocked,
+ justBlocked,
+ crasherBlocked,
+ ];
+ modules.sort(compareFunc);
+ Assert.equal(
+ JSON.stringify([
+ crasherBlocked,
+ justBlocked,
+ crasherNotBlocked,
+ uninterestingButSlow,
+ uninteresting,
+ ]),
+ JSON.stringify(modules),
+ "Modules sort in expected order"
+ );
+}
+
+add_task(async () => {
+ registerCleanupFunction(() => {
+ unregisterAll();
+ });
+ await registerObject();
+ registerExtensions();
+ loadShellExtension();
+
+ await kATP.collectSystemInfo();
+ Assert.equal(
+ kATP.lookupModuleType(kExtensionModuleName),
+ Ci.nsIAboutThirdParty.ModuleType_ShellExtension,
+ "lookupModuleType() returns a correct type " +
+ "after system info was collected."
+ );
+
+ await BrowserTestUtils.withNewTab("about:third-party", async browser => {
+ if (!content.fetchDataDone) {
+ const mainDiv = content.document.getElementById("main");
+ await BrowserTestUtils.waitForMutationCondition(
+ mainDiv,
+ { childList: true },
+ () => mainDiv.childElementCount > 0
+ );
+ Assert.ok(content.fetchDataDone, "onLoad() is completed.");
+ }
+
+ const reload = content.document.getElementById("button-reload");
+ if (!reload.hidden) {
+ reload.click();
+ await BrowserTestUtils.waitForMutationCondition(
+ reload,
+ { attributes: true, attributeFilter: ["hidden"] },
+ () => reload.hidden
+ );
+ }
+
+ Assert.ok(
+ content.document.getElementById("no-data").hidden,
+ "The no-data message is hidden."
+ );
+ const blockedCards = getCardsByName(
+ content.document,
+ kUserBlockedModuleName
+ );
+ Assert.equal(
+ blockedCards.length,
+ 1,
+ "Only one card matching the blocked module exists."
+ );
+ const blockedCard = blockedCards[0];
+ Assert.equal(
+ blockedCard.querySelectorAll(".button-block.module-blocked").length,
+ 1,
+ "The blocked module has a button indicating it is blocked"
+ );
+ let blockedBlockButton = blockedCard.querySelector(
+ ".button-block.module-blocked"
+ );
+ Assert.equal(
+ blockedBlockButton.getAttribute("data-l10n-id"),
+ "third-party-button-to-unblock-module",
+ "Button to block the module has correct title"
+ );
+ blockedBlockButton.click();
+ await BrowserTestUtils.promiseAlertDialogOpen("cancel");
+ Assert.ok(
+ !blockedBlockButton.classList.contains("module-blocked"),
+ "After clicking to unblock a module, button should not have module-blocked class."
+ );
+ Assert.equal(
+ blockedBlockButton.getAttribute("data-l10n-id"),
+ "third-party-button-to-block-module",
+ "After clicking to unblock a module, button should have correct title."
+ );
+ // Restore this to blocked for later tests
+ blockedBlockButton.click();
+ await BrowserTestUtils.promiseAlertDialogOpen("cancel");
+ Assert.ok(
+ blockedBlockButton.classList.contains("module-blocked"),
+ "After clicking to block a module, button should have module-blocked class."
+ );
+ Assert.equal(
+ blockedBlockButton.getAttribute("data-l10n-id"),
+ "third-party-button-to-unblock-module",
+ "After clicking to block a module, button should have correct title."
+ );
+
+ const cards = getCardsByName(content.document, kExtensionModuleName);
+ Assert.equal(cards.length, 1, "Only one card matching the module exists.");
+ const card = cards[0];
+
+ const blockButton = card.querySelector(".button-block");
+ Assert.ok(
+ !blockButton.classList.contains("blocklist-disabled"),
+ "Button to block the module does not indicate the blocklist is disabled."
+ );
+ Assert.ok(
+ !blockButton.classList.contains("module-blocked"),
+ "Button to block the module does not indicate the module is blocked."
+ );
+
+ Assert.ok(
+ card.querySelector(".image-warning").hidden,
+ "No warning sign for the module."
+ );
+ Assert.equal(
+ card.querySelector(".image-unsigned").hidden,
+ false,
+ "The module is labeled as unsigned."
+ );
+ Assert.equal(
+ card.querySelector(".tag-shellex").hidden,
+ false,
+ "The module is labeled as a shell extension."
+ );
+ Assert.equal(
+ card.querySelector(".tag-ime").hidden,
+ true,
+ "The module is not labeled as an IME."
+ );
+
+ const versionRow = getDetailRow(card, "third-party-detail-version");
+ Assert.equal(
+ versionRow.childNodes[1].textContent,
+ "1.2.3.4",
+ "The version matches a value in TestShellEx.rc."
+ );
+ const vendorRow = getDetailRow(card, "third-party-detail-vendor");
+ Assert.equal(
+ vendorRow.childNodes[1].textContent,
+ "Mozilla Corporation",
+ "The vendor name matches a value in TestShellEx.rc."
+ );
+ const occurrencesRow = getDetailRow(card, "third-party-detail-occurrences");
+ Assert.equal(
+ Number(occurrencesRow.childNodes[1].textContent),
+ 1,
+ "The module was loaded once."
+ );
+ const durationRow = getDetailRow(card, "third-party-detail-duration");
+ Assert.ok(
+ Number(durationRow.childNodes[1].textContent),
+ "The duration row shows a valid number."
+ );
+
+ const eventTable = card.querySelector(".event-table");
+ const tableCells = eventTable.querySelectorAll("td");
+ Assert.equal(
+ tableCells.length,
+ 3,
+ "The table has three cells as there is only one event."
+ );
+ Assert.equal(
+ tableCells[0].querySelector(".process-type").getAttribute("data-l10n-id"),
+ "process-type-default",
+ "The module was loaded into the main process."
+ );
+ Assert.ok(
+ Number(tableCells[0].querySelector(".process-id").textContent),
+ "A valid process ID is displayed."
+ );
+ Assert.equal(
+ tableCells[1].querySelector(".event-duration").textContent,
+ durationRow.childNodes[1].textContent,
+ "The event's duration is the same as the average " +
+ "as there is only one event."
+ );
+ Assert.equal(
+ tableCells[1].querySelector(".tag-background").hidden,
+ true,
+ "The icon handler is loaded in the main thread."
+ );
+ Assert.equal(
+ tableCells[2].getAttribute("data-l10n-id"),
+ "third-party-status-loaded",
+ "The module was really loaded without being blocked."
+ );
+
+ const button = content.document.getElementById("button-copy-to-clipboard");
+ button.click();
+
+ // Wait until copying is done and the button becomes clickable.
+ await BrowserTestUtils.waitForMutationCondition(
+ button,
+ { attributes: true },
+ () => !button.disabled
+ );
+
+ const copiedJSON = JSON.parse(await navigator.clipboard.readText());
+ Assert.ok(copiedJSON instanceof Object, "Data is an object.");
+ verifyClipboardData(copiedJSON);
+
+ verifyModuleSorting(content.moduleCompareForDisplay);
+ });
+});
diff --git a/toolkit/components/aboutthirdparty/tests/browser/head.js b/toolkit/components/aboutthirdparty/tests/browser/head.js
new file mode 100644
index 0000000000..eba3948046
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/browser/head.js
@@ -0,0 +1,144 @@
+/* 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/. */
+
+"use strict";
+
+const kClsidTestShellEx = "{10a9521e-0205-4cc7-93a1-62f30a9a54b3}";
+const kFriendlyName = "Minimum Shell Extension for Firefox testing";
+const kExtensionSubkeys = [".zzz\\shellex\\IconHandler"];
+const kExtensionModuleName = "TestShellEx.dll";
+const kUserBlockedModuleName = "TestDllBlocklist_UserBlocked.dll";
+const kFileFilterInDialog = "*.zzz";
+const kATP = Cc["@mozilla.org/about-thirdparty;1"].getService(
+ Ci.nsIAboutThirdParty
+);
+
+function loadShellExtension() {
+ // This method call opens the file dialog and shows the support file
+ // "hello.zzz" in it, which loads TestShellEx.dll to show an icon
+ // for files with the .zzz extension.
+ kATP.openAndCloseFileDialogForTesting(
+ kExtensionModuleName,
+ getTestFilePath(""),
+ kFileFilterInDialog
+ );
+}
+
+async function registerObject() {
+ const reg = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+
+ reg.create(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Classes\\CLSID\\" + kClsidTestShellEx,
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+
+ reg.writeStringValue("", kFriendlyName);
+
+ const inprocServer = reg.createChild(
+ "InprocServer32",
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+
+ const moduleFullPath = getTestFilePath(kExtensionModuleName);
+ Assert.ok(await IOUtils.exists(moduleFullPath), "The module file exists.");
+
+ inprocServer.writeStringValue("", moduleFullPath);
+ inprocServer.writeStringValue("ThreadingModel", "Apartment");
+ reg.close();
+
+ info("registerObject() done - " + moduleFullPath);
+}
+
+function registerExtensions() {
+ for (const subkey of kExtensionSubkeys) {
+ const reg = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ reg.create(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Classes\\" + subkey,
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+
+ let currentExtension = "";
+ try {
+ // If the key was just created above, the default value does not exist,
+ // so readStringValue will throw NS_ERROR_FAILURE.
+ currentExtension = reg.readStringValue("");
+ } catch (e) {}
+
+ try {
+ if (!currentExtension) {
+ reg.writeStringValue("", kClsidTestShellEx);
+ } else if (currentExtension != kClsidTestShellEx) {
+ throw new Error(
+ `Another extension \`${currentExtension}\` has been registered.`
+ );
+ }
+ } catch (e) {
+ throw new Error("Failed to register TestShellEx.dll: " + e);
+ } finally {
+ reg.close();
+ }
+ }
+}
+
+function unregisterAll() {
+ for (const subkey of kExtensionSubkeys) {
+ const reg = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+
+ try {
+ reg.open(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Classes\\" + subkey,
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+
+ if (reg.readStringValue("") != kClsidTestShellEx) {
+ // If another extension is registered, don't overwrite it.
+ continue;
+ }
+
+ // Set an empty string instead of deleting the key
+ // not to touch non-default values.
+ reg.writeStringValue("", "");
+ } catch (e) {
+ info(`Failed to unregister \`${subkey}\`: ` + e);
+ } finally {
+ reg.close();
+ }
+ }
+
+ const reg = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ reg.open(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Classes\\CLSID",
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+
+ try {
+ const child = reg.openChild(
+ kClsidTestShellEx,
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+ try {
+ child.removeChild("InprocServer32");
+ } catch (e) {
+ } finally {
+ child.close();
+ }
+
+ reg.removeChild(kClsidTestShellEx);
+ } catch (e) {
+ } finally {
+ reg.close();
+ }
+}
diff --git a/toolkit/components/aboutthirdparty/tests/browser/hello.zzz b/toolkit/components/aboutthirdparty/tests/browser/hello.zzz
new file mode 100644
index 0000000000..e69ab604b3
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/browser/hello.zzz
@@ -0,0 +1 @@
+Showing this file on Windows Shell loads TestShellEx.dll.
diff --git a/toolkit/components/aboutthirdparty/tests/gtest/TestAboutThirdParty.cpp b/toolkit/components/aboutthirdparty/tests/gtest/TestAboutThirdParty.cpp
new file mode 100644
index 0000000000..565e87e0f1
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/gtest/TestAboutThirdParty.cpp
@@ -0,0 +1,125 @@
+/* -*- Mode: C++; tab-width: 8; 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 https://mozilla.org/MPL/2.0/. */
+
+#include <windows.h>
+#include "gtest/gtest.h"
+
+#include "../../AboutThirdPartyUtils.h"
+#include "mozilla/AboutThirdParty.h"
+#include "mozilla/ArrayUtils.h"
+#include "nsTArray.h"
+
+using namespace mozilla;
+
+#define WEATHER_RU u"\x041F\x043E\x0433\x043E\x0434\x0430"_ns
+#define WEATHER_JA u"\x5929\x6C17"_ns
+
+TEST(AboutThirdParty, CompareIgnoreCase)
+{
+ EXPECT_EQ(CompareIgnoreCase(u""_ns, u""_ns), 0);
+ EXPECT_EQ(CompareIgnoreCase(u"abc"_ns, u"aBc"_ns), 0);
+ EXPECT_LT(CompareIgnoreCase(u"a"_ns, u"ab"_ns), 0);
+ EXPECT_GT(CompareIgnoreCase(u"ab"_ns, u"A"_ns), 0);
+ EXPECT_LT(CompareIgnoreCase(u""_ns, u"aB"_ns), 0);
+ EXPECT_GT(CompareIgnoreCase(u"ab"_ns, u""_ns), 0);
+
+ // non-ascii testcases
+ EXPECT_EQ(CompareIgnoreCase(WEATHER_JA, WEATHER_JA), 0);
+ EXPECT_EQ(CompareIgnoreCase(WEATHER_RU, WEATHER_RU), 0);
+ EXPECT_LT(CompareIgnoreCase(WEATHER_RU, WEATHER_JA), 0);
+ EXPECT_GT(CompareIgnoreCase(WEATHER_JA, WEATHER_RU), 0);
+ EXPECT_EQ(CompareIgnoreCase(WEATHER_RU u"x"_ns WEATHER_JA,
+ WEATHER_RU u"X"_ns WEATHER_JA),
+ 0);
+ EXPECT_GT(
+ CompareIgnoreCase(WEATHER_RU u"a"_ns WEATHER_JA, WEATHER_RU u"A"_ns), 0);
+ EXPECT_LT(CompareIgnoreCase(WEATHER_RU u"a"_ns WEATHER_RU,
+ WEATHER_RU u"A"_ns WEATHER_JA),
+ 0);
+}
+
+TEST(AboutThirdParty, MsiPackGuid)
+{
+ nsAutoString packedGuid;
+ EXPECT_FALSE(
+ MsiPackGuid(u"EDA620E3-AA98-3846-B81E-3493CB2E0E02"_ns, packedGuid));
+ EXPECT_FALSE(
+ MsiPackGuid(u"*EDA620E3-AA98-3846-B81E-3493CB2E0E02*"_ns, packedGuid));
+ EXPECT_TRUE(
+ MsiPackGuid(u"{EDA620E3-AA98-3846-B81E-3493CB2E0E02}"_ns, packedGuid));
+ EXPECT_STREQ(packedGuid.get(), L"3E026ADE89AA64838BE14339BCE2E020");
+}
+
+TEST(AboutThirdParty, CorrectMsiComponentPath)
+{
+ nsAutoString testPath;
+
+ testPath = u""_ns;
+ EXPECT_FALSE(CorrectMsiComponentPath(testPath));
+
+ testPath = u"\\\\server\\share"_ns;
+ EXPECT_FALSE(CorrectMsiComponentPath(testPath));
+
+ testPath = u"hello"_ns;
+ EXPECT_FALSE(CorrectMsiComponentPath(testPath));
+
+ testPath = u"02:\\Software"_ns;
+ EXPECT_FALSE(CorrectMsiComponentPath(testPath));
+
+ testPath = u"C:\\path\\"_ns;
+ EXPECT_TRUE(CorrectMsiComponentPath(testPath));
+ EXPECT_STREQ(testPath.get(), L"C:\\path\\");
+
+ testPath = u"C?\\path\\"_ns;
+ EXPECT_TRUE(CorrectMsiComponentPath(testPath));
+ EXPECT_STREQ(testPath.get(), L"C:\\path\\");
+
+ testPath = u"C:\\?path\\"_ns;
+ EXPECT_TRUE(CorrectMsiComponentPath(testPath));
+ EXPECT_STREQ(testPath.get(), L"C:\\path\\");
+
+ testPath = u"\\?path\\"_ns;
+ EXPECT_FALSE(CorrectMsiComponentPath(testPath));
+}
+
+TEST(AboutThirdParty, InstallLocations)
+{
+ const nsLiteralString kDirectoriesUnsorted[] = {
+ u"C:\\duplicate\\"_ns, u"C:\\duplicate\\"_ns, u"C:\\app1\\"_ns,
+ u"C:\\app2\\"_ns, u"C:\\app11\\"_ns, u"C:\\app12\\"_ns,
+ };
+
+ struct TestCase {
+ nsLiteralString mFile;
+ nsLiteralString mInstallPath;
+ } const kTestCases[] = {
+ {u"C:\\app\\sub\\file.dll"_ns, u""_ns},
+ {u"C:\\app1\\sub\\file.dll"_ns, u"C:\\app1\\"_ns},
+ {u"C:\\app11\\sub\\file.dll"_ns, u"C:\\app11\\"_ns},
+ {u"C:\\app12\\sub\\file.dll"_ns, u"C:\\app12\\"_ns},
+ {u"C:\\app13\\sub\\file.dll"_ns, u""_ns},
+ {u"C:\\duplicate\\sub\\file.dll"_ns, u""_ns},
+ };
+
+ nsTArray<InstallLocationT> locations(ArrayLength(kDirectoriesUnsorted));
+ for (size_t i = 0; i < ArrayLength(kDirectoriesUnsorted); ++i) {
+ locations.EmplaceBack(kDirectoriesUnsorted[i], new InstalledApplication());
+ }
+
+ locations.Sort([](const InstallLocationT& aA, const InstallLocationT& aB) {
+ return CompareIgnoreCase(aA.first(), aB.first());
+ });
+
+ for (const auto& testCase : kTestCases) {
+ auto bounds = EqualRange(locations, 0, locations.Length(),
+ InstallLocationComparator(testCase.mFile));
+ if (bounds.second - bounds.first != 1) {
+ EXPECT_TRUE(testCase.mInstallPath.IsEmpty());
+ continue;
+ }
+
+ EXPECT_EQ(locations[bounds.first].first(), testCase.mInstallPath);
+ }
+}
diff --git a/toolkit/components/aboutthirdparty/tests/gtest/moz.build b/toolkit/components/aboutthirdparty/tests/gtest/moz.build
new file mode 100644
index 0000000000..659c0836db
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/gtest/moz.build
@@ -0,0 +1,13 @@
+# -*- 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/.
+
+LOCAL_INCLUDES += [
+ "../..",
+]
+
+UNIFIED_SOURCES += ["TestAboutThirdParty.cpp"]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/toolkit/components/aboutthirdparty/tests/xpcshell/head.js b/toolkit/components/aboutthirdparty/tests/xpcshell/head.js
new file mode 100644
index 0000000000..ede1d3cc73
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/xpcshell/head.js
@@ -0,0 +1,10 @@
+/* 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/. */
+
+"use strict";
+
+const kExtensionModuleName = "TestShellEx.dll";
+const kATP = Cc["@mozilla.org/about-thirdparty;1"].getService(
+ Ci.nsIAboutThirdParty
+);
diff --git a/toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js b/toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js
new file mode 100644
index 0000000000..281fe42188
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/xpcshell/test_aboutthirdparty.js
@@ -0,0 +1,65 @@
+/* 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/. */
+
+add_task(async () => {
+ Assert.equal(
+ kATP.lookupModuleType(kExtensionModuleName),
+ 0,
+ "lookupModuleType() returns 0 before system info is collected."
+ );
+
+ // Make sure successive calls of collectSystemInfo() do not
+ // cause anything bad.
+ const kLoopCount = 100;
+ const promises = [];
+ for (let i = 0; i < kLoopCount; ++i) {
+ promises.push(kATP.collectSystemInfo());
+ }
+
+ const collectSystemInfoResults = await Promise.allSettled(promises);
+ Assert.equal(collectSystemInfoResults.length, kLoopCount);
+
+ for (const result of collectSystemInfoResults) {
+ Assert.ok(
+ result.status == "fulfilled",
+ "All results from collectSystemInfo() are resolved."
+ );
+ }
+
+ Assert.equal(
+ kATP.lookupModuleType("SHELL32.dll"),
+ Ci.nsIAboutThirdParty.ModuleType_ShellExtension,
+ "Shell32.dll is always registered as a shell extension."
+ );
+
+ Assert.equal(
+ kATP.lookupModuleType(""),
+ Ci.nsIAboutThirdParty.ModuleType_Unknown,
+ "Looking up an empty string succeeds and returns ModuleType_Unknown."
+ );
+
+ Assert.equal(
+ kATP.lookupModuleType(null),
+ Ci.nsIAboutThirdParty.ModuleType_Unknown,
+ "Looking up null succeeds and returns ModuleType_Unknown."
+ );
+
+ Assert.equal(
+ kATP.lookupModuleType("invalid name"),
+ Ci.nsIAboutThirdParty.ModuleType_Unknown,
+ "Looking up an invalid name succeeds and returns ModuleType_Unknown."
+ );
+
+ Assert.equal(
+ kATP.lookupApplication(""),
+ null,
+ "Looking up an empty string returns null."
+ );
+
+ Assert.equal(
+ kATP.lookupApplication("invalid path"),
+ null,
+ "Looking up an invalid path returns null."
+ );
+});
diff --git a/toolkit/components/aboutthirdparty/tests/xpcshell/xpcshell.toml b/toolkit/components/aboutthirdparty/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..46a2fbc2ff
--- /dev/null
+++ b/toolkit/components/aboutthirdparty/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+head = "head.js"
+
+["test_aboutthirdparty.js"]