mirror of
https://github.com/zen-browser/desktop.git
synced 2026-08-19 05:01:33 +00:00
gh-14959: Sync upstream Firefox to version 154.0 (gh-15012)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
4be2ba3142dc23601c46ab7e8268923f1939442d
|
||||
1fb0703fa9723fd9c400ad60d18e64ee4f9bf125
|
||||
@@ -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;
|
||||
|
||||
@@ -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'",
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
901
src/zen/tests/mochitests/shell/unit/test_customIconManager.js
Normal file
901
src/zen/tests/mochitests/shell/unit/test_customIconManager.js
Normal 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"
|
||||
);
|
||||
});
|
||||
@@ -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'",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<audio id="toplevel" src="audio.ogg" loop></audio>
|
||||
<iframe id="inner" src="file_audiblechange_iframe_inner.html"></iframe>
|
||||
@@ -0,0 +1,2 @@
|
||||
<!DOCTYPE html>
|
||||
<audio id="audio" src="audio.ogg" loop></audio>
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user