gh-14959: Sync upstream Firefox to version 154.0 (gh-15012)

This commit is contained in:
mr. m
2026-08-18 23:44:19 +02:00
committed by GitHub
parent e2e1ab4e6d
commit cebaa0cefa
30 changed files with 2287 additions and 298 deletions

View File

@@ -34,7 +34,7 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne
### Firefox Versions
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `153.0.4`! 🚀
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `154.0`! 🚀
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 154.0`!
### Contributing

View File

@@ -1 +1 @@
4be2ba3142dc23601c46ab7e8268923f1939442d
1fb0703fa9723fd9c400ad60d18e64ee4f9bf125

View File

@@ -1496,14 +1496,14 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
}
getToolbarColor(isDarkMode = false, accentColor = undefined) {
const opacity = 0.8;
const opacity = 0.9;
let baseColor = isDarkMode ? [255, 255, 255, opacity] : [0, 0, 0, opacity]; // Default toolbar
if (accentColor && this.canBeTransparent) {
if (accentColor) {
// Blend a bit with the accent color to make it more visible
baseColor = this.blendColors(
accentColor,
baseColor.slice(0, 3),
10
this.canBeTransparent ? 15 : 5
).concat(opacity);
}
return baseColor;

View File

@@ -104,6 +104,11 @@ skip-if = [
"tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449
]
["browser_pinToTaskbarTelemetry.js"]
run-if = [
"os == 'win'",
]
["browser_processAUMID.js"]
run-if = [
"os == 'win'",
@@ -118,6 +123,12 @@ run-if = [
]
tags = "os_integration"
["browser_setDefaultPDFHandler_mac.js"]
run-if = [
"os == 'mac'",
]
tags = "os_integration"
["browser_setDefaultProtocolHandler.js"]
run-if = [
"os == 'win'",

View File

@@ -151,7 +151,7 @@ add_task(async function () {
// Saves the file in ~/Pictures
shellSvc.setDesktopBackground(image, 0, backgroundImage.leafName);
await BrowserTestUtils.waitForCondition(() => backgroundImage.exists());
await TestUtils.waitForCondition(() => backgroundImage.exists());
info(`${backgroundImage.path} downloaded`);
Assert.ok(
FileUtils.File(backgroundImage.path).exists(),

View File

@@ -52,17 +52,23 @@ const gDirectoryServiceProvider = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIDirectoryServiceProvider]),
};
add_setup(() => {
add_setup(async () => {
Services.dirsvc
.QueryInterface(Ci.nsIDirectoryService)
.registerProvider(gDirectoryServiceProvider);
await SpecialPowers.pushPrefEnv({
set: [["browser.shell.shortcut.test", true]],
});
});
registerCleanupFunction(() => {
registerCleanupFunction(async () => {
gBase.remove(true);
Services.dirsvc
.QueryInterface(Ci.nsIDirectoryService)
.unregisterProvider(gDirectoryServiceProvider);
await SpecialPowers.popPrefEnv();
});
add_task(async function test_CreateWindowsShortcut() {

View File

@@ -0,0 +1,81 @@
/* Any copyright is dedicated to the Public Domain.
* https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
ChromeUtils.defineESModuleGetters(this, {
ShellService: "moz-src:///browser/components/shell/ShellService.sys.mjs",
});
const kStubPref = "browser.shell.taskbar.test.pinWinRtStubResult";
add_setup(function () {
Services.fog.initializeFOG();
registerCleanupFunction(() => {
Services.prefs.clearUserPref(kStubPref);
});
});
// Success outcomes: the WinRT stub returns Ok with no fallback, so the promise
// resolves to the matching PinResult.
add_task(async function successCases() {
const cases = [
["success_pinned", Ci.nsIWindowsShellService.PINNED],
["success_rejected", Ci.nsIWindowsShellService.REJECTED],
["success_fire_and_forget", Ci.nsIWindowsShellService.UNKNOWN],
];
for (let [stubValue, expected] of cases) {
Services.prefs.setCharPref(kStubPref, stubValue);
Assert.equal(undefined, Glean.taskbar.pinWinrt.testGetValue());
let result = await ShellService.pinCurrentAppToTaskbar(false);
Assert.equal(result, expected, `resolved PinResult for ${stubValue}`);
let events = Glean.taskbar.pinWinrt.testGetValue();
Assert.equal(events.length, 1, `one pin_winrt event for ${stubValue}`);
Assert.equal(
events[0].extra.result,
stubValue,
"result extra matches pref"
);
Services.fog.testResetFOG();
}
});
// Error outcomes: telemetry is recorded before the COM fallback runs. The
// fallback's resolve/reject is environment-dependent, so only assert telemetry.
add_task(async function errorCases() {
const errors = [
"error_get_aumid",
"error_set_aumid",
"error_get_taskbar_manager",
"error_schedule_request_pin",
"error_request_pin",
];
for (let stubValue of errors) {
Services.prefs.setCharPref(kStubPref, stubValue);
Assert.equal(undefined, Glean.taskbar.pinWinrt.testGetValue());
try {
await ShellService.pinCurrentAppToTaskbar(false);
} catch (e) {
// The COM fallback may reject; the pin_winrt telemetry is already recorded.
}
let events = Glean.taskbar.pinWinrt.testGetValue();
Assert.equal(events.length, 1, `one pin_winrt event for ${stubValue}`);
Assert.equal(
events[0].extra.result,
stubValue,
"result extra matches pref"
);
Services.fog.testResetFOG();
}
});

View File

@@ -17,11 +17,11 @@ add_task(async function test_processAUMID() {
// This function will trigger the relevant code paths that
// incorrectly changes the process AUMID on MSIX, prior to
// Bug 1950734 being fixed
await ShellService.checkPinCurrentAppToTaskbarAsync(false);
await ShellService.pinToTaskbar();
is(
processAUMID,
ShellService.checkCurrentProcessAUMIDForTesting(),
processAUMID,
"The process AUMID should not be changed"
);
});

View File

@@ -42,6 +42,9 @@ const launchModernSettingsDialogDefaultAppsStub = sinon.stub();
const shellStub = sinon.stub(ShellService, "shellService").value({
setDefaultBrowser: setDefaultStub,
queryCurrentDefaultHandlerFor: queryCurrentDefaultHandlerForStub,
// setAsDefaultPDFHandler samples this for the recorded telemetry; the value
// doesn't matter for these assertions.
isDefaultHandlerFor: sinon.stub(),
launchSetDefaultAppPicker: launchSetDefaultAppPickerStub,
launchModernSettingsDialogDefaultApps:
launchModernSettingsDialogDefaultAppsStub,

View File

@@ -0,0 +1,260 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// macOS-only checks for ShellService.setAsDefaultPDFHandler and the supporting
// isDefaultHandlerFor / canSetAsDefaultPDFHandler branches. On macOS these go
// through NSWorkspace; the native layer is mocked here.
ChromeUtils.defineESModuleGetters(this, {
sinon: "resource://testing-common/Sinon.sys.mjs",
});
Assert.equal(AppConstants.platform, "macosx", "Platform is macOS");
const setAsDefaultHandlerForStub = sinon.stub().resolves(false);
const isDefaultHandlerForStub = sinon.stub().returns(false);
const isDefaultHandlerAWebBrowserForStub = sinon.stub().returns(false);
const fakeShellService = {
canSetAsDefaultHandler: true,
setAsDefaultHandlerFor: setAsDefaultHandlerForStub,
isDefaultHandlerFor: isDefaultHandlerForStub,
isDefaultHandlerAWebBrowserFor: isDefaultHandlerAWebBrowserForStub,
QueryInterface: ChromeUtils.generateQI([]),
};
const shellStub = sinon
.stub(ShellService, "shellService")
.value(fakeShellService);
registerCleanupFunction(() => {
shellStub.restore();
});
function resetStubs() {
setAsDefaultHandlerForStub.resetHistory();
isDefaultHandlerForStub.resetHistory();
isDefaultHandlerAWebBrowserForStub.resetHistory();
}
function getAttemptEvent() {
const events = Glean.browser.setDefaultPdfHandlerAttempt.testGetValue();
Assert.ok(events?.length, "Recorded a set_default_pdf_handler_attempt event");
Assert.equal(events.length, 1, "Recorded exactly one attempt event");
return events[0];
}
add_task(async function test_canSetAsDefaultPDFHandler() {
fakeShellService.canSetAsDefaultHandler = true;
Assert.ok(
ShellService.canSetAsDefaultPDFHandler,
"canSetAsDefaultPDFHandler is true when the native value is true"
);
try {
fakeShellService.canSetAsDefaultHandler = false;
Assert.ok(
!ShellService.canSetAsDefaultPDFHandler,
"canSetAsDefaultPDFHandler is false when the native value is false"
);
} finally {
fakeShellService.canSetAsDefaultHandler = true;
}
});
add_task(async function test_isDefaultHandlerFor() {
isDefaultHandlerForStub.returns(true);
Assert.ok(
ShellService.isDefaultHandlerFor(".pdf"),
"isDefaultHandlerFor('.pdf') reflects the native result"
);
Assert.ok(
isDefaultHandlerForStub.calledWith(".pdf"),
"Forwarded '.pdf' to the native isDefaultHandlerFor"
);
isDefaultHandlerForStub.returns(false);
Assert.ok(
!ShellService.isDefaultHandlerFor(".pdf"),
"isDefaultHandlerFor('.pdf') is false when Firefox is not the default"
);
isDefaultHandlerForStub.returns(true);
Assert.ok(
ShellService.isDefaultHandlerFor("https"),
"isDefaultHandlerFor('https') reflects the native result"
);
Assert.ok(
isDefaultHandlerForStub.calledWith("https"),
"Forwarded 'https' to the native isDefaultHandlerFor"
);
resetStubs();
});
add_task(async function test_setAsDefaultPDFHandler_unsupported() {
Services.fog.testResetFOG();
try {
fakeShellService.canSetAsDefaultHandler = false;
setAsDefaultHandlerForStub.resolves(true);
isDefaultHandlerForStub.returns(true);
const result = await ShellService.setAsDefaultPDFHandler();
Assert.strictEqual(result, false, "Resolves false when unsupported");
Assert.ok(
setAsDefaultHandlerForStub.notCalled,
"Did not call the native setter when unsupported"
);
Assert.ok(
!Glean.browser.setDefaultPdfHandlerAttempt.testGetValue(),
"Did not record an attempt event when unsupported"
);
} finally {
fakeShellService.canSetAsDefaultHandler = true;
resetStubs();
}
});
add_task(async function test_setAsDefaultPDFHandler_confirmed() {
Services.fog.testResetFOG();
setAsDefaultHandlerForStub.resolves(true);
isDefaultHandlerForStub.returns(true);
const result = await ShellService.setAsDefaultPDFHandler();
Assert.strictEqual(result, true, "Resolves true when the user confirms");
Assert.ok(
setAsDefaultHandlerForStub.calledOnce,
"Called the native setAsDefaultHandlerFor once"
);
Assert.ok(
setAsDefaultHandlerForStub.calledWith(".pdf"),
"Forwarded '.pdf' to the native setAsDefaultHandlerFor"
);
const event = getAttemptEvent();
Assert.equal(
event.extra.method,
"launch_services",
"Event method is launch_services"
);
Assert.equal(event.extra.success, "true", "Event success is true");
Assert.equal(
event.extra.result_is_default,
"true",
"Event result_is_default reflects isDefaultHandlerFor"
);
resetStubs();
});
add_task(async function test_setAsDefaultPDFHandler_recordsIndependentState() {
Services.fog.testResetFOG();
// success (the native setter's result) and result_is_default (a separate
// isDefaultHandlerFor sample) come from different sources, so drive them to
// different values: a bug that conflated the two would pass every test where
// they happen to agree.
setAsDefaultHandlerForStub.resolves(true);
isDefaultHandlerForStub.returns(false);
const result = await ShellService.setAsDefaultPDFHandler();
Assert.strictEqual(result, true, "Resolves with the native setter's result");
const event = getAttemptEvent();
Assert.equal(
event.extra.success,
"true",
"success reflects the native setter"
);
Assert.equal(
event.extra.result_is_default,
"false",
"result_is_default reflects isDefaultHandlerFor, not success"
);
resetStubs();
});
add_task(async function test_setAsDefaultPDFHandler_declined() {
Services.fog.testResetFOG();
setAsDefaultHandlerForStub.resolves(false);
isDefaultHandlerForStub.returns(false);
const result = await ShellService.setAsDefaultPDFHandler();
Assert.strictEqual(result, false, "Resolves false when the user declines");
const event = getAttemptEvent();
Assert.equal(event.extra.success, "false", "Event success is false");
Assert.equal(
event.extra.result_is_default,
"false",
"Event result_is_default is false"
);
resetStubs();
});
add_task(async function test_setAsDefaultPDFHandler_error() {
Services.fog.testResetFOG();
setAsDefaultHandlerForStub.rejects(new Error("mock NSWorkspace failure"));
isDefaultHandlerForStub.returns(false);
const result = await ShellService.setAsDefaultPDFHandler();
Assert.strictEqual(
result,
false,
"Resolves false when the native call rejects"
);
const event = getAttemptEvent();
Assert.equal(event.extra.success, "false", "Event success is false on error");
resetStubs();
setAsDefaultHandlerForStub.resolves(false);
});
add_task(async function test_setAsDefaultPDFHandler_onlyIfKnownBrowser() {
setAsDefaultHandlerForStub.resolves(true);
isDefaultHandlerForStub.returns(true);
isDefaultHandlerAWebBrowserForStub.returns(true);
let result = await ShellService.setAsDefaultPDFHandler(true);
Assert.strictEqual(
result,
true,
"Resolves true when the handler is a browser"
);
Assert.ok(
setAsDefaultHandlerForStub.called,
"Set the default when the current handler is a browser"
);
Assert.ok(
isDefaultHandlerAWebBrowserForStub.calledWith(".pdf"),
"Forwarded '.pdf' to the native isDefaultHandlerAWebBrowserFor"
);
resetStubs();
isDefaultHandlerAWebBrowserForStub.returns(false);
result = await ShellService.setAsDefaultPDFHandler(true);
Assert.strictEqual(
result,
false,
"Resolves false when the handler is not a browser"
);
Assert.ok(
setAsDefaultHandlerForStub.notCalled,
"Did not set the default when the current handler is not a browser"
);
resetStubs();
isDefaultHandlerAWebBrowserForStub.returns(false);
result = await ShellService.setAsDefaultPDFHandler(false);
Assert.strictEqual(
result,
true,
"Resolves true and sets unconditionally when onlyIfKnownBrowser is false"
);
Assert.ok(
setAsDefaultHandlerForStub.called,
"Set the default unconditionally when onlyIfKnownBrowser is false"
);
Assert.ok(
isDefaultHandlerAWebBrowserForStub.notCalled,
"Did not consult the browser check when onlyIfKnownBrowser is false"
);
resetStubs();
});

View File

@@ -1,237 +0,0 @@
/* 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 <gtest/gtest.h>
#include <gmock/gmock.h>
#include <string_view>
#include <string>
#include <windows.h>
#include <ole2.h>
#include <tlhelp32.h>
#include <uiautomation.h>
#include "mozilla/Maybe.h"
#include "mozilla/UniquePtr.h"
#include "WindowsDefaultBrowser.h"
static bool RegWriteStringValue(HKEY aRoot, std::wstring_view aKey,
const mozilla::Maybe<std::wstring>& aName,
std::wstring_view aValue) {
HKEY key;
LSTATUS ls{::RegCreateKeyExW(aRoot, aKey.data(), 0, nullptr,
REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, nullptr,
&key, nullptr)};
if (ls != ERROR_SUCCESS) {
return false;
}
const BYTE* value{reinterpret_cast<const BYTE*>(aValue.data())};
const DWORD size{static_cast<DWORD>((aValue.size() + 1) * sizeof(WCHAR))};
ls = ::RegSetValueExW(key, aName ? aName->c_str() : nullptr, 0, REG_SZ, value,
size);
::RegCloseKey(key);
return (ls == ERROR_SUCCESS);
}
static bool RegDeleteValue(HKEY aRoot, std::wstring_view aKey,
std::wstring_view aName) {
HKEY key;
LSTATUS ls{
::RegOpenKeyExW(HKEY_CURRENT_USER, aKey.data(), 0, KEY_SET_VALUE, &key)};
if (ls == ERROR_SUCCESS) {
::RegDeleteValueW(key, aName.data());
::RegCloseKey(key);
return true;
}
return false;
}
static bool RegDeleteKey(HKEY aRoot, std::wstring_view aKey) {
LSTATUS ls{::RegDeleteTreeW(HKEY_CURRENT_USER, aKey.data())};
return (ls == ERROR_SUCCESS);
}
static void TerminateProcessById(DWORD aProcessId) {
HANDLE process{
::OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, aProcessId)};
if (process) {
::TerminateProcess(process, 0);
::WaitForSingleObject(process, INFINITE);
::CloseHandle(process);
}
}
static void TerminateProcessByName(LPCWSTR aProcessName) {
HANDLE processes{::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)};
if (processes == INVALID_HANDLE_VALUE) {
return;
}
PROCESSENTRY32W process{};
process.dwSize = sizeof(PROCESSENTRY32W);
if (::Process32FirstW(processes, &process)) {
do {
if (::_wcsicmp(process.szExeFile, aProcessName) == 0) {
TerminateProcessById(process.th32ProcessID);
}
} while (::Process32NextW(processes, &process));
}
::CloseHandle(processes);
}
static UIWindowElement WaitForSetDefaultBrowserButton() {
const int kMaxAttempts{40};
const DWORD kRetryDelayMs{500};
for (int i{0}; i < kMaxAttempts; ++i) {
auto [window, button]{FindSetDefaultBrowserButton()};
if (window && button) {
return {window, button};
}
Sleep(kRetryDelayMs);
}
return {};
}
static bool IsElementFocus(const UIElement& aElement) {
BOOL isFocus{FALSE};
aElement->get_CurrentHasKeyboardFocus(&isFocus);
return isFocus;
}
class SetDefaultBrowserButtonTests : public ::testing::Test {
protected:
static void SetUpTestSuite() {
ASSERT_TRUE(GetAppRegName(sAppRegName));
RegisterAsBrowser();
}
static void TearDownTestSuite() { UnregisterAsBrowser(); }
void TearDown() override { TerminateSystemSettings(); }
void TerminateSystemSettings() {
LPCWSTR processName{L"SystemSettings.exe"};
TerminateProcessByName(processName);
}
private:
static void RegisterAsBrowser() {
const std::wstring appRegName{sAppRegName.get()};
WCHAR exePath[MAX_PATH];
::GetModuleFileNameW(nullptr, exePath, MAX_PATH);
const std::wstring appName{L"Firefox Test"};
// clang-format off
// HKEY_CURRENT_USER\Software\RegisteredApplications
{
const std::wstring key{L"Software\\RegisteredApplications"};
const std::wstring value{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\Capabilities"};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Some(appRegName), value));
}
// HKEY_CURRENT_USER\Software\Clients\StartMenuInternet\Firefox-XYZ
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Nothing(), appName));
}
// HKEY_CURRENT_USER\Software\Clients\StartMenuInternet\Firefox-XYZ\Capabilities
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\Capabilities"};
const std::wstring name{L"ApplicationDescription"};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Some(name), appName));
}
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\Capabilities"};
const std::wstring name{L"ApplicationIcon"};
const std::wstring value{std::wstring(exePath) + L",0"};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Some(name), value));
}
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\Capabilities"};
const std::wstring name{L"ApplicationName"};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Some(name), appName));
}
// HKEY_CURRENT_USER\Software\Clients\StartMenuInternet\Firefox-XYZ\Capabilities\URLAssociations
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\Capabilities\\URLAssociations"};
const std::wstring name{L"http"};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Some(name), appRegName));
}
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\Capabilities\\URLAssociations"};
const std::wstring name{L"https"};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Some(name), appRegName));
}
// HKEY_CURRENT_USER\Software\Clients\StartMenuInternet\Firefox-XYZ\DefaultIcon
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\DefaultIcon"};
const std::wstring value{std::wstring(exePath) + L",0"};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Nothing(), value));
}
// HKEY_CURRENT_USER\Software\Clients\StartMenuInternet\Firefox-XYZ\shell\open\command
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\shell\\open\\command"};
const std::wstring value{L"\"" + std::wstring(exePath) + L"\""};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Nothing(), value));
}
// HKEY_CURRENT_USER\Software\Classes\Firefox-XYZ\shell\open\command
{
const std::wstring key{L"Software\\Classes\\" + appRegName + L"\\shell\\open\\command"};
const std::wstring value{L"\"" + std::wstring(exePath) + L"\" -osint -url \"%1\""};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Nothing(), value));
}
// clang-format on
}
static void UnregisterAsBrowser() {
const std::wstring appRegName{sAppRegName.get()};
// clang-format off
// HKEY_CURRENT_USER\Software\RegisteredApplications
{
const std::wstring key{L"Software\\RegisteredApplications"};
ASSERT_TRUE(RegDeleteValue(HKEY_CURRENT_USER, key, appRegName));
}
// HKEY_CURRENT_USER\Software\Clients\StartMenuInternet\Firefox-XYZ
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName};
ASSERT_TRUE(RegDeleteKey(HKEY_CURRENT_USER, key));
}
// HKEY_CURRENT_USER\Software\Classes\Firefox-XYZ
{
const std::wstring key{L"Software\\Classes\\" + appRegName};
ASSERT_TRUE(RegDeleteKey(HKEY_CURRENT_USER, key));
}
// clang-format on
}
inline static mozilla::UniquePtr<WCHAR[]> sAppRegName;
};
TEST_F(SetDefaultBrowserButtonTests, FindDefaultBrowserButton) {
ASSERT_TRUE(LaunchModernSettingsDialogDefaultApps());
auto [window, button]{WaitForSetDefaultBrowserButton()};
ASSERT_THAT(window, testing::NotNull());
ASSERT_THAT(button, testing::NotNull());
}
TEST_F(SetDefaultBrowserButtonTests, FocusDefaultBrowserButton) {
ASSERT_TRUE(LaunchModernSettingsDialogDefaultApps());
auto [window, button]{WaitForSetDefaultBrowserButton()};
ASSERT_THAT(window, testing::NotNull());
ASSERT_THAT(button, testing::NotNull());
FocusElement(window, button);
ASSERT_TRUE(IsElementFocus(button));
}

View File

@@ -0,0 +1,220 @@
/* 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 "WindowsDefaultBrowserTests.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <string_view>
#include <string>
#include <windows.h>
#include <ole2.h>
#include <uiautomation.h>
#include <tlhelp32.h>
#include "mozilla/Maybe.h"
#include "mozilla/UniquePtr.h"
#include "WindowsDefaultBrowser.h"
static bool RegDeleteKey(HKEY aRoot, std::wstring_view aKey) {
LSTATUS ls{::RegDeleteTreeW(HKEY_CURRENT_USER, aKey.data())};
return (ls == ERROR_SUCCESS);
}
static bool RegDeleteValue(HKEY aRoot, std::wstring_view aKey,
std::wstring_view aName) {
HKEY key;
LSTATUS ls{
::RegOpenKeyExW(HKEY_CURRENT_USER, aKey.data(), 0, KEY_SET_VALUE, &key)};
if (ls == ERROR_SUCCESS) {
::RegDeleteValueW(key, aName.data());
::RegCloseKey(key);
return true;
}
return false;
}
static bool RegWriteStringValue(HKEY aRoot, std::wstring_view aKey,
const mozilla::Maybe<std::wstring>& aName,
std::wstring_view aValue) {
HKEY key;
LSTATUS ls{::RegCreateKeyExW(aRoot, aKey.data(), 0, nullptr,
REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, nullptr,
&key, nullptr)};
if (ls != ERROR_SUCCESS) {
return false;
}
const BYTE* value{reinterpret_cast<const BYTE*>(aValue.data())};
const DWORD size{static_cast<DWORD>((aValue.size() + 1) * sizeof(WCHAR))};
ls = ::RegSetValueExW(key, aName ? aName->c_str() : nullptr, 0, REG_SZ, value,
size);
::RegCloseKey(key);
return (ls == ERROR_SUCCESS);
}
static void TerminateProcessById(DWORD aProcessId) {
HANDLE process{
::OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, aProcessId)};
if (process) {
::TerminateProcess(process, 0);
::WaitForSingleObject(process, INFINITE);
::CloseHandle(process);
}
}
static void TerminateProcessByName(LPCWSTR aProcessName) {
HANDLE processes{::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)};
if (processes == INVALID_HANDLE_VALUE) {
return;
}
PROCESSENTRY32W process{};
process.dwSize = sizeof(PROCESSENTRY32W);
if (::Process32FirstW(processes, &process)) {
do {
if (::_wcsicmp(process.szExeFile, aProcessName) == 0) {
TerminateProcessById(process.th32ProcessID);
}
} while (::Process32NextW(processes, &process));
}
::CloseHandle(processes);
}
void FindSetDefaultBrowserButtonTests::SetUpTestSuite() {
ASSERT_TRUE(GetAppRegName(sAppRegName));
RegisterAsBrowser();
}
void FindSetDefaultBrowserButtonTests::TearDownTestSuite() {
UnregisterAsBrowser();
}
void FindSetDefaultBrowserButtonTests::TearDown() { TerminateSystemSettings(); }
void FindSetDefaultBrowserButtonTests::TerminateSystemSettings() {
LPCWSTR processName{L"SystemSettings.exe"};
TerminateProcessByName(processName);
}
UIWindowElement
FindSetDefaultBrowserButtonTests::WaitForSetDefaultBrowserButton() {
const int kMaxAttempts{40};
const DWORD kRetryDelayMs{500};
for (int i{0}; i < kMaxAttempts; ++i) {
auto [window, button]{FindSetDefaultBrowserButton()};
if (window && button) {
return {window, button};
}
Sleep(kRetryDelayMs);
}
return {};
}
void FindSetDefaultBrowserButtonTests::RegisterAsBrowser() {
const std::wstring appRegName{sAppRegName.get()};
WCHAR exePath[MAX_PATH];
::GetModuleFileNameW(nullptr, exePath, MAX_PATH);
const std::wstring appName{L"Firefox Test"};
// clang-format off
// HKEY_CURRENT_USER\Software\RegisteredApplications
{
const std::wstring key{L"Software\\RegisteredApplications"};
const std::wstring value{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\Capabilities"};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Some(appRegName), value));
}
// HKEY_CURRENT_USER\Software\Clients\StartMenuInternet\Firefox-XYZ
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Nothing(), appName));
}
// HKEY_CURRENT_USER\Software\Clients\StartMenuInternet\Firefox-XYZ\Capabilities
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\Capabilities"};
const std::wstring name{L"ApplicationDescription"};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Some(name), appName));
}
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\Capabilities"};
const std::wstring name{L"ApplicationIcon"};
const std::wstring value{std::wstring(exePath) + L",0"};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Some(name), value));
}
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\Capabilities"};
const std::wstring name{L"ApplicationName"};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Some(name), appName));
}
// HKEY_CURRENT_USER\Software\Clients\StartMenuInternet\Firefox-XYZ\Capabilities\URLAssociations
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\Capabilities\\URLAssociations"};
const std::wstring name{L"http"};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Some(name), appRegName));
}
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\Capabilities\\URLAssociations"};
const std::wstring name{L"https"};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Some(name), appRegName));
}
// HKEY_CURRENT_USER\Software\Clients\StartMenuInternet\Firefox-XYZ\DefaultIcon
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\DefaultIcon"};
const std::wstring value{std::wstring(exePath) + L",0"};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Nothing(), value));
}
// HKEY_CURRENT_USER\Software\Clients\StartMenuInternet\Firefox-XYZ\shell\open\command
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName + L"\\shell\\open\\command"};
const std::wstring value{L"\"" + std::wstring(exePath) + L"\""};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Nothing(), value));
}
// HKEY_CURRENT_USER\Software\Classes\Firefox-XYZ\shell\open\command
{
const std::wstring key{L"Software\\Classes\\" + appRegName + L"\\shell\\open\\command"};
const std::wstring value{L"\"" + std::wstring(exePath) + L"\" -osint -url \"%1\""};
ASSERT_TRUE(RegWriteStringValue(HKEY_CURRENT_USER, key, mozilla::Nothing(), value));
}
// clang-format on
}
void FindSetDefaultBrowserButtonTests::UnregisterAsBrowser() {
const std::wstring appRegName{sAppRegName.get()};
// clang-format off
// HKEY_CURRENT_USER\Software\RegisteredApplications
{
const std::wstring key{L"Software\\RegisteredApplications"};
ASSERT_TRUE(RegDeleteValue(HKEY_CURRENT_USER, key, appRegName));
}
// HKEY_CURRENT_USER\Software\Clients\StartMenuInternet\Firefox-XYZ
{
const std::wstring key{L"Software\\Clients\\StartMenuInternet\\" + appRegName};
ASSERT_TRUE(RegDeleteKey(HKEY_CURRENT_USER, key));
}
// HKEY_CURRENT_USER\Software\Classes\Firefox-XYZ
{
const std::wstring key{L"Software\\Classes\\" + appRegName};
ASSERT_TRUE(RegDeleteKey(HKEY_CURRENT_USER, key));
}
// clang-format on
}
mozilla::UniquePtr<WCHAR[]> FindSetDefaultBrowserButtonTests::sAppRegName;
TEST_F(FindSetDefaultBrowserButtonTests, ButtonFound) {
ASSERT_TRUE(LaunchModernSettingsDialogDefaultApps());
auto [window, button]{WaitForSetDefaultBrowserButton()};
ASSERT_THAT(window, testing::NotNull());
ASSERT_THAT(button, testing::NotNull());
}

View File

@@ -0,0 +1,30 @@
/* 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 WINDOWS_DEFAULT_BROWSER_TESTS_H_
#define WINDOWS_DEFAULT_BROWSER_TESTS_H_
#include <gtest/gtest.h>
#include <windows.h>
#include "mozilla/UniquePtr.h"
#include "WindowsDefaultBrowser.h"
class FindSetDefaultBrowserButtonTests : public ::testing::Test {
protected:
static void SetUpTestSuite();
static void TearDownTestSuite();
void TearDown() override;
void TerminateSystemSettings();
static UIWindowElement WaitForSetDefaultBrowserButton();
private:
static void RegisterAsBrowser();
static void UnregisterAsBrowser();
static mozilla::UniquePtr<WCHAR[]> sAppRegName;
};
#endif

View File

@@ -0,0 +1,94 @@
/* 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 <gtest/gtest.h>
#include <gmock/gmock.h>
#include <cstdint>
#include <windows.h>
#include <ole2.h>
#include <uiautomation.h>
#include "WindowsDefaultBrowser.h"
#include "WindowsDefaultBrowserTests.h"
#include "WindowsUIElement.h"
#include "mozilla/RefPtr.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/WindowsVersion.h"
#include "mozilla/gtest/MozAssertions.h"
#include "nsCOMPtr.h"
#include "nsISerialEventTarget.h"
#include "nsITimer.h"
#include "nsLiteralString.h"
#include "nsThreadUtils.h"
static bool IsElementFocus(const UIElement& aElement) {
BOOL isFocus{FALSE};
aElement->get_CurrentHasKeyboardFocus(&isFocus);
return isFocus;
}
class WindowsUIElementTests : public FindSetDefaultBrowserButtonTests {};
TEST_F(WindowsUIElementTests, DefaultBrowserButtonFocused) {
ASSERT_TRUE(LaunchModernSettingsDialogDefaultApps());
auto [window, button]{WaitForSetDefaultBrowserButton()};
ASSERT_THAT(window, testing::NotNull());
ASSERT_THAT(button, testing::NotNull());
RefPtr<mozilla::WindowsUIElement> element{
new mozilla::WindowsUIElement(window, button)};
element->Focus();
ASSERT_TRUE(IsElementFocus(button));
}
TEST_F(WindowsUIElementTests, DefaultBrowserButtonStopsMoving) {
if (!mozilla::IsWin11OrLater()) {
// This test is intended to run only on Win11 since it's part of the feature
// that displays the Kit image behind the Set Default Browser button
return;
}
ASSERT_TRUE(LaunchModernSettingsDialogDefaultApps());
auto [window, button]{WaitForSetDefaultBrowserButton()};
ASSERT_THAT(window, testing::NotNull());
ASSERT_THAT(button, testing::NotNull());
RefPtr<mozilla::WindowsUIElement> element{
new mozilla::WindowsUIElement(window, button)};
nsCOMPtr<nsISerialEventTarget> queue;
ASSERT_NS_SUCCEEDED(NS_CreateBackgroundTaskQueue(
"WindowsUIElementTests::DefaultBrowserButtonStopsMovingQueue",
getter_AddRefs(queue)));
int ticks{0};
bool elementIsMoving{true};
bool done{false};
nsCOMPtr<nsITimer> timer;
auto callback{[element, &ticks, &elementIsMoving, &done](nsITimer* aTimer) {
elementIsMoving = element->IsMoving().valueOr(true);
constexpr int kMaxTicks{10};
if (!elementIsMoving || ++ticks >= kMaxTicks) {
aTimer->Cancel();
NS_DispatchToMainThread(NS_NewRunnableFunction(
"WindowsUIElementTests::DefaultBrowserButtonStopsMovingDone",
[&done] { done = true; }));
}
}};
const uint32_t kDelayMs{500};
ASSERT_NS_SUCCEEDED(NS_NewTimerWithCallback(
getter_AddRefs(timer), callback, kDelayMs, nsITimer::TYPE_REPEATING_SLACK,
"WindowsUIElementTests::DefaultBrowserButtonStopsMovingTimer"_ns, queue));
mozilla::SpinEventLoopUntil(
"WindowsUIElementTests::DefaultBrowserButtonStopsMovingLoop"_ns,
[&done] { return bool(done); });
ASSERT_FALSE(elementIsMoving);
}

View File

@@ -0,0 +1,275 @@
/* 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 <gtest/gtest.h>
#include <gmock/gmock.h>
#include <cstdint>
#include <vector>
#include <windows.h>
#include "WindowsUIOverlayImage.h"
#include "WindowsDefaultBrowser.h"
#include "WindowsDefaultBrowserTests.h"
#include "WindowsUIElement.h"
#include "mozilla/RefPtr.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/SyncRunnable.h"
#include "mozilla/WindowsVersion.h"
#include "mozilla/gtest/MozAssertions.h"
#include "nsCOMPtr.h"
#include "nsISerialEventTarget.h"
#include "nsISupportsImpl.h"
#include "nsITimer.h"
#include "nsLiteralString.h"
#include "nsThreadUtils.h"
namespace {
class CoverWindow final {
public:
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CoverWindow)
static RefPtr<CoverWindow> Create(HWND aWindow) {
RECT rect{};
if (!GetWindowRect(aWindow, &rect)) {
return nullptr;
}
constexpr LPCWSTR kWindowClassName{L"CoverWindow"};
WNDCLASSEXW windowClass{sizeof(windowClass)};
windowClass.lpfnWndProc = DefWindowProcW;
windowClass.hInstance = GetModuleHandleW(nullptr);
windowClass.lpszClassName = kWindowClassName;
RegisterClassExW(&windowClass);
HWND window{nullptr};
mozilla::SyncRunnable::DispatchToThread(
mozilla::GetMainThreadSerialEventTarget(),
NS_NewRunnableFunction("CoverWindow::Create", [&window, rect] {
window = CreateWindowExW(
WS_EX_TOPMOST, kWindowClassName, nullptr, WS_POPUP, rect.left,
rect.top, rect.right - rect.left, rect.bottom - rect.top, nullptr,
nullptr, GetModuleHandleW(nullptr), nullptr);
}));
if (!window) {
return nullptr;
}
ShowWindow(window, SW_SHOWNOACTIVATE);
return RefPtr<CoverWindow>{new CoverWindow(window)};
}
// Non-copyable and non-movable
CoverWindow(const CoverWindow&) = delete;
CoverWindow(CoverWindow&&) = delete;
CoverWindow& operator=(const CoverWindow&) = delete;
CoverWindow& operator=(CoverWindow&&) = delete;
private:
explicit CoverWindow(HWND aWindow) : mWindow{aWindow} {}
~CoverWindow() {
if (mWindow) {
NS_DispatchToMainThread(NS_NewRunnableFunction(
"CoverWindow::DestroyWindow",
[window = mWindow] { DestroyWindow(window); }));
mWindow = nullptr;
}
}
HWND mWindow;
};
} // namespace
class WindowsUIOverlayImageTests : public FindSetDefaultBrowserButtonTests {};
TEST_F(WindowsUIOverlayImageTests, OverlayHiddenWhenCovered) {
if (!mozilla::IsWin11OrLater()) {
// This test is intended to run only on Win11 since it's part of the feature
// that displays the Kit image behind the Set Default Browser button
return;
}
ASSERT_TRUE(LaunchModernSettingsDialogDefaultApps());
auto [window, button]{WaitForSetDefaultBrowserButton()};
ASSERT_THAT(window, testing::NotNull());
ASSERT_THAT(button, testing::NotNull());
RefPtr<mozilla::WindowsUIElement> element{
new mozilla::WindowsUIElement(window, button)};
nsCOMPtr<nsISerialEventTarget> queue;
ASSERT_NS_SUCCEEDED(NS_CreateBackgroundTaskQueue(
"WindowsUIOverlayImageTests::OverlayHiddenWhenCoveredQueue",
getter_AddRefs(queue)));
bool elementIsMoving{true};
RefPtr<mozilla::WindowsUIOverlayImage> overlayImage;
bool overlayImageIsVisibleBefore{false};
RefPtr<CoverWindow> coverWindow;
bool overlayImageIsVisibleAfter{true};
bool done{false};
int ticks{0};
int state{0};
nsCOMPtr<nsITimer> timer;
auto callback{[element, &elementIsMoving, &overlayImage,
&overlayImageIsVisibleBefore, window, &coverWindow,
&overlayImageIsVisibleAfter, &done, &ticks,
&state](nsITimer* aTimer) {
bool finished{false};
switch (state) {
case 0:
// Wait until the element stops moving
elementIsMoving = element->IsMoving().valueOr(true);
if (!elementIsMoving) {
++state;
}
break;
case 1:
// Create the overlay image
overlayImage = element->CreateOverlayImage(
mozilla::WindowsUIOverlayImage::DisplayMode::Static);
finished = !overlayImage;
++state;
break;
case 2:
// The overlay image is visible before anything covers it
overlayImageIsVisibleBefore = overlayImage->IsVisible();
if (overlayImageIsVisibleBefore) {
++state;
}
break;
case 3:
// Cover the Windows Settings window
coverWindow = CoverWindow::Create(window);
finished = !coverWindow;
++state;
break;
case 4:
// The overlay image shouldn't be visible
overlayImageIsVisibleAfter = overlayImage->IsVisible();
finished = true;
break;
}
constexpr int kMaxTicks{40};
if (finished || ++ticks >= kMaxTicks) {
aTimer->Cancel();
NS_DispatchToMainThread(NS_NewRunnableFunction(
"WindowsUIOverlayImageTests::OverlayHiddenWhenCoveredDone",
[&done] { done = true; }));
}
}};
const uint32_t kDelayMs{500};
ASSERT_NS_SUCCEEDED(NS_NewTimerWithCallback(
getter_AddRefs(timer), callback, kDelayMs, nsITimer::TYPE_REPEATING_SLACK,
"WindowsUIOverlayImageTests::OverlayHiddenWhenCoveredTimer"_ns, queue));
mozilla::SpinEventLoopUntil(
"WindowsUIOverlayImageTests::OverlayHiddenWhenCoveredLoop"_ns,
[&done] { return bool(done); });
ASSERT_FALSE(elementIsMoving);
ASSERT_THAT(overlayImage, testing::NotNull());
ASSERT_TRUE(overlayImageIsVisibleBefore);
ASSERT_THAT(coverWindow, testing::NotNull());
ASSERT_FALSE(overlayImageIsVisibleAfter);
}
TEST(GetFrameDurationTests, KitImageFirstFrameDurationIs40ms)
{
nsCOMPtr<nsIFile> imageFile{mozilla::GetImageFile()};
ASSERT_THAT(imageFile, testing::NotNull());
RefPtr<IWICImagingFactory> factory{mozilla::CreateWICImagingFactory()};
ASSERT_THAT(factory, testing::NotNull());
RefPtr<IWICBitmapDecoder> decoder{
mozilla::CreateWICBitmapDecoder(factory, imageFile)};
ASSERT_THAT(decoder, testing::NotNull());
RefPtr<IWICBitmapFrameDecode> frame;
constexpr UINT kFrameNumber{0};
ASSERT_TRUE(
SUCCEEDED(decoder->GetFrame(kFrameNumber, getter_AddRefs(frame))));
const mozilla::TimeDuration kSentinelFrameDuration{
mozilla::TimeDuration::FromMilliseconds(999)};
const mozilla::TimeDuration duration{
mozilla::GetFrameDuration(frame, kSentinelFrameDuration)};
// Kit image (kit.gif) frame duration is 40 ms
const mozilla::TimeDuration kFrameDuration{
mozilla::TimeDuration::FromMilliseconds(40)};
EXPECT_EQ(duration, kFrameDuration);
}
class ComputeAdvancedFrameTests : public testing::Test {
protected:
const mozilla::TimeDuration mFrameDuration{
mozilla::TimeDuration::FromMilliseconds(25)};
const std::vector<mozilla::WindowsUIOverlayImage::Frame> mFrames{
4, mozilla::WindowsUIOverlayImage::Frame{{}, mFrameDuration}};
};
TEST_F(ComputeAdvancedFrameTests, DoesNotAdvanceWhenTimeBelowFrameDuration) {
mozilla::TimeDuration accumulated{
mozilla::TimeDuration::FromMilliseconds(20)};
size_t frame{mozilla::ComputeAdvancedFrame(mFrames, 0, accumulated)};
EXPECT_EQ(frame, 0u);
EXPECT_EQ(accumulated, mozilla::TimeDuration::FromMilliseconds(20));
}
TEST_F(ComputeAdvancedFrameTests, AdvancesSingleFrame) {
mozilla::TimeDuration accumulated{
mozilla::TimeDuration::FromMilliseconds(30)};
size_t frame{mozilla::ComputeAdvancedFrame(mFrames, 0, accumulated)};
EXPECT_EQ(frame, 1u);
EXPECT_EQ(accumulated,
mozilla::TimeDuration::FromMilliseconds(30) - mFrameDuration);
}
TEST_F(ComputeAdvancedFrameTests, AdvancesMultipleFramesInOneCall) {
mozilla::TimeDuration accumulated{
mozilla::TimeDuration::FromMilliseconds(60)};
size_t frame{mozilla::ComputeAdvancedFrame(mFrames, 0, accumulated)};
EXPECT_EQ(frame, 2u);
EXPECT_EQ(accumulated,
mozilla::TimeDuration::FromMilliseconds(60) - mFrameDuration * 2);
}
TEST_F(ComputeAdvancedFrameTests, DoesNotAdvancePastLastFrame) {
mozilla::TimeDuration accumulated{
mozilla::TimeDuration::FromMilliseconds(1000)};
size_t frame{mozilla::ComputeAdvancedFrame(mFrames, 0, accumulated)};
EXPECT_EQ(frame, 3u);
// The time for the frames not reached is left untouched
EXPECT_EQ(accumulated,
mozilla::TimeDuration::FromMilliseconds(1000) - mFrameDuration * 3);
}
TEST_F(ComputeAdvancedFrameTests, StaysOnLastFrame) {
mozilla::TimeDuration accumulated{
mozilla::TimeDuration::FromMilliseconds(1000)};
size_t frame{mozilla::ComputeAdvancedFrame(mFrames, 3, accumulated)};
EXPECT_EQ(frame, 3u);
EXPECT_EQ(accumulated, mozilla::TimeDuration::FromMilliseconds(1000));
}

View File

@@ -1,14 +1,16 @@
# 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/.
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
LOCAL_INCLUDES += ["/browser/components/shell"]
UNIFIED_SOURCES += [
"LimitedAccessFeatureTests.cpp",
"SetDefaultBrowserButtonTests.cpp",
"ShellLinkTests.cpp",
]
FINAL_LIBRARY = "xul-gtest"
# 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/.
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
LOCAL_INCLUDES += ["/browser/components/shell"]
UNIFIED_SOURCES += [
"LimitedAccessFeatureTests.cpp",
"ShellLinkTests.cpp",
"WindowsDefaultBrowserTests.cpp",
"WindowsUIElementTests.cpp",
"WindowsUIOverlayImageTests.cpp",
]
FINAL_LIBRARY = "xul-gtest"

View File

@@ -0,0 +1,901 @@
/* Any copyright is dedicated to the Public Domain.
* https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { MockRegistrar } = ChromeUtils.importESModule(
"resource://testing-common/MockRegistrar.sys.mjs"
);
const { sinon } = ChromeUtils.importESModule(
"resource://testing-common/Sinon.sys.mjs"
);
// This is an xpcshell test, but a sibling browser.toml makes eslint apply the
// browser-test env, where TestUtils is a predefined global. It isn't one in
// xpcshell (there's no head.js here to import it either), so the import is
// required.
// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix
const { TestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TestUtils.sys.mjs"
);
const {
CustomIconManager,
ICON_CATALOG,
resolvePreview,
resolveResourceId,
OS_LIGHT,
OS_DARK,
} = ChromeUtils.importESModule(
"moz-src:///browser/components/shell/CustomIconManager.sys.mjs"
);
// Importing this module constructs the toolkit profile service as a side effect,
// which requires setupProfileService() to have run, so it must stay lazy and
// only be touched from add_setup() onwards.
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
SelectableProfileService:
"resource:///modules/profiles/SelectableProfileService.sys.mjs",
});
const PREF_ICON_ID = "browser.shell.customIcon.id";
const TEST_AUMID = "Test.Firefox.AUMID";
const TEST_SHORTCUTS = ["C:\\fake\\Desktop\\Nightly.lnk"];
const RETRO_RESOURCE_ID = ICON_CATALOG.retro2004.iconResourceId;
// CustomIconManager.apply() refuses to run on MSIX (packaged) builds, so on the
// MSIX CI job every task except the MSIX-specific one (which fakes the
// condition itself and runs everywhere) is skipped.
const ON_MSIX = Services.sysinfo.getProperty("hasWinPackageId");
// add_task() mutates the options object it is handed (tagging it isTask), so
// each call needs its own fresh object rather than a shared one.
function skipOnMsix() {
return { skip_if: () => ON_MSIX };
}
function exePath() {
return Services.dirsvc.get("XREExeF", Ci.nsIFile).path;
}
let shellServiceMock = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWindowsShellService]),
enumerateInstallShortcuts: sinon.stub(),
setShortcutsIcon: sinon.stub(),
createShortcut: sinon.stub(),
};
let winTaskbarMock = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWinTaskbar]),
setAllWindowIcons: sinon.stub(),
refreshTaskbarButtons: sinon.stub(),
get defaultGroupId() {
return TEST_AUMID;
},
};
// ensureAppliedOrRevert() awaits SelectableProfileService.init() on the startup
// path so the shared custom-icon pref can finish loading from the profiles
// database before it reconciles. We stub it here so these unit tests don't spin up
// the real profiles machinery.
let spsInitStub;
// Reset stub history + default behaviour, clear the pref, and drop any recorded
// Glean values before each task.
function resetMocks() {
shellServiceMock.enumerateInstallShortcuts.reset();
shellServiceMock.enumerateInstallShortcuts.resolves(TEST_SHORTCUTS.slice());
shellServiceMock.setShortcutsIcon.reset();
shellServiceMock.setShortcutsIcon.resolves();
shellServiceMock.createShortcut.reset();
shellServiceMock.createShortcut.resolves();
winTaskbarMock.setAllWindowIcons.reset();
winTaskbarMock.refreshTaskbarButtons.reset();
spsInitStub.reset();
spsInitStub.resolves();
Services.prefs.clearUserPref(PREF_ICON_ID);
Services.fog.testResetFOG();
}
// The single "changed" event recorded since the last reset, or undefined if
// none. Fails if more than one was recorded (each task exercises one change).
function singleChangedEvent() {
let events = Glean.customIcon.changed.testGetValue() ?? [];
Assert.lessOrEqual(events.length, 1, "at most one changed event recorded");
return events[0];
}
// Since we're importing SelectableProfileService for this test, we lift some of
// the setup from toolkit/profile/test/xpcshell/head.js which lets the service
// be imported and executed in debug xpcshell tests.
function setupProfileService() {
let profD = do_get_profile();
let dataHome = profD.clone();
dataHome.append("data");
dataHome.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
let dataHomeLocal = profD.clone();
dataHomeLocal.append("local");
dataHomeLocal.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
let xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].getService(
Ci.nsIXREDirProvider
);
xreDirProvider.setUserDataDirectory(dataHome, false);
xreDirProvider.setUserDataDirectory(dataHomeLocal, true);
}
add_setup(function () {
setupProfileService();
Services.fog.initializeFOG();
let shellCid = MockRegistrar.register(
"@mozilla.org/browser/shell-service;1",
shellServiceMock
);
let taskbarCid = MockRegistrar.register(
"@mozilla.org/windows-taskbar;1",
winTaskbarMock
);
spsInitStub = sinon.stub(lazy.SelectableProfileService, "init").resolves();
registerCleanupFunction(() => {
spsInitStub.restore();
MockRegistrar.unregister(taskbarCid);
MockRegistrar.unregister(shellCid);
Services.prefs.clearUserPref(PREF_ICON_ID);
});
});
/**
* This test verifies that apply() enumerates shortcuts by the default AUMID,
* writes the catalog resource ID (positive, un-negated) and the executable
* path to the matching shortcuts, sets the runtime window icon, and records
* the pref.
*/
add_task(
skipOnMsix(),
async function test_apply_updates_shortcuts_pref_and_runtime() {
resetMocks();
await CustomIconManager.apply("retro2004");
Assert.ok(
shellServiceMock.enumerateInstallShortcuts.calledOnceWithExactly(
TEST_AUMID
),
"enumerateInstallShortcuts called once with the default AUMID"
);
Assert.ok(
shellServiceMock.setShortcutsIcon.calledOnce,
"setShortcutsIcon called once"
);
let [shortcuts, iconPath, resourceId] =
shellServiceMock.setShortcutsIcon.getCall(0).args;
Assert.deepEqual(
shortcuts,
TEST_SHORTCUTS,
"passed the enumerated shortcuts through"
);
Assert.equal(iconPath, exePath(), "icon source is the running executable");
Assert.equal(
resourceId,
RETRO_RESOURCE_ID,
"passed the catalog resource ID as-is (negation happens in C++, not JS)"
);
Assert.ok(
winTaskbarMock.setAllWindowIcons.calledOnceWithExactly(RETRO_RESOURCE_ID),
"runtime window icon set to the retro resource ID"
);
Assert.equal(
Services.prefs.getStringPref(PREF_ICON_ID, ""),
"retro2004",
"pref records the applied id"
);
let event = singleChangedEvent();
Assert.ok(event, "a changed event was recorded");
Assert.equal(event.category, "custom_icon", "event category");
Assert.equal(event.name, "changed", "event name");
Assert.equal(
event.extra.icon_id,
"retro2004",
"changed event carries the applied id"
);
}
);
/**
* This test verifies that apply() rejects when given an id absent from the
* catalog, without touching any shortcut or runtime state or the pref.
*/
add_task(skipOnMsix(), async function test_apply_unknown_id_throws() {
resetMocks();
await Assert.rejects(
CustomIconManager.apply("does-not-exist"),
/Unknown icon id/,
"apply rejects for an unknown catalog id"
);
Assert.ok(
shellServiceMock.setShortcutsIcon.notCalled,
"no shortcut work attempted for an unknown id"
);
Assert.ok(
winTaskbarMock.setAllWindowIcons.notCalled,
"no runtime work attempted for an unknown id"
);
Assert.equal(
Services.prefs.getStringPref(PREF_ICON_ID, ""),
"",
"pref left untouched"
);
});
/**
* This test verifies that apply() throws on MSIX (packaged) builds, where the
* feature is unsupported, without touching shortcuts, the runtime icon, or the
* pref.
*/
add_task(async function test_apply_throws_on_msix() {
resetMocks();
// Fake an MSIX build by flipping the sysinfo property the manager checks.
// nsSystemInfo is a writable property bag, so set it directly and restore it.
let bag = Services.sysinfo.QueryInterface(Ci.nsIWritablePropertyBag2);
let original = bag.getProperty("hasWinPackageId");
bag.setPropertyAsBool("hasWinPackageId", true);
try {
await Assert.rejects(
CustomIconManager.apply("retro2004"),
/MSIX/,
"apply rejects on an MSIX build"
);
Assert.ok(
shellServiceMock.setShortcutsIcon.notCalled,
"no shortcut work attempted on MSIX"
);
Assert.ok(
winTaskbarMock.setAllWindowIcons.notCalled,
"no runtime work attempted on MSIX"
);
Assert.equal(
Services.prefs.getStringPref(PREF_ICON_ID, ""),
"",
"pref left untouched on MSIX"
);
} finally {
bag.setPropertyAsBool("hasWinPackageId", original);
}
});
/**
* This test verifies that revert() resets matching shortcuts to the
* executable's default icon (resource ID 0), clears the runtime override, and
* clears the pref.
*/
add_task(
skipOnMsix(),
async function test_revert_resets_shortcuts_pref_and_runtime() {
resetMocks();
Services.prefs.setStringPref(PREF_ICON_ID, "retro2004");
await CustomIconManager.revert();
Assert.ok(
shellServiceMock.setShortcutsIcon.calledOnce,
"setShortcutsIcon called once"
);
let [, iconPath, resourceId] =
shellServiceMock.setShortcutsIcon.getCall(0).args;
Assert.equal(iconPath, exePath(), "reverts using the executable path");
Assert.equal(
resourceId,
0,
"resource ID 0 selects the executable's default icon"
);
Assert.ok(
winTaskbarMock.setAllWindowIcons.calledOnceWithExactly(0),
"runtime window icon cleared (0)"
);
Assert.ok(!Services.prefs.prefHasUserValue(PREF_ICON_ID), "pref cleared");
let event = singleChangedEvent();
Assert.ok(event, "reverting from a custom icon records a changed event");
Assert.equal(
event.extra.icon_id,
"default",
"revert records the default id as the new selection"
);
}
);
/**
* This test verifies that when enumeration matches no shortcuts, apply() skips
* setShortcutsIcon but still applies the runtime icon and records the pref, so
* the running window updates even though no shortcut could be changed.
*/
add_task(skipOnMsix(), async function test_apply_no_matching_shortcuts() {
resetMocks();
shellServiceMock.enumerateInstallShortcuts.resolves([]);
// Must not throw even though nothing matched.
await CustomIconManager.apply("retro2004");
Assert.ok(
shellServiceMock.setShortcutsIcon.notCalled,
"setShortcutsIcon not called when enumeration matched nothing"
);
Assert.ok(
winTaskbarMock.setAllWindowIcons.calledOnceWithExactly(RETRO_RESOURCE_ID),
"runtime icon still applied even though no shortcut changed"
);
Assert.equal(
Services.prefs.getStringPref(PREF_ICON_ID, ""),
"retro2004",
"pref still recorded"
);
Assert.equal(
singleChangedEvent()?.extra.icon_id,
"retro2004",
"changed event still recorded even though no shortcut matched"
);
});
/**
* This test verifies that when setShortcutsIcon rejects, apply() logs and
* swallows the failure rather than throwing, and still applies the runtime
* icon and pref.
*/
add_task(
skipOnMsix(),
async function test_apply_shortcut_write_failure_is_swallowed() {
resetMocks();
shellServiceMock.setShortcutsIcon.rejects(
Components.Exception(
"mock setShortcutsIcon failure",
Cr.NS_ERROR_NOT_AVAILABLE
)
);
// A shortcut-write failure is logged, not thrown.
await CustomIconManager.apply("retro2004");
Assert.ok(
shellServiceMock.setShortcutsIcon.calledOnce,
"setShortcutsIcon was attempted"
);
Assert.ok(
winTaskbarMock.setAllWindowIcons.calledOnceWithExactly(RETRO_RESOURCE_ID),
"runtime icon still applied despite the shortcut-write failure"
);
Assert.equal(
Services.prefs.getStringPref(PREF_ICON_ID, ""),
"retro2004",
"pref still recorded"
);
Assert.equal(
singleChangedEvent()?.extra.icon_id,
"retro2004",
"changed event still recorded despite the shortcut-write failure"
);
}
);
/**
* This test verifies that re-applying the icon that is already active (as the
* theme observer and startup reconcile do) records no changed event, so the
* probe only fires on a genuine change of icon.
*/
add_task(skipOnMsix(), async function test_apply_same_id_records_no_change() {
resetMocks();
await CustomIconManager.apply("retro2004");
Assert.ok(singleChangedEvent(), "first apply records a change");
Services.fog.testResetFOG();
await CustomIconManager.apply("retro2004");
Assert.equal(
Glean.customIcon.changed.testGetValue(),
undefined,
"re-applying the same id records no changed event"
);
});
/**
* This test verifies that revert() over the already-default state (no custom
* icon active) records no changed event.
*/
add_task(
skipOnMsix(),
async function test_revert_when_default_records_nothing() {
resetMocks();
await CustomIconManager.revert();
Assert.equal(
Glean.customIcon.changed.testGetValue(),
undefined,
"reverting when already default records no changed event"
);
}
);
/**
* This test verifies that a rejected apply() (unknown id) records no changed
* event.
*/
add_task(skipOnMsix(), async function test_unknown_id_records_no_change() {
resetMocks();
await Assert.rejects(
CustomIconManager.apply("does-not-exist"),
/Unknown icon id/,
"apply rejects for an unknown catalog id"
);
Assert.equal(
Glean.customIcon.changed.testGetValue(),
undefined,
"no changed event recorded for a rejected apply"
);
});
/**
* This test verifies that ensureAppliedOrRevert() records the current-icon
* string metric once at startup: the active id for a known custom icon, and
* "default" when no custom icon is set.
*/
add_task(
skipOnMsix(),
async function test_ensureAppliedOrRevert_records_current() {
resetMocks();
Services.prefs.setStringPref(PREF_ICON_ID, "retro2004");
await CustomIconManager.ensureAppliedOrRevert();
Assert.equal(
Glean.customIcon.current.testGetValue(),
"retro2004",
"current records the active custom icon id"
);
resetMocks();
await CustomIconManager.ensureAppliedOrRevert();
Assert.equal(
Glean.customIcon.current.testGetValue(),
"default",
"current records the default id when no custom icon is set"
);
}
);
/**
* This test verifies that ensureAppliedOrRevert() with a pref naming a known
* catalog id re-applies the runtime icon only, without rewriting shortcuts,
* and keeps the pref.
*/
add_task(
skipOnMsix(),
async function test_ensureAppliedOrRevert_applies_known_id() {
resetMocks();
Services.prefs.setStringPref(PREF_ICON_ID, "retro2004");
await CustomIconManager.ensureAppliedOrRevert();
Assert.ok(
winTaskbarMock.setAllWindowIcons.calledOnceWithExactly(RETRO_RESOURCE_ID),
"runtime icon applied for a known id"
);
Assert.ok(
shellServiceMock.setShortcutsIcon.notCalled,
"ensureAppliedOrRevert does not rewrite shortcuts for a known id"
);
Assert.equal(
Services.prefs.getStringPref(PREF_ICON_ID, ""),
"retro2004",
"pref retained"
);
}
);
/**
* This test verifies that ensureAppliedOrRevert() with a pref naming an id
* absent from the catalog (e.g. a newer build's icon, or one since retired)
* reverts the shortcuts and runtime icon to default and clears the pref.
*/
add_task(
skipOnMsix(),
async function test_ensureAppliedOrRevert_reverts_unknown_id() {
resetMocks();
Services.prefs.setStringPref(PREF_ICON_ID, "icon-from-a-newer-build");
await CustomIconManager.ensureAppliedOrRevert();
// Unknown id -> revert: shortcuts reset to default, runtime cleared, pref
// cleared.
Assert.ok(
shellServiceMock.setShortcutsIcon.calledOnce,
"revert rewrote shortcuts"
);
Assert.equal(
shellServiceMock.setShortcutsIcon.getCall(0).args[2],
0,
"shortcuts reset to the default icon"
);
Assert.ok(
winTaskbarMock.setAllWindowIcons.calledOnceWithExactly(0),
"runtime icon cleared"
);
Assert.ok(!Services.prefs.prefHasUserValue(PREF_ICON_ID), "pref cleared");
Assert.equal(
Glean.customIcon.changed.testGetValue(),
undefined,
"the startup reconcile of an unknown id is not a user change"
);
}
);
/**
* This test verifies that ensureAppliedOrRevert() does nothing when no custom
* icon pref is set.
*/
add_task(
skipOnMsix(),
async function test_ensureAppliedOrRevert_noop_without_pref() {
resetMocks();
await CustomIconManager.ensureAppliedOrRevert();
Assert.ok(
shellServiceMock.setShortcutsIcon.notCalled,
"no shortcut work when no custom icon is recorded"
);
Assert.ok(
winTaskbarMock.setAllWindowIcons.notCalled,
"no runtime work when no custom icon is recorded"
);
}
);
/**
* This test checks that ensureAppliedOrRevert() will run setShortcutsIcon
* even if no custom ID is set, but only if it's being called because a remote
* profile updated.
*/
add_task(
skipOnMsix(),
async function test_ensureAppliedOrRevert_when_remoteProfileUpdated() {
resetMocks();
await CustomIconManager.ensureAppliedOrRevert(
true /* remoteProfileUpdated */
);
Assert.ok(
shellServiceMock.setShortcutsIcon.notCalled,
"Shortcuts were not modified if a remote profile cleared the icon"
);
Assert.ok(
winTaskbarMock.setAllWindowIcons.calledOnce,
"Runtime icon was modified if a remote profile cleared the icon"
);
}
);
/**
* This test verifies that the startup reconcile (remoteProfileUpdated = false)
* awaits SelectableProfileService.init() before reading the pref, so a custom
* icon synced late from the selectable-profiles database is still applied
* rather than missed. init() stands in for that shared-pref load and only sets
* the pref after yielding, so a reconcile that read the pref without awaiting
* would see no icon and apply nothing.
*/
add_task(
skipOnMsix(),
async function test_ensureAppliedOrRevert_waits_for_shared_pref_load() {
resetMocks();
spsInitStub.callsFake(async () => {
await Promise.resolve();
Services.prefs.setStringPref(PREF_ICON_ID, "retro2004");
});
await CustomIconManager.ensureAppliedOrRevert();
Assert.ok(
spsInitStub.calledOnce,
"The startup reconcile awaited SelectableProfileService.init()."
);
Assert.ok(
winTaskbarMock.setAllWindowIcons.calledOnceWithExactly(RETRO_RESOURCE_ID),
"The icon synced during init() was applied to runtime windows."
);
}
);
/**
* This test verifies the theme-aware catalog shape: a theme-aware icon exposes
* distinct dark/light variants and resolveResourceId()/resolvePreview() pick the
* scheme-specific asset, while a flat icon ignores the scheme.
*/
add_task(async function test_theme_aware_catalog() {
let minimal = ICON_CATALOG.minimal;
Assert.ok(minimal.variants, "minimal is theme-aware");
Assert.notEqual(
minimal.variants.dark.iconResourceId,
minimal.variants.light.iconResourceId,
"dark and light variants use distinct resource IDs"
);
Assert.notEqual(
resolvePreview(minimal, "dark"),
resolvePreview(minimal, "light"),
"resolvePreview returns the scheme-specific preview for a theme-aware icon"
);
Assert.equal(
resolveResourceId(minimal, "dark"),
minimal.variants.dark.iconResourceId,
"resolveResourceId picks the dark variant's id under a dark scheme"
);
Assert.equal(
resolveResourceId(minimal, "light"),
minimal.variants.light.iconResourceId,
"resolveResourceId picks the light variant's id under a light scheme"
);
let retro = ICON_CATALOG.retro2004;
Assert.ok(!retro.variants, "retro2004 is theme-agnostic");
Assert.equal(
resolvePreview(retro, "dark"),
resolvePreview(retro, "light"),
"a flat entry returns the same preview regardless of scheme"
);
Assert.equal(
resolveResourceId(retro, "dark"),
retro.iconResourceId,
"a flat entry returns its single resource id regardless of scheme"
);
});
/**
* This test verifies the OS-theme runtime behaviour of a theme-aware icon:
* apply() picks the variant matching the OS taskbar theme, and a
* look-and-feel-changed notification re-applies the other variant when (and
* only when) the OS theme actually flips.
*
* osColorScheme() reads the Windows registry, so we mock nsIWindowsRegKey for
* the duration of this task only (to avoid disturbing other registry reads) and
* drive SystemUsesLightTheme directly.
*/
add_task(skipOnMsix(), async function test_theme_change_reapplies_variant() {
resetMocks();
// The test flips this between OS_LIGHT/OS_DARK to simulate the user
// changing their OS theme.
let osTheme = OS_LIGHT;
let regKeyMock = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWindowsRegKey]),
open() {},
close() {},
hasValue: name => name === "SystemUsesLightTheme",
readIntValue: name => (name === "SystemUsesLightTheme" ? osTheme : 0),
};
let regCid = MockRegistrar.register(
"@mozilla.org/windows-registry-key;1",
regKeyMock
);
let { dark, light } = ICON_CATALOG.minimal.variants;
try {
// Startup registers the look-and-feel-changed observer.
CustomIconManager.applyRuntimeOverrideForStartup();
// Light OS theme -> Minimal applies the light variant.
osTheme = OS_LIGHT;
await CustomIconManager.apply("minimal");
Assert.equal(
shellServiceMock.setShortcutsIcon.lastCall.args[2],
light.iconResourceId,
"Minimal applies the light variant under a light OS theme"
);
Assert.ok(
winTaskbarMock.setAllWindowIcons.calledWith(light.iconResourceId),
"runtime window icon set to the light variant"
);
// Flip the OS to dark and notify -> the active Minimal re-applies as dark.
osTheme = OS_DARK;
shellServiceMock.setShortcutsIcon.resetHistory();
winTaskbarMock.setAllWindowIcons.resetHistory();
Services.obs.notifyObservers(null, "look-and-feel-changed");
await TestUtils.waitForCondition(
() => shellServiceMock.setShortcutsIcon.called,
"icon re-applied after the OS theme flipped to dark"
);
Assert.equal(
shellServiceMock.setShortcutsIcon.lastCall.args[2],
dark.iconResourceId,
"the dark variant is applied after the theme flips to dark"
);
// A notification with no actual theme change is a no-op (the
// gLastAppliedScheme guard short-circuits before re-applying).
shellServiceMock.setShortcutsIcon.resetHistory();
Services.obs.notifyObservers(null, "look-and-feel-changed");
Assert.ok(
shellServiceMock.setShortcutsIcon.notCalled,
"no re-apply when the OS theme is unchanged"
);
} finally {
MockRegistrar.unregister(regCid);
Services.prefs.clearUserPref(PREF_ICON_ID);
}
});
/**
* This test verifies that ensureShortcutInPerUserStartMenu() does not create a
* shortcut when one already exists in the per-user Start Menu Programs folder.
*/
add_task(
skipOnMsix(),
async function test_ensureShortcutInPerUserStartMenu_already_exists() {
resetMocks();
let programsPath = Services.dirsvc.get("Progs", Ci.nsIFile).path;
shellServiceMock.enumerateInstallShortcuts.resolves([
programsPath + "\\Nightly.lnk",
]);
await CustomIconManager.ensureShortcutInPerUserStartMenu();
Assert.ok(
shellServiceMock.createShortcut.notCalled,
"createShortcut not called when a per-user Start Menu shortcut already exists"
);
}
);
/**
* This test verifies that ensureShortcutInPerUserStartMenu() creates a shortcut
* in the Programs folder when none is found among the enumerated shortcuts.
*/
add_task(
skipOnMsix(),
async function test_ensureShortcutInPerUserStartMenu_creates_shortcut() {
resetMocks();
// TEST_SHORTCUTS ("C:\\fake\\Desktop\\Nightly.lnk") does not live in
// the Programs dir, so the method must create the missing shortcut.
await CustomIconManager.ensureShortcutInPerUserStartMenu();
Assert.ok(
shellServiceMock.createShortcut.calledOnce,
"createShortcut called when no per-user Start Menu shortcut exists"
);
let [exeFile, args, , iconFile, iconIndex, aumid, location, name] =
shellServiceMock.createShortcut.getCall(0).args;
Assert.equal(
exeFile.path,
exePath(),
"shortcut targets the running executable"
);
Assert.deepEqual(args, [], "no extra arguments");
Assert.equal(iconFile.path, exePath(), "icon source is the executable");
Assert.equal(
iconIndex,
0,
"icon index 0 selects the executable's default icon"
);
Assert.equal(aumid, TEST_AUMID, "shortcut carries the install AUMID");
Assert.equal(
location,
"Programs",
"shortcut placed in the Programs location"
);
Assert.ok(name.endsWith(".lnk"), "shortcut filename ends with .lnk");
}
);
/**
* This test verifies that when enumerateInstallShortcuts rejects,
* ensureShortcutInPerUserStartMenu() swallows the error and does not attempt
* to create a shortcut.
*/
add_task(
skipOnMsix(),
async function test_ensureShortcutInPerUserStartMenu_enumeration_failure() {
resetMocks();
shellServiceMock.enumerateInstallShortcuts.rejects(
Components.Exception("mock enum failure", Cr.NS_ERROR_FAILURE)
);
await CustomIconManager.ensureShortcutInPerUserStartMenu();
Assert.ok(
shellServiceMock.createShortcut.notCalled,
"createShortcut not attempted when enumeration fails"
);
}
);
/**
* This test verifies that when createShortcut rejects,
* ensureShortcutInPerUserStartMenu() swallows the error and does not throw.
*/
add_task(
skipOnMsix(),
async function test_ensureShortcutInPerUserStartMenu_create_failure() {
resetMocks();
shellServiceMock.createShortcut.rejects(
Components.Exception("mock create failure", Cr.NS_ERROR_FAILURE)
);
await CustomIconManager.ensureShortcutInPerUserStartMenu();
Assert.ok(
shellServiceMock.createShortcut.calledOnce,
"createShortcut was attempted despite the eventual failure"
);
}
);
/**
* This test verifies that ensureShortcutInPerUserStartMenu() is a no-op on
* MSIX (packaged) builds where shortcut creation is unsupported.
*/
add_task(
{ skip_if: () => !ON_MSIX },
async function test_ensureShortcutInPerUserStartMenu_noop_on_msix() {
resetMocks();
await CustomIconManager.ensureShortcutInPerUserStartMenu();
Assert.ok(
shellServiceMock.enumerateInstallShortcuts.notCalled,
"no enumeration on MSIX"
);
Assert.ok(
shellServiceMock.createShortcut.notCalled,
"no shortcut creation on MSIX"
);
}
);
/**
* This test verifies that refreshTaskbarButtons() delegates to
* WinTaskbar.refreshTaskbarButtons().
*/
add_task(function test_refreshTaskbarButtons_calls_wintaskbar() {
winTaskbarMock.refreshTaskbarButtons.reset();
CustomIconManager.refreshTaskbarButtons();
Assert.ok(
winTaskbarMock.refreshTaskbarButtons.calledOnce,
"refreshTaskbarButtons delegates to WinTaskbar"
);
});
/**
* This test verifies that refreshTaskbarButtons() swallows errors thrown by
* WinTaskbar.refreshTaskbarButtons() rather than propagating them.
*/
add_task(function test_refreshTaskbarButtons_swallows_errors() {
winTaskbarMock.refreshTaskbarButtons.reset();
winTaskbarMock.refreshTaskbarButtons.throws(
Components.Exception("mock failure", Cr.NS_ERROR_FAILURE)
);
CustomIconManager.refreshTaskbarButtons();
Assert.ok(
winTaskbarMock.refreshTaskbarButtons.calledOnce,
"refreshTaskbarButtons was attempted"
);
});

View File

@@ -5,6 +5,11 @@ run-if = [
firefox-appdir = "browser"
tags = "os_integration"
["test_customIconManager.js"]
run-if = [
"os == 'win'",
]
["test_desktopEntryStatus.js"]
run-if = [
"os == 'linux'",

View File

@@ -6,6 +6,8 @@ support-files = [
"audio.ogg",
"audioEndedDuringPlaying.webm",
"file_almostSilentAudioTrack.html",
"file_audiblechange_iframe.html",
"file_audiblechange_iframe_inner.html",
"file_autoplay_media.html",
"file_empty.html",
"file_mediaPlayback.html",
@@ -20,6 +22,8 @@ support-files = [
"silentAudioTrack.webm",
]
["browser_audiblechange_iframe.js"]
["browser_destroy_iframe.js"]
https_first_disabled = true
@@ -33,6 +37,10 @@ https_first_disabled = true
["browser_mute2.js"]
["browser_mute_persist_navigation.js"]
["browser_mute_restore_closed_audible_tab.js"]
["browser_mute_webAudio.js"]
["browser_sound_indicator_silent_video.js"]

View File

@@ -0,0 +1,134 @@
// When a tab has audio sources in both the top-level document and an iframe,
// pausing only one of them must not make the tab sound indicator disappear —
// it should stay until all sources are silent.
const PAGE_URL = GetTestWebBasedURL("file_audiblechange_iframe.html");
/**
* Start both top-level and iframe audio, wait for indicator, then pause each
* one in turn verifying the indicator only disappears when both are silent.
*
* @param {tab} tab
* @param {BrowsingContext} innerBC - iframe's browsing context
* @param {string} pauseFirst - "toplevel" or "iframe"
*/
async function testBothSourcesThenPauseOne(tab, innerBC, pauseFirst) {
const browser = tab.linkedBrowser;
info(`play both sources (pause-first: ${pauseFirst})`);
await SpecialPowers.spawn(browser, [], async () => {
await content.document.getElementById("toplevel").play();
});
await SpecialPowers.spawn(innerBC, [], async () => {
await content.document.getElementById("audio").play();
});
await waitForTabSoundIndicatorAppears(tab);
const observer = createSoundIndicatorObserver(tab);
if (pauseFirst === "toplevel") {
info("pause top-level only — iframe still playing");
await SpecialPowers.spawn(browser, [], () => {
content.document.getElementById("toplevel").pause();
});
// Wait for the iframe audio to advance, proving it is still audible.
await SpecialPowers.spawn(innerBC, [], async () => {
await new Promise(r => {
content.document.getElementById("audio").ontimeupdate = r;
});
});
ok(
!observer.hasEverUpdated(),
"indicator stays after top-level pause (iframe still playing)"
);
info("pause iframe — all silent");
await SpecialPowers.spawn(innerBC, [], () => {
content.document.getElementById("audio").pause();
});
} else {
info("pause iframe only — top-level still playing");
await SpecialPowers.spawn(innerBC, [], () => {
content.document.getElementById("audio").pause();
});
// Wait for the top-level audio to advance, proving it is still audible.
await SpecialPowers.spawn(browser, [], async () => {
await new Promise(r => {
content.document.getElementById("toplevel").ontimeupdate = r;
});
});
ok(
!observer.hasEverUpdated(),
"indicator stays after iframe pause (top-level still playing)"
);
info("pause top-level — all silent");
await SpecialPowers.spawn(browser, [], () => {
content.document.getElementById("toplevel").pause();
});
}
await waitForTabSoundIndicatorDisappears(tab);
}
add_task(async function testSoundIndicatorPauseTopLevelFirst() {
info("open tab with top-level audio and same-origin iframe audio");
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
const innerBC = tab.linkedBrowser.browsingContext.children[0];
await testBothSourcesThenPauseOne(tab, innerBC, "toplevel");
info("remove tab");
BrowserTestUtils.removeTab(tab);
});
add_task(async function testSoundIndicatorPauseIframeFirst() {
info("open tab with top-level audio and same-origin iframe audio");
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
const innerBC = tab.linkedBrowser.browsingContext.children[0];
await testBothSourcesThenPauseOne(tab, innerBC, "iframe");
info("remove tab");
BrowserTestUtils.removeTab(tab);
});
add_task(async function testSoundIndicatorOnlyTopLevelPlaying() {
info("open tab and play only the top-level audio (iframe silent)");
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
const browser = tab.linkedBrowser;
await SpecialPowers.spawn(browser, [], async () => {
await content.document.getElementById("toplevel").play();
});
await waitForTabSoundIndicatorAppears(tab);
info("pause top-level — tab should go silent");
await SpecialPowers.spawn(browser, [], () => {
content.document.getElementById("toplevel").pause();
});
await waitForTabSoundIndicatorDisappears(tab);
info("remove tab");
BrowserTestUtils.removeTab(tab);
});
add_task(async function testSoundIndicatorOnlyIframePlaying() {
info("open tab and play only the iframe audio (top-level silent)");
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
const innerBC = tab.linkedBrowser.browsingContext.children[0];
await SpecialPowers.spawn(innerBC, [], async () => {
await content.document.getElementById("audio").play();
});
await waitForTabSoundIndicatorAppears(tab);
info("pause iframe — tab should go silent");
await SpecialPowers.spawn(innerBC, [], () => {
content.document.getElementById("audio").pause();
});
await waitForTabSoundIndicatorDisappears(tab);
info("remove tab");
BrowserTestUtils.removeTab(tab);
});

View File

@@ -4,6 +4,7 @@ const CORS_AUTPLAY_PAGE_URL = GetTestWebBasedURL(
"file_autoplay_media.html",
true
);
const CORS_WEB_AUDIO_PAGE_URL = GetTestWebBasedURL("file_webAudio.html", true);
/**
* When an iframe that has audible media gets destroyed, if there is no other
@@ -32,6 +33,33 @@ add_task(async function testDestroyAudibleIframe() {
}
});
/**
* When an iframe with a Web Audio context (uncontrolled source) is destroyed,
* the sound indicator should disappear if no other audible source remains.
* This exercises the mAudibleUncontrolledSources.Remove() path in
* MediaStatusManager::NotifyBrowsingContextDiscarded().
*/
add_task(async function testDestroyAudibleWebAudioIframe() {
info(`open a tab, create a cross-origin iframe with Web Audio`);
const tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
EMPTY_PAGE_URL
);
await createIframeAndLoadURL(tab, CORS_WEB_AUDIO_PAGE_URL);
info(`sound indicator should appear because of audible Web Audio`);
await waitForTabSoundIndicatorAppears(tab);
info(
`sound indicator should disappear after destroying the Web Audio iframe`
);
await removeIframe(tab);
await waitForTabSoundIndicatorDisappears(tab);
info("remove tab");
BrowserTestUtils.removeTab(tab);
});
function createIframeAndLoadURL(tab, url) {
// eslint-disable-next-line no-shadow
return SpecialPowers.spawn(tab.linkedBrowser, [url], async url => {

View File

@@ -1,24 +1,13 @@
const PAGE = GetTestWebBasedURL("file_mediaPlayback.html");
const FRAME = GetTestWebBasedURL("file_mediaPlaybackFrame.html");
function wait_for_event(browser, event) {
return BrowserTestUtils.waitForEvent(browser, event, false, e => {
is(
e.originalTarget,
browser,
"Event must be dispatched to correct browser."
);
ok(!e.cancelable, "The event should not be cancelable");
return true;
});
}
async function test_on_browser(url, browser) {
info(`run test for ${url}`);
const startPromise = wait_for_event(browser, "DOMAudioPlaybackStarted");
const tab = gBrowser.getTabForBrowser(browser);
const startPromise = waitForTabSoundIndicatorAppears(tab);
BrowserTestUtils.startLoadingURIString(browser, url);
await startPromise;
await wait_for_event(browser, "DOMAudioPlaybackStopped");
await waitForTabSoundIndicatorDisappears(tab);
}
add_task(async function test_page() {

View File

@@ -1,17 +1,6 @@
const PAGE = GetTestWebBasedURL("file_mediaPlayback2.html");
const FRAME = GetTestWebBasedURL("file_mediaPlaybackFrame2.html");
function wait_for_event(browser, event) {
return BrowserTestUtils.waitForEvent(browser, event, false, e => {
is(
e.originalTarget,
browser,
"Event must be dispatched to correct browser."
);
return true;
});
}
function test_audio_in_browser() {
function get_audio_element() {
var doc = content.document;
@@ -36,18 +25,21 @@ function test_audio_in_browser() {
}
async function test_on_browser(url, browser) {
const tab = gBrowser.getTabForBrowser(browser);
BrowserTestUtils.startLoadingURIString(browser, url);
await wait_for_event(browser, "DOMAudioPlaybackStarted");
await waitForTabSoundIndicatorAppears(tab);
var result = await SpecialPowers.spawn(browser, [], test_audio_in_browser);
is(result.computedVolume, 1, "Audio volume is 1");
is(result.computedMuted, false, "Audio is not muted");
ok(!browser.audioMuted, "Audio should not be muted by default");
browser.mute();
// Use the tab-level API (which internally calls mediaController.mute()) to
// test the full browser-mute path as the user would trigger it.
tab.toggleMuteAudio();
ok(browser.audioMuted, "Audio should be muted now");
await wait_for_event(browser, "DOMAudioPlaybackStopped");
await waitForTabSoundIndicatorDisappears(tab);
result = await SpecialPowers.spawn(browser, [], test_audio_in_browser);
is(result.computedVolume, 0, "Audio volume is 0 when muted");
@@ -55,8 +47,9 @@ async function test_on_browser(url, browser) {
}
async function test_visibility(url, browser) {
const tab = gBrowser.getTabForBrowser(browser);
BrowserTestUtils.startLoadingURIString(browser, url);
await wait_for_event(browser, "DOMAudioPlaybackStarted");
await waitForTabSoundIndicatorAppears(tab);
var result = await SpecialPowers.spawn(browser, [], test_audio_in_browser);
is(result.computedVolume, 1, "Audio volume is 1");
@@ -71,10 +64,12 @@ async function test_visibility(url, browser) {
);
ok(!browser.audioMuted, "Audio should not be muted by default");
browser.mute();
// Use the tab-level API (which internally calls mediaController.mute()) to
// test the full browser-mute path as the user would trigger it.
tab.toggleMuteAudio();
ok(browser.audioMuted, "Audio should be muted now");
await wait_for_event(browser, "DOMAudioPlaybackStopped");
await waitForTabSoundIndicatorDisappears(tab);
result = await SpecialPowers.spawn(browser, [], test_audio_in_browser);
is(result.computedVolume, 0, "Audio volume is 0 when muted");

View File

@@ -2,9 +2,9 @@ const PAGE = "data:text/html,page";
function test_on_browser(browser) {
ok(!browser.audioMuted, "Audio should not be muted by default");
browser.mute();
browser.browsingContext.mediaController.mute();
ok(browser.audioMuted, "Audio should be muted now");
browser.unmute();
browser.browsingContext.mediaController.unmute();
ok(!browser.audioMuted, "Audio should be unmuted now");
}

View File

@@ -2,7 +2,7 @@ const PAGE = "data:text/html,page";
async function test_on_browser(browser) {
ok(!browser.audioMuted, "Audio should not be muted by default");
browser.mute();
browser.browsingContext.mediaController.mute();
ok(browser.audioMuted, "Audio should be muted now");
await BrowserTestUtils.withNewTab(
@@ -13,7 +13,7 @@ async function test_on_browser(browser) {
test_on_browser2
);
browser.unmute();
browser.browsingContext.mediaController.unmute();
ok(!browser.audioMuted, "Audio should be unmuted now");
}

View File

@@ -0,0 +1,86 @@
// Verify that tab mute state persists across navigation: a tab muted before
// navigation keeps audio muted on the new page, and unmuting restores audibility.
"use strict";
const PAGE = GetTestWebBasedURL("file_mediaPlayback2.html");
// Mute a tab while it is on about:blank, then navigate to a page with audio.
// Verify that the audio in the new page is muted immediately on playback, and
// that unmuting the tab makes the audio audible in content.
add_task(async function test_mute_persists_and_unmute_restores() {
await SpecialPowers.pushPrefEnv({
set: [["media.useAudioChannelService.testing", true]],
});
const tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"about:blank"
);
const browser = tab.linkedBrowser;
ok(!browser.audioMuted, "Tab should not be muted initially");
tab.toggleMuteAudio();
ok(browser.audioMuted, "Tab should now be muted");
ok(tab.hasAttribute("muted"), "Tab element should have muted attribute");
BrowserTestUtils.startLoadingURIString(browser, PAGE);
await BrowserTestUtils.browserLoaded(browser, false, PAGE);
await waitForAudioPlaying(browser);
let computedMuted = await getAudioComputedMuted(browser);
is(
computedMuted,
true,
"Audio should be muted in the content page after navigation"
);
ok(
browser.audioMuted,
"browser.audioMuted should remain true after navigation"
);
ok(
!tab.hasAttribute("soundplaying"),
"Tab should not show the sound indicator while muted"
);
tab.toggleMuteAudio();
ok(!browser.audioMuted, "Tab should be unmuted now");
await TestUtils.waitForCondition(async () => {
return !(await getAudioComputedMuted(browser));
}, "Audio should become unmuted in content after tab unmute");
computedMuted = await getAudioComputedMuted(browser);
is(computedMuted, false, "Audio should no longer be muted after unmute");
BrowserTestUtils.removeTab(tab);
});
// Following are helper functions.
async function getAudioComputedMuted(browser) {
return SpecialPowers.spawn(browser, [], () => {
const audio = content.document.getElementById("v");
if (!audio) {
ok(false, "audio element not found");
return null;
}
return audio.computedMuted;
});
}
async function waitForAudioPlaying(browser) {
await SpecialPowers.spawn(browser, [], async () => {
const audio = content.document.getElementById("v");
if (!audio) {
ok(false, "audio element not found");
return;
}
if (!audio.paused) {
return;
}
await new Promise(resolve =>
audio.addEventListener("playing", resolve, { once: true })
);
});
}

View File

@@ -0,0 +1,93 @@
// Verify that closing a tab preserves the mute state it gets restored with: a
// tab closed while it was audible and unmuted comes back unmuted and audible,
// and a tab the user muted comes back muted and silent.
"use strict";
const PAGE = GetTestWebBasedURL("file_mediaPlayback2.html");
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [["media.useAudioChannelService.testing", true]],
});
});
add_task(async function test_closing_audible_tab_restores_unmuted() {
const tab = await openAudibleTab();
ok(!tab.muted, "Tab is not muted before it gets closed");
ok(tab.soundPlaying, "Tab is still audible when it gets closed");
const restored = await closeAndRestoreTab(tab);
ok(!restored.muted, "Restored tab should not be muted");
ok(
!restored.linkedBrowser.audioMuted,
"Restored tab's audio should not be muted"
);
await waitForTabSoundIndicatorAppears(restored);
BrowserTestUtils.removeTab(restored);
});
add_task(async function test_closing_muted_tab_restores_muted() {
const tab = await openAudibleTab();
tab.toggleMuteAudio();
ok(tab.muted, "Tab is muted before it gets closed");
await waitForTabSoundIndicatorDisappears(tab);
const restored = await closeAndRestoreTab(tab);
ok(restored.muted, "Restored tab should still be muted");
ok(restored.linkedBrowser.audioMuted, "Restored tab's audio should be muted");
await BrowserTestUtils.browserLoaded(restored.linkedBrowser, false, PAGE);
is(
await getComputedMutedOncePlaying(restored.linkedBrowser),
true,
"Restored tab stays silent once its media resumes"
);
BrowserTestUtils.removeTab(restored);
});
// Following are helper functions.
async function openAudibleTab() {
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
await waitForTabSoundIndicatorAppears(tab);
return tab;
}
async function closeAndRestoreTab(tab) {
const closedTabCount = SessionStore.getClosedTabCountForWindow(window);
const mutedWhenClosed = tab.muted;
const flushed = BrowserTestUtils.waitForSessionStoreUpdate(tab);
await BrowserTestUtils.removeTab(tab);
await flushed;
is(
SessionStore.getClosedTabCountForWindow(window),
closedTabCount + 1,
"SessionStore recorded the closed tab"
);
is(
!!SessionStore.getClosedTabData(window)[0].state.muted,
mutedWhenClosed,
"Closed tab data records the mute state the tab was closed with"
);
return SessionStore.undoCloseTab(window, 0);
}
async function getComputedMutedOncePlaying(browser) {
return SpecialPowers.spawn(browser, [], async () => {
const audio = await ContentTaskUtils.waitForCondition(
() => content.document.getElementById("v"),
"wait for the restored page to create its audio element"
);
if (audio.paused) {
await new Promise(resolve =>
audio.addEventListener("playing", resolve, { once: true })
);
}
return audio.computedMuted;
});
}

View File

@@ -0,0 +1,3 @@
<!DOCTYPE html>
<audio id="toplevel" src="audio.ogg" loop></audio>
<iframe id="inner" src="file_audiblechange_iframe_inner.html"></iframe>

View File

@@ -0,0 +1,2 @@
<!DOCTYPE html>
<audio id="audio" src="audio.ogg" loop></audio>

View File

@@ -5,7 +5,7 @@
"binaryName": "zen",
"version": {
"product": "firefox",
"version": "153.0.4",
"version": "154.0",
"candidate": "154.0",
"candidateBuild": 1
},
@@ -20,7 +20,7 @@
"brandShortName": "Zen",
"brandFullName": "Zen Browser",
"release": {
"displayVersion": "1.21.14b",
"displayVersion": "1.21.15b",
"github": {
"repo": "zen-browser/desktop"
},