).
-function fileInTempDir() {
- let contentTempKey = "TmpD";
-
- // get the content temp dir, make sure it exists
- let ctmp = Services.dirsvc.get(contentTempKey, Ci.nsIFile);
- Assert.ok(ctmp.exists(), "Temp dir exists");
- Assert.ok(ctmp.isDirectory(), "Temp dir is a directory");
-
- // build a file object for a new file in content temp
- let tempFile = ctmp.clone();
- tempFile.appendRelativePath(uuid());
- Assert.ok(!tempFile.exists(), tempFile.path + " does not exist");
- return tempFile;
-}
-
-function GetProfileDir() {
- // get profile directory
- let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
- return profileDir;
-}
-
-function GetHomeDir() {
- // get home directory
- let homeDir = Services.dirsvc.get("Home", Ci.nsIFile);
- return homeDir;
-}
-
-function GetHomeSubdir(subdir) {
- return GetSubdir(GetHomeDir(), subdir);
-}
-
-function GetHomeSubdirFile(subdir) {
- return GetSubdirFile(GetHomeSubdir(subdir));
-}
-
-function GetSubdir(dir, subdir) {
- let newSubdir = dir.clone();
- newSubdir.appendRelativePath(subdir);
- return newSubdir;
-}
-
-function GetSubdirFile(dir) {
- let newFile = dir.clone();
- newFile.appendRelativePath(uuid());
- return newFile;
-}
-
-function GetPerUserExtensionDir() {
- return Services.dirsvc.get("XREUSysExt", Ci.nsIFile);
-}
-
-// Returns a file object for the file or directory named |name| in the
-// profile directory.
-function GetProfileEntry(name) {
- let entry = GetProfileDir();
- entry.append(name);
- return entry;
-}
-
-function GetDir(path) {
- info(`GetDir(${path})`);
- let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
- dir.initWithPath(path);
- Assert.ok(dir.isDirectory(), `${path} is a directory`);
- return dir;
-}
-
-function GetDirFromEnvVariable(varName) {
- return GetDir(Services.env.get(varName));
-}
-
-function GetFile(path) {
- let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
- file.initWithPath(path);
- return file;
-}
-
-function GetBrowserType(type) {
- let browserType = undefined;
-
- if (!GetBrowserType[type]) {
- if (type === "web") {
- GetBrowserType[type] = gBrowser.selectedBrowser;
- } else {
- // open a tab in a `type` content process
- gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
- preferredRemoteType: type,
- allowInheritPrincipal: true,
- });
- // get the browser for the `type` process tab
- GetBrowserType[type] = gBrowser.getBrowserForTab(gBrowser.selectedTab);
- }
- }
-
- browserType = GetBrowserType[type];
- Assert.strictEqual(
- browserType.remoteType,
- type,
- `GetBrowserType(${type}) returns a ${type} process`
- );
- return browserType;
-}
-
-function GetWebBrowser() {
- return GetBrowserType("web");
-}
-
-function isFileContentProcessEnabled() {
- // Ensure that the file content process is enabled.
- let fileContentProcessEnabled = Services.prefs.getBoolPref(
- "browser.tabs.remote.separateFileUriProcess"
- );
- ok(fileContentProcessEnabled, "separate file content process is enabled");
- return fileContentProcessEnabled;
-}
-
-function GetFileBrowser() {
- if (!isFileContentProcessEnabled()) {
- return undefined;
- }
- return GetBrowserType("file");
-}
-
-function GetSandboxLevel() {
- // Current level
- return Services.prefs.getIntPref("security.sandbox.content.level");
-}
-
-async function runTestsList(tests) {
- let level = GetSandboxLevel();
-
- // remove tests not enabled by the current sandbox level
- tests = tests.filter(test => test.minLevel <= level);
-
- for (let test of tests) {
- let okString = test.ok ? "allowed" : "blocked";
- let processType = test.browser.remoteType;
-
- // ensure the file/dir exists before we ask a content process to stat
- // it so we know a failure is not due to a nonexistent file/dir
- if (test.func === statPath) {
- ok(test.file.exists(), `${test.file.path} exists`);
- }
-
- let result = await SpecialPowers.spawn(
- test.browser,
- [test.file.path],
- test.func
- );
-
- Assert.equal(
- result.ok,
- test.ok,
- `reading ${test.desc} from a ${processType} process ` +
- `is ${okString} (${test.file.path})`
- );
-
- // if the directory is not expected to be readable,
- // ensure the listing has zero entries
- if (test.func === readDir && !test.ok) {
- Assert.equal(
- result.numEntries,
- 0,
- `directory list is empty (${test.file.path})`
- );
- }
-
- if (test.cleanup != undefined) {
- await test.cleanup(test.file.path);
- }
- }
-}
diff --git a/src/zen/tests/mochitests/sandbox/browser_profiler.toml b/src/zen/tests/mochitests/sandbox/browser_profiler.toml
deleted file mode 100644
index de46910e3..000000000
--- a/src/zen/tests/mochitests/sandbox/browser_profiler.toml
+++ /dev/null
@@ -1,21 +0,0 @@
-[DEFAULT]
-skip-if = [
- "ccov",
- "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517 for sandbox, bug 1885381 for profiler
- "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517 for sandbox, bug 1885381 for profiler
-]
-tags = "contentsandbox"
-
-# This is here to make sure we will not have prelaunched processes, which will
-# mess with sandbox profiling interaction: we will miss launch-related markers
-# and this makes the test intermittently fail on TV jobs
-prefs = [
- "dom.ipc.processPrelaunch.fission.number=0"
-]
-
-environment = "MOZ_SANDBOX_LOGGING_FOR_TESTS=1"
-
-["browser_sandbox_profiler.js"]
-run-if = [
- "os == 'linux'",
-]
diff --git a/src/zen/tests/mochitests/sandbox/browser_sandbox_profiler.js b/src/zen/tests/mochitests/sandbox/browser_sandbox_profiler.js
deleted file mode 100644
index c20310b51..000000000
--- a/src/zen/tests/mochitests/sandbox/browser_sandbox_profiler.js
+++ /dev/null
@@ -1,153 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-const { ProfilerTestUtils } = ChromeUtils.importESModule(
- "resource://testing-common/ProfilerTestUtils.sys.mjs"
-);
-
-async function addTab() {
- const tab = BrowserTestUtils.addTab(gBrowser, "https://example.com/browser", {
- forceNewProcess: true,
- });
- const browser = gBrowser.getBrowserForTab(tab);
- await BrowserTestUtils.browserLoaded(browser);
- return tab;
-}
-
-const sandboxSettingsEnabled = {
- entries: 8 * 1024 * 1024, // 8M entries = 64MB
- interval: 1, // ms
- features: ["stackwalk", "sandbox"],
- threads: ["SandboxProfilerEmitter"],
-};
-
-const sandboxSettingsDisabled = {
- entries: 8 * 1024 * 1024, // 8M entries = 64MB
- interval: 1, // ms
- features: ["stackwalk"],
- threads: ["SandboxProfilerEmitter"],
-};
-
-const kNewProcesses = 2;
-
-async function waitForMaybeSandboxProfilerData(
- threadName,
- name1,
- withStacks,
- enabled
-) {
- let tabs = [];
- for (let i = 0; i < kNewProcesses; ++i) {
- tabs.push(await addTab());
- }
-
- let profile;
- let intercepted = undefined;
- try {
- await TestUtils.waitForCondition(
- async () => {
- profile = await Services.profiler.getProfileDataAsync();
- intercepted = profile.processes
- .flatMap(ps => {
- let sandboxThreads = ps.threads.filter(
- th => th.name === threadName
- );
- return sandboxThreads.flatMap(th => {
- let markersData = th.markers.data;
- return markersData.flatMap(d => {
- let [, , , , , o] = d;
- return o;
- });
- });
- })
- .filter(x => "name1" in x && name1.includes(x.name1) >= 0);
- return !!intercepted.length;
- },
- `Wait for some samples from ${threadName}`,
- /* interval*/ 100,
- /* maxTries */ 25
- );
- Assert.greater(
- intercepted.length,
- 0,
- `Should have collected some data from ${threadName}`
- );
- } catch (ex) {
- if (!enabled && ex.includes(`Wait for some samples from ${threadName}`)) {
- Assert.equal(
- intercepted.length,
- 0,
- `Should have NOT collected data from ${threadName}`
- );
- } else {
- throw ex;
- }
- }
-
- if (withStacks) {
- let stacks = profile.processes.flatMap(ps => {
- let sandboxThreads = ps.threads.filter(th => th.name === threadName);
- return sandboxThreads.flatMap(th => {
- let stackTableData = th.stackTable.data;
- return stackTableData.flatMap(d => {
- return [d];
- });
- });
- });
- if (enabled) {
- Assert.greater(stacks.length, 0, "Should have some stack as well");
- } else {
- Assert.equal(stacks.length, 0, "Should have NO stack as well");
- }
- }
-
- for (let tab of tabs) {
- await BrowserTestUtils.removeTab(tab);
- }
-}
-
-add_task(async () => {
- await ProfilerTestUtils.startProfiler(sandboxSettingsEnabled);
- await waitForMaybeSandboxProfilerData(
- "SandboxProfilerEmitterSyscalls",
- ["id", "init"],
- /* withStacks */ true,
- /* enabled */ true
- );
- await Services.profiler.StopProfiler();
-});
-
-add_task(async () => {
- await ProfilerTestUtils.startProfiler(sandboxSettingsEnabled);
- await waitForMaybeSandboxProfilerData(
- "SandboxProfilerEmitterLogs",
- ["log"],
- /* withStacks */ false,
- /* enabled */ true
- );
- await Services.profiler.StopProfiler();
-});
-
-add_task(async () => {
- await ProfilerTestUtils.startProfiler(sandboxSettingsDisabled);
- await waitForMaybeSandboxProfilerData(
- "SandboxProfilerEmitterSyscalls",
- ["id", "init"],
- /* withStacks */ true,
- /* enabled */ false
- );
- await Services.profiler.StopProfiler();
-});
-
-add_task(async () => {
- await ProfilerTestUtils.startProfiler(sandboxSettingsEnabled);
- await waitForMaybeSandboxProfilerData(
- "SandboxProfilerEmitterLogs",
- ["log"],
- /* withStacks */ false,
- /* enabled */ false
- );
- await Services.profiler.StopProfiler();
-});
diff --git a/src/zen/tests/mochitests/sandbox/browser_sandbox_test.js b/src/zen/tests/mochitests/sandbox/browser_sandbox_test.js
deleted file mode 100644
index 58c37989c..000000000
--- a/src/zen/tests/mochitests/sandbox/browser_sandbox_test.js
+++ /dev/null
@@ -1,78 +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 https://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-function test() {
- waitForExplicitFinish();
-
- // Types of processes to test, taken from GeckoProcessTypes.h
- // GPU process might not run depending on the platform, so we need it to be
- // the last one of the list to allow the remainingTests logic below to work
- // as expected.
- //
- // For UtilityProcess, allow constructing a string made of the process type
- // and the sandbox variant we want to test, e.g.,
- // utility:0 for GENERIC_UTILITY
- // utility:1 for AppleMedia/WMF on macOS/Windows
- var processTypes = ["tab", "socket", "rdd", "gmplugin", "utility:0", "gpu"];
-
- const platform = SpecialPowers.Services.appinfo.OS;
- if (platform === "WINNT" || platform === "Darwin") {
- processTypes.push("utility:1");
- }
-
- // A callback called after each test-result.
- let sandboxTestResult = (subject, topic, data) => {
- let { testid, passed, message } = JSON.parse(data);
- ok(
- passed,
- "Test " + testid + (passed ? " passed: " : " failed: ") + message
- );
- };
- Services.obs.addObserver(sandboxTestResult, "sandbox-test-result");
-
- var remainingTests = processTypes.length;
-
- // A callback that is notified when a child process is done running tests.
- let sandboxTestDone = () => {
- remainingTests = remainingTests - 1;
- if (remainingTests == 0) {
- // Clean up test file
- if (homeTestFile.exists()) {
- ok(homeTestFile.isFile(), "homeTestFile should be a file");
- if (homeTestFile.isFile()) {
- homeTestFile.remove(false);
- }
- }
-
- Services.obs.removeObserver(sandboxTestResult, "sandbox-test-result");
- Services.obs.removeObserver(sandboxTestDone, "sandbox-test-done");
-
- // Notify SandboxTest component that it should terminate the connection
- // with the child processes.
- comp.finishTests();
- // Notify mochitest that all process tests are complete.
- finish();
- }
- };
- Services.obs.addObserver(sandboxTestDone, "sandbox-test-done");
-
- var comp = Cc["@mozilla.org/sandbox/sandbox-test;1"].getService(
- Ci.mozISandboxTest
- );
-
- let homeTestFile;
- try {
- homeTestFile = Services.dirsvc.get("Home", Ci.nsIFile);
- homeTestFile.append(".mozilla_gpu_sandbox_read_test");
- if (!homeTestFile.exists()) {
- homeTestFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
- }
- } catch (e) {
- ok(false, "Failed to create home test file: " + e);
- }
-
- comp.startTests(processTypes);
-}
diff --git a/src/zen/tests/mochitests/sandbox/browser_snap.toml b/src/zen/tests/mochitests/sandbox/browser_snap.toml
deleted file mode 100644
index 19a4a1004..000000000
--- a/src/zen/tests/mochitests/sandbox/browser_snap.toml
+++ /dev/null
@@ -1,20 +0,0 @@
-# Any copyright is dedicated to the Public Domain.
-# http://creativecommons.org/publicdomain/zero/1.0/
-[DEFAULT]
-skip-if = [
- "ccov",
- "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517
- "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517
-]
-tags = "contentsandbox"
-support-files = [
- "browser_content_sandbox_utils.js",
- "browser_content_sandbox_fs_tests.js",
-]
-test-directories = "/tmp/.snap_firefox_current_real/"
-environment = "SNAP=/tmp/.snap_firefox_current_real/"
-
-["browser_content_sandbox_fs_snap.js"]
-run-if = [
- "os == 'linux'",
-]
diff --git a/src/zen/tests/mochitests/sandbox/browser_xdg_default.toml b/src/zen/tests/mochitests/sandbox/browser_xdg_default.toml
deleted file mode 100644
index f03f4c06f..000000000
--- a/src/zen/tests/mochitests/sandbox/browser_xdg_default.toml
+++ /dev/null
@@ -1,26 +0,0 @@
-# Any copyright is dedicated to the Public Domain.
-# http://creativecommons.org/publicdomain/zero/1.0/
-[DEFAULT]
-skip-if = [
- "ccov",
- "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517
- "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517
-]
-tags = "contentsandbox"
-support-files = [
- "browser_content_sandbox_utils.js",
- "browser_content_sandbox_fs_tests.js",
-]
-test-directories = [
- "/tmp/.xdg_default_test",
- "/tmp/.xdg_default_test/.config/mozilla/firefox/xdg_default_profile",
-]
-environment = [
- "HOME=/tmp/.xdg_default_test",
-]
-profile-path = "/tmp/.xdg_default_test/.config/mozilla/firefox/xdg_default_profile"
-
-["browser_content_sandbox_fs_xdg_default.js"]
-run-if = [
- "os == 'linux'"
-]
diff --git a/src/zen/tests/mochitests/sandbox/browser_xdg_mozLegacyHome.toml b/src/zen/tests/mochitests/sandbox/browser_xdg_mozLegacyHome.toml
deleted file mode 100644
index 7f7b680a8..000000000
--- a/src/zen/tests/mochitests/sandbox/browser_xdg_mozLegacyHome.toml
+++ /dev/null
@@ -1,29 +0,0 @@
-# Any copyright is dedicated to the Public Domain.
-# http://creativecommons.org/publicdomain/zero/1.0/
-[DEFAULT]
-skip-if = [
- "ccov",
- "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517
- "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517
-]
-tags = "contentsandbox"
-support-files = [
- "browser_content_sandbox_utils.js",
- "browser_content_sandbox_fs_tests.js",
-]
-test-directories = [
- "/tmp/.xdg_mozLegacyHome_test/.config",
- "/tmp/.xdg_config_home_test",
- "/tmp/.xdg_mozLegacyHome_test/.mozilla/firefox/xdg_mozLegacyHome_profile",
-]
-environment = [
- "XDG_CONFIG_HOME=/tmp/.xdg_config_home_test",
- "HOME=/tmp/.xdg_mozLegacyHome_test",
- "MOZ_LEGACY_HOME=1",
-]
-profile-path = "/tmp/.xdg_mozLegacyHome_test/.mozilla/firefox/xdg_mozLegacyHome_profile"
-
-["browser_content_sandbox_fs_xdg_mozLegacyHome.js"]
-run-if = [
- "os == 'linux'"
-]
diff --git a/src/zen/tests/mochitests/sandbox/browser_xdg_xdgConfigHome.toml b/src/zen/tests/mochitests/sandbox/browser_xdg_xdgConfigHome.toml
deleted file mode 100644
index b32d0c25d..000000000
--- a/src/zen/tests/mochitests/sandbox/browser_xdg_xdgConfigHome.toml
+++ /dev/null
@@ -1,27 +0,0 @@
-# Any copyright is dedicated to the Public Domain.
-# http://creativecommons.org/publicdomain/zero/1.0/
-[DEFAULT]
-skip-if = [
- "ccov",
- "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && asan", # bug 1784517
- "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && tsan", # bug 1784517
-]
-tags = "contentsandbox"
-support-files = [
- "browser_content_sandbox_utils.js",
- "browser_content_sandbox_fs_tests.js",
-]
-test-directories = [
- "/tmp/.xdg_config_home_test",
- "/tmp/.xdg_config_home_test/mozilla/firefox/xdg_config_home_profile",
-]
-environment = [
- "XDG_CONFIG_HOME=/tmp/.xdg_config_home_test",
- "MOZ_LEGACY_HOME=0",
-]
-profile-path = "/tmp/.xdg_config_home_test/mozilla/firefox/xdg_config_home_profile"
-
-["browser_content_sandbox_fs_xdg_xdgConfigHome.js"]
-run-if = [
- "os == 'linux'",
-]
diff --git a/src/zen/tests/mochitests/sandbox/bug1393259.html b/src/zen/tests/mochitests/sandbox/bug1393259.html
deleted file mode 100644
index b1e3cca99..000000000
--- a/src/zen/tests/mochitests/sandbox/bug1393259.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-abcdefghijklmnopqrstuvwxyz
-abcdefghijklmnopqrstuvwxyz
-abcdefghijklmnopqrstuvwxyz
-
-
-
-
diff --git a/src/zen/tests/mochitests/sandbox/mac_register_font.py b/src/zen/tests/mochitests/sandbox/mac_register_font.py
deleted file mode 100755
index 549becf56..000000000
--- a/src/zen/tests/mochitests/sandbox/mac_register_font.py
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/usr/bin/python
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"""
-mac_register_font.py
-
-Mac-specific utility command to register a font file with the OS.
-"""
-
-import argparse
-import sys
-
-import Cocoa
-import CoreText
-
-
-def main():
- parser = argparse.ArgumentParser()
- parser.add_argument(
- "-v",
- "--verbose",
- action="store_true",
- help="print verbose registration failures",
- default=False,
- )
- parser.add_argument(
- "file", nargs="*", help="font file to register or unregister", default=[]
- )
- parser.add_argument(
- "-u",
- "--unregister",
- action="store_true",
- help="unregister the provided fonts",
- default=False,
- )
- parser.add_argument(
- "-p",
- "--persist-user",
- action="store_true",
- help="permanently register the font",
- default=False,
- )
-
- args = parser.parse_args()
-
- if args.persist_user:
- scope = CoreText.kCTFontManagerScopeUser
- scopeDesc = "user"
- else:
- scope = CoreText.kCTFontManagerScopeSession
- scopeDesc = "session"
-
- failureCount = 0
- for fontPath in args.file:
- fontURL = Cocoa.NSURL.fileURLWithPath_(fontPath)
- (result, error) = register_or_unregister_font(fontURL, args.unregister, scope)
- if result:
- print(
- "%sregistered font %s with %s scope"
- % (("un" if args.unregister else ""), fontPath, scopeDesc)
- )
- else:
- print(
- "Failed to %sregister font %s with %s scope"
- % (("un" if args.unregister else ""), fontPath, scopeDesc)
- )
- if args.verbose:
- print(error)
- failureCount += 1
-
- sys.exit(failureCount)
-
-
-def register_or_unregister_font(fontURL, unregister, scope):
- return (
- CoreText.CTFontManagerUnregisterFontsForURL(fontURL, scope, None)
- if unregister
- else CoreText.CTFontManagerRegisterFontsForURL(fontURL, scope, None)
- )
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/zen/tests/mochitests/shell/browser.toml b/src/zen/tests/mochitests/shell/browser.toml
index 8f6af8802..79e0e5b60 100644
--- a/src/zen/tests/mochitests/shell/browser.toml
+++ b/src/zen/tests/mochitests/shell/browser.toml
@@ -2,7 +2,10 @@
["browser_1119088.js"]
disabled="Disabled by import_external_tests.py"
-support-files = ["mac_desktop_image.py"]
+support-files = [
+ "large.png",
+ "mac_desktop_image.py"
+]
run-if = [
"os == 'mac'",
]
@@ -12,6 +15,7 @@ skip-if = [
]
["browser_420786.js"]
+support-files = ["large.png"]
run-if = [
"os == 'linux' && os_version == '22.04' && arch == 'x86_64' && display == 'wayland'",
"os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11'",
@@ -116,4 +120,5 @@ tags = "os_integration"
["browser_setDesktopBackgroundPreview.js"]
disabled="Disabled by import_external_tests.py"
+support-files = ["large.png"]
tags = "os_integration"
diff --git a/src/zen/tests/mochitests/shell/browser_1119088.js b/src/zen/tests/mochitests/shell/browser_1119088.js
index 6702769a3..2f466c92d 100644
--- a/src/zen/tests/mochitests/shell/browser_1119088.js
+++ b/src/zen/tests/mochitests/shell/browser_1119088.js
@@ -92,20 +92,19 @@ add_setup(async function () {
/**
* Tests "Set As Desktop Background" platform implementation on macOS.
*
- * Sets the desktop background image to the browser logo from the about:logo
- * page and verifies it was set successfully. Setting the desktop background
- * (which uses the nsIShellService::setDesktopBackground() interface method)
- * downloads the image to ~/Pictures using a unique file name and sets the
- * desktop background to the downloaded file leaving the download in place.
- * After setDesktopBackground() is called, the test uses a python script to
- * validate that the current desktop background is in fact set to the
- * downloaded logo.
+ * Sets the desktop background image to the large.png image and verifies it was
+ * set successfully. Setting the desktop background (which uses the
+ * nsIShellService::setDesktopBackground() interface method) downloads the
+ * image to ~/Pictures using a unique file name and sets the desktop background
+ * to the downloaded file leaving the download in place. After
+ * setDesktopBackground() is called, the test uses a python script to validate
+ * that the current desktop background is in fact set to the downloaded image.
*/
add_task(async function () {
await BrowserTestUtils.withNewTab(
{
gBrowser,
- url: "about:logo",
+ url: getRootDirectory(gTestPath) + "large.png",
},
async () => {
let dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(
@@ -140,7 +139,7 @@ add_task(async function () {
// For simplicity, we're going to reach in and access the image on the
// page directly, which means the page shouldn't be running in a remote
- // browser. Thankfully, about:logo runs in the parent process for now.
+ // browser. Thankfully, chrome:// runs in the parent process for now.
Assert.ok(
!gBrowser.selectedBrowser.isRemoteBrowser,
"image can be accessed synchronously from the parent process"
diff --git a/src/zen/tests/mochitests/shell/browser_420786.js b/src/zen/tests/mochitests/shell/browser_420786.js
index b9becb49c..1f7beb45c 100644
--- a/src/zen/tests/mochitests/shell/browser_420786.js
+++ b/src/zen/tests/mochitests/shell/browser_420786.js
@@ -12,7 +12,7 @@ add_task(async function () {
await BrowserTestUtils.withNewTab(
{
gBrowser,
- url: "about:logo",
+ url: getRootDirectory(gTestPath) + "large.png",
},
() => {
var brandName = Services.strings
@@ -46,7 +46,7 @@ add_task(async function () {
// For simplicity, we're going to reach in and access the image on the
// page directly, which means the page shouldn't be running in a remote
- // browser. Thankfully, about:logo runs in the parent process for now.
+ // browser. Thankfully, chrome:// runs in the parent process for now.
Assert.ok(
!gBrowser.selectedBrowser.isRemoteBrowser,
"image can be accessed synchronously from the parent process"
diff --git a/src/zen/tests/mochitests/shell/browser_setDefaultPDFHandler.js b/src/zen/tests/mochitests/shell/browser_setDefaultPDFHandler.js
index 332a7c6f7..04cc1f069 100644
--- a/src/zen/tests/mochitests/shell/browser_setDefaultPDFHandler.js
+++ b/src/zen/tests/mochitests/shell/browser_setDefaultPDFHandler.js
@@ -323,14 +323,39 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() {
}
});
+// Wait for the deferred set_default_pdf_handler_attempt event to be recorded,
+// then return the single event that was emitted by the most recent call.
+async function awaitAttemptEvent() {
+ await TestUtils.waitForCondition(() => {
+ const events = Glean.browser.setDefaultPdfHandlerAttempt.testGetValue();
+ return events && events.length;
+ }, "Recorded set_default_pdf_handler_attempt event");
+ const events = Glean.browser.setDefaultPdfHandlerAttempt.testGetValue();
+ Assert.equal(events.length, 1, "Recorded exactly one attempt event");
+ return events[0];
+}
+
add_task(async function test_setAsDefaultPDFHandler_fallback() {
const sandbox = sinon.createSandbox();
+ // Enable the IOpenWithLauncher branch explicitly so the test does not
+ // depend on the build-channel default of
+ // browser.shell.setDefaultPDFHandler.useOpenWith, and use a 0ms wait so
+ // the deferred attempt event fires promptly.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shell.setDefaultPDFHandler.useOpenWith", true],
+ ["browser.shell.setDefaultPDFHandler.attemptWaitTimeMs", 0],
+ ],
+ });
try {
const userChoiceStub = sandbox
.stub(ShellService, "setAsDefaultPDFHandlerUserChoice")
.rejects(new Error("mock userChoice failure"));
sandbox.stub(ShellService, "_isWindows11").returns(true);
+ const isDefaultHandlerForStub = sandbox
+ .stub(ShellService, "isDefaultHandlerFor")
+ .returns(true);
info(
"When userChoice fails and open-with picker succeeds, should not fall back to settings dialog"
@@ -352,27 +377,42 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() {
1,
"Recorded user-choice failure"
);
+
+ let event = await awaitAttemptEvent();
+ Assert.equal(event.extra.method, "open_with", "Event method is open_with");
+ Assert.equal(event.extra.success, "true", "Event success is true");
Assert.equal(
- Glean.browser.setDefaultPdfHandlerUserChoiceResult.Success.testGetValue(),
- undefined,
- "Did not record user-choice success"
+ event.extra.result_is_default,
+ "true",
+ "Event result_is_default reflects isDefaultHandlerFor"
);
- Assert.equal(
- Glean.browser.setDefaultPdfHandlerOpenWithResult.Success.testGetValue(),
- 1,
- "Recorded open-with success"
- );
- Assert.equal(
- Glean.browser.setDefaultPdfHandlerOpenWithResult.Failure.testGetValue(),
- undefined,
- "Did not record open-with failure"
- );
- Assert.equal(
- Glean.browser.setDefaultPdfHandlerModernSettingsResult.Success.testGetValue(),
- undefined,
- "Did not record modern settings result"
+ Assert.ok(
+ isDefaultHandlerForStub.calledWith(".pdf"),
+ "Sampled isDefaultHandlerFor after the delay"
);
userChoiceStub.resetHistory();
+ isDefaultHandlerForStub.resetHistory();
+ launchOpenWithDefaultPickerForFileTypeStub.resetHistory();
+ launchModernSettingsDialogDefaultAppsStub.resetHistory();
+
+ info(
+ "When the picker succeeds but Firefox is not default after the delay, event records result_is_default=false"
+ );
+ Services.fog.testResetFOG();
+ isDefaultHandlerForStub.returns(false);
+ await ShellService.setAsDefaultPDFHandler(false);
+
+ event = await awaitAttemptEvent();
+ Assert.equal(event.extra.method, "open_with", "Event method is open_with");
+ Assert.equal(event.extra.success, "true", "Event success is true");
+ Assert.equal(
+ event.extra.result_is_default,
+ "false",
+ "Event result_is_default is false when Firefox did not become default"
+ );
+ isDefaultHandlerForStub.returns(true);
+ userChoiceStub.resetHistory();
+ isDefaultHandlerForStub.resetHistory();
launchOpenWithDefaultPickerForFileTypeStub.resetHistory();
launchModernSettingsDialogDefaultAppsStub.resetHistory();
@@ -399,72 +439,120 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() {
1,
"Recorded user-choice failure"
);
- Assert.equal(
- Glean.browser.setDefaultPdfHandlerUserChoiceResult.Success.testGetValue(),
- undefined,
- "Did not record user-choice success"
- );
- Assert.equal(
- Glean.browser.setDefaultPdfHandlerOpenWithResult.Failure.testGetValue(),
- 1,
- "Recorded open-with failure"
- );
- Assert.equal(
- Glean.browser.setDefaultPdfHandlerOpenWithResult.Success.testGetValue(),
- undefined,
- "Did not record open-with success"
- );
Assert.equal(
Glean.browser.setDefaultPdfHandlerModernSettingsResult.Success.testGetValue(),
1,
"Recorded modern settings success"
);
+
+ event = await awaitAttemptEvent();
Assert.equal(
- Glean.browser.setDefaultPdfHandlerModernSettingsResult.Failure.testGetValue(),
- undefined,
- "Did not record modern settings failure"
+ event.extra.method,
+ "settings",
+ "Event method is settings (last attempted)"
+ );
+ Assert.equal(
+ event.extra.success,
+ "true",
+ "Event success reflects modern settings launch"
+ );
+ Assert.equal(
+ event.extra.result_is_default,
+ "true",
+ "Event result_is_default reflects isDefaultHandlerFor"
);
userChoiceStub.resetHistory();
+ isDefaultHandlerForStub.resetHistory();
launchOpenWithDefaultPickerForFileTypeStub.resetHistory();
launchModernSettingsDialogDefaultAppsStub.resetHistory();
info(
- "When userChoice fails, open-with fails, and modern settings fails, should record all failures"
+ "When userChoice fails, open-with fails, and modern settings fails, event records success=false"
);
Services.fog.testResetFOG();
+ isDefaultHandlerForStub.returns(false);
launchModernSettingsDialogDefaultAppsStub.throws(
new Error("mock modern settings failure")
);
await ShellService.setAsDefaultPDFHandler(false);
- Assert.equal(
- Glean.browser.setDefaultPdfHandlerUserChoiceResult.ErrOther.testGetValue(),
- 1,
- "Recorded user-choice failure"
- );
- Assert.equal(
- Glean.browser.setDefaultPdfHandlerUserChoiceResult.Success.testGetValue(),
- undefined,
- "Did not record user-choice success"
- );
- Assert.equal(
- Glean.browser.setDefaultPdfHandlerOpenWithResult.Failure.testGetValue(),
- 1,
- "Recorded open-with failure"
- );
Assert.equal(
Glean.browser.setDefaultPdfHandlerModernSettingsResult.Failure.testGetValue(),
1,
"Recorded modern settings failure"
);
+
+ event = await awaitAttemptEvent();
Assert.equal(
- Glean.browser.setDefaultPdfHandlerModernSettingsResult.Success.testGetValue(),
- undefined,
- "Did not record modern settings success"
+ event.extra.method,
+ "settings",
+ "Event method is settings (last attempted)"
+ );
+ Assert.equal(
+ event.extra.success,
+ "false",
+ "Event success is false when every method failed"
+ );
+ Assert.equal(
+ event.extra.result_is_default,
+ "false",
+ "Event result_is_default is false when no method set the default"
);
} finally {
launchOpenWithDefaultPickerForFileTypeStub.reset();
launchModernSettingsDialogDefaultAppsStub.reset();
sandbox.restore();
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+add_task(async function test_setAsDefaultPDFHandler_useOpenWithDisabled() {
+ const sandbox = sinon.createSandbox();
+ // With useOpenWith disabled, a userChoice failure should skip the
+ // IOpenWithLauncher branch entirely and fall straight through to the
+ // modern settings dialog.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shell.setDefaultPDFHandler.useOpenWith", false],
+ ["browser.shell.setDefaultPDFHandler.attemptWaitTimeMs", 0],
+ ],
+ });
+
+ try {
+ sandbox
+ .stub(ShellService, "setAsDefaultPDFHandlerUserChoice")
+ .rejects(new Error("mock userChoice failure"));
+ sandbox.stub(ShellService, "_isWindows11").returns(true);
+ sandbox.stub(ShellService, "isDefaultHandlerFor").returns(true);
+
+ Services.fog.testResetFOG();
+ await ShellService.setAsDefaultPDFHandler(false);
+
+ Assert.ok(
+ launchOpenWithDefaultPickerForFileTypeStub.notCalled,
+ "Did not invoke open-with picker when pref is disabled"
+ );
+ Assert.ok(
+ launchModernSettingsDialogDefaultAppsStub.called,
+ "Fell through to modern settings dialog"
+ );
+
+ const event = await awaitAttemptEvent();
+ Assert.equal(
+ event.extra.method,
+ "settings",
+ "Event method skipped open_with and recorded settings"
+ );
+ Assert.equal(event.extra.success, "true", "Event success is true");
+ Assert.equal(
+ event.extra.result_is_default,
+ "true",
+ "Event result_is_default reflects isDefaultHandlerFor"
+ );
+ } finally {
+ launchOpenWithDefaultPickerForFileTypeStub.reset();
+ launchModernSettingsDialogDefaultAppsStub.reset();
+ sandbox.restore();
+ await SpecialPowers.popPrefEnv();
}
});
diff --git a/src/zen/tests/mochitests/shell/browser_setDesktopBackgroundPreview.js b/src/zen/tests/mochitests/shell/browser_setDesktopBackgroundPreview.js
index b847d0998..733123e04 100644
--- a/src/zen/tests/mochitests/shell/browser_setDesktopBackgroundPreview.js
+++ b/src/zen/tests/mochitests/shell/browser_setDesktopBackgroundPreview.js
@@ -16,7 +16,7 @@ add_task(async function () {
await BrowserTestUtils.withNewTab(
{
gBrowser,
- url: "about:logo",
+ url: getRootDirectory(gTestPath) + "large.png",
},
async () => {
const dialogLoad = BrowserTestUtils.domWindowOpened(null, async win => {
diff --git a/src/zen/tests/mochitests/shell/gtest/SetDefaultBrowserButtonTests.cpp b/src/zen/tests/mochitests/shell/gtest/SetDefaultBrowserButtonTests.cpp
index 18f901df8..d28d077b9 100644
--- a/src/zen/tests/mochitests/shell/gtest/SetDefaultBrowserButtonTests.cpp
+++ b/src/zen/tests/mochitests/shell/gtest/SetDefaultBrowserButtonTests.cpp
@@ -1,4 +1,3 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
diff --git a/src/zen/tests/mochitests/shell/large.png b/src/zen/tests/mochitests/shell/large.png
new file mode 100644
index 000000000..37012cf96
Binary files /dev/null and b/src/zen/tests/mochitests/shell/large.png differ
diff --git a/src/zen/tests/mochitests/shell/unit/test_desktopEntryStatus.js b/src/zen/tests/mochitests/shell/unit/test_desktopEntryStatus.js
new file mode 100644
index 000000000..de9b047e9
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/unit/test_desktopEntryStatus.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ShellService: "moz-src:///browser/components/shell/ShellService.sys.mjs",
+});
+
+const BAREBONES_DESKTOP_ENTRY = `[Desktop Entry]
+Version=1.5
+Type=Application
+Name=test_desktopEntryStatus.js test case
+`;
+
+let gHomeDir;
+let gSystemDir;
+
+const filename = what => `test_desktopEntryStatus_file_${what}.desktop`;
+
+// GLib caches results for efficiency. Unfortunately, it doesn't really provide
+// a way to invalidate that cache, aside from hoping that the file monitor
+// picks up on it. Resolve this by setting up all of the desktop entries at the
+// start, then doing checks, then exiting.
+//
+// (Some others are special-cased, namely absent and Hidden= checks.)
+const kDesktopEntries = [
+ {
+ label: "visible",
+ content: BAREBONES_DESKTOP_ENTRY,
+ expected: Ci.nsIGNOMEShellService.DESKTOP_ENTRY_VISIBLE,
+ },
+ {
+ label: "nodisplay",
+ content: BAREBONES_DESKTOP_ENTRY + "NoDisplay=true\n",
+ expected: Ci.nsIGNOMEShellService.DESKTOP_ENTRY_INVISIBLE,
+ },
+ {
+ label: "onlyshowin-matching",
+ content: BAREBONES_DESKTOP_ENTRY + "OnlyShowIn=FirefoxOS\n",
+ expected: Ci.nsIGNOMEShellService.DESKTOP_ENTRY_VISIBLE,
+ },
+ {
+ label: "onlyshowin-notmatching",
+ content: BAREBONES_DESKTOP_ENTRY + "OnlyShowIn=another\n",
+ expected: Ci.nsIGNOMEShellService.DESKTOP_ENTRY_INVISIBLE,
+ },
+ {
+ label: "notshowin-matching",
+ content: BAREBONES_DESKTOP_ENTRY + "NotShowIn=FirefoxOS\n",
+ expected: Ci.nsIGNOMEShellService.DESKTOP_ENTRY_INVISIBLE,
+ },
+ {
+ label: "notshowin-notmatching",
+ content: BAREBONES_DESKTOP_ENTRY + "NotShowIn=another\n",
+ expected: Ci.nsIGNOMEShellService.DESKTOP_ENTRY_VISIBLE,
+ },
+];
+
+add_setup(async function setup() {
+ let unique = await IOUtils.createUniqueDirectory(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "desktopEntryStatusTest"
+ );
+
+ let homeDir = PathUtils.join(unique, "data-home");
+ Services.env.set("XDG_DATA_HOME", homeDir);
+ gHomeDir = PathUtils.join(homeDir, "applications");
+ await IOUtils.makeDirectory(gHomeDir, { createAncestors: true });
+
+ let systemDir = PathUtils.join(unique, "data-system");
+ Services.env.set("XDG_DATA_DIRS", systemDir);
+ gSystemDir = PathUtils.join(systemDir, "applications");
+ await IOUtils.makeDirectory(gSystemDir, { createAncestors: true });
+
+ Services.env.set("XDG_CURRENT_DESKTOP", "FirefoxOS");
+
+ await IOUtils.writeUTF8(
+ PathUtils.join(gHomeDir, filename("deleted")),
+ BAREBONES_DESKTOP_ENTRY + "Hidden=true\n"
+ );
+ await IOUtils.writeUTF8(
+ PathUtils.join(gSystemDir, filename("deleted")),
+ BAREBONES_DESKTOP_ENTRY
+ );
+
+ for (const desktopEntry of kDesktopEntries) {
+ await IOUtils.writeUTF8(
+ PathUtils.join(gHomeDir, filename(desktopEntry.label + "-home")),
+ desktopEntry.content
+ );
+ await IOUtils.writeUTF8(
+ PathUtils.join(gSystemDir, filename(desktopEntry.label + "-system")),
+ desktopEntry.content
+ );
+ }
+
+ registerCleanupFunction(async () => {
+ return IOUtils.remove(unique, { recursive: true });
+ });
+});
+
+add_task(function test_desktopEntryStatus() {
+ Assert.equal(
+ ShellService.getDesktopEntryStatus(filename("absent")),
+ Ci.nsIGNOMEShellService.DESKTOP_ENTRY_ABSENT,
+ "A desktop entry that doesn't exist should be absent."
+ );
+ Assert.equal(
+ ShellService.getDesktopEntryStatus(filename("hidden")),
+ Ci.nsIGNOMEShellService.DESKTOP_ENTRY_ABSENT,
+ "A desktop entry shadowed by one with the Hidden= attribute should be absent."
+ );
+
+ for (const desktopEntry of kDesktopEntries) {
+ Assert.equal(
+ ShellService.getDesktopEntryStatus(
+ filename(desktopEntry.label + "-home")
+ ),
+ desktopEntry.expected,
+ "Desktop entry matches when at the local level: " + desktopEntry.label
+ );
+ Assert.equal(
+ ShellService.getDesktopEntryStatus(
+ filename(desktopEntry.label + "-system")
+ ),
+ desktopEntry.expected,
+ "Desktop entry matches when at the system level: " + desktopEntry.label
+ );
+ }
+});
diff --git a/src/zen/tests/mochitests/shell/unit/test_maybeCreateLaunchOnLoginOnFirstRun.js b/src/zen/tests/mochitests/shell/unit/test_maybeCreateLaunchOnLoginOnFirstRun.js
new file mode 100644
index 000000000..16106de4d
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/unit/test_maybeCreateLaunchOnLoginOnFirstRun.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ StartupOSIntegration:
+ "moz-src:///browser/components/shell/StartupOSIntegration.sys.mjs",
+ WindowsLaunchOnLogin: "resource://gre/modules/WindowsLaunchOnLogin.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+const PREF = "browser.startup.windowsLaunchOnLogin.defaultEnabled";
+
+async function runWith({ isFirstRun, prefValue, approved }) {
+ let sandbox = sinon.createSandbox();
+ let approvedStub = sandbox
+ .stub(WindowsLaunchOnLogin, "getLaunchOnLoginApproved")
+ .resolves(approved);
+ let createStub = sandbox
+ .stub(WindowsLaunchOnLogin, "createLaunchOnLogin")
+ .resolves();
+
+ if (prefValue === null) {
+ Services.prefs.clearUserPref(PREF);
+ } else {
+ Services.prefs.setBoolPref(PREF, prefValue);
+ }
+
+ try {
+ await StartupOSIntegration.maybeCreateLaunchOnLoginOnFirstRun(isFirstRun);
+ return { approvedStub, createStub };
+ } finally {
+ sandbox.restore();
+ Services.prefs.clearUserPref(PREF);
+ }
+}
+
+add_task(async function test_creates_when_all_conditions_true() {
+ let { createStub } = await runWith({
+ isFirstRun: true,
+ prefValue: true,
+ approved: true,
+ });
+ Assert.ok(
+ createStub.calledOnce,
+ "createLaunchOnLogin should be called when isFirstRun, pref, and approval are all true"
+ );
+});
+
+add_task(async function test_skips_when_not_first_run() {
+ let { createStub, approvedStub } = await runWith({
+ isFirstRun: false,
+ prefValue: true,
+ approved: true,
+ });
+ Assert.ok(
+ !createStub.called,
+ "createLaunchOnLogin should not be called when isFirstRun is false"
+ );
+ Assert.ok(
+ !approvedStub.called,
+ "getLaunchOnLoginApproved should be short-circuited when isFirstRun is false"
+ );
+});
+
+add_task(async function test_skips_when_pref_disabled() {
+ let { createStub, approvedStub } = await runWith({
+ isFirstRun: true,
+ prefValue: false,
+ approved: true,
+ });
+ Assert.ok(
+ !createStub.called,
+ "createLaunchOnLogin should not be called when pref is false"
+ );
+ Assert.ok(
+ !approvedStub.called,
+ "getLaunchOnLoginApproved should be short-circuited when pref is false"
+ );
+});
+
+add_task(async function test_skips_when_windows_policy_denies() {
+ let { createStub, approvedStub } = await runWith({
+ isFirstRun: true,
+ prefValue: true,
+ approved: false,
+ });
+ Assert.ok(
+ approvedStub.calledOnce,
+ "getLaunchOnLoginApproved should be consulted when pref and isFirstRun are true"
+ );
+ Assert.ok(
+ !createStub.called,
+ "createLaunchOnLogin should not be called when Windows policy denies"
+ );
+});
+
+add_task(async function test_uses_pref_default_when_unset() {
+ let { createStub } = await runWith({
+ isFirstRun: true,
+ prefValue: null,
+ approved: true,
+ });
+ Assert.ok(
+ createStub.calledOnce,
+ "createLaunchOnLogin should be called when pref is at its built-in default of true"
+ );
+});
diff --git a/src/zen/tests/mochitests/shell/unit/xpcshell.toml b/src/zen/tests/mochitests/shell/unit/xpcshell.toml
index 4b4a87edc..bfd6f01ea 100644
--- a/src/zen/tests/mochitests/shell/unit/xpcshell.toml
+++ b/src/zen/tests/mochitests/shell/unit/xpcshell.toml
@@ -5,6 +5,11 @@ run-if = [
firefox-appdir = "browser"
tags = "os_integration"
+["test_desktopEntryStatus.js"]
+run-if = [
+ "os == 'linux'",
+]
+
["test_linuxDesktopEntry.js"]
run-if = [
"os == 'linux'",
@@ -15,6 +20,11 @@ run-if = [
"os == 'mac'",
]
+["test_maybeCreateLaunchOnLoginOnFirstRun.js"]
+run-if = [
+ "os == 'win'"
+]
+
["test_secondaryTileJs.js"]
run-if = [
"os == 'win'"
diff --git a/src/zen/tests/mochitests/tooltiptext/browser_input_file_tooltips.js b/src/zen/tests/mochitests/tooltiptext/browser_input_file_tooltips.js
index 2f1385f37..7e4cfb9a0 100644
--- a/src/zen/tests/mochitests/tooltiptext/browser_input_file_tooltips.js
+++ b/src/zen/tests/mochitests/tooltiptext/browser_input_file_tooltips.js
@@ -63,7 +63,7 @@ async function do_test(test) {
if (test.value) {
info("Creating mock filepicker to select files");
let MockFilePicker = SpecialPowers.MockFilePicker;
- MockFilePicker.init(window.browsingContext);
+ MockFilePicker.init();
MockFilePicker.returnValue = MockFilePicker.returnOK;
MockFilePicker.displayDirectory = FileUtils.getDir("TmpD", []);
MockFilePicker.setFiles([tempFile]);
diff --git a/src/zen/tests/moz.build b/src/zen/tests/moz.build
index a79f5a7f4..fbbf132a8 100644
--- a/src/zen/tests/moz.build
+++ b/src/zen/tests/moz.build
@@ -13,6 +13,7 @@ BROWSER_CHROME_MANIFESTS += [
"pinned/browser.toml",
"popover/browser.toml",
"site_control/browser.toml",
+ "space_routing/browser.toml",
"spaces/browser.toml",
"split_view/browser.toml",
"tabs/browser.toml",
diff --git a/src/zen/tests/popover/browser.toml b/src/zen/tests/popover/browser.toml
index 756cc7fa3..fae0c68c0 100644
--- a/src/zen/tests/popover/browser.toml
+++ b/src/zen/tests/popover/browser.toml
@@ -3,10 +3,9 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
[DEFAULT]
-prefs = ["zen.view.mac.show-three-dot-menu=true"]
+prefs = ["zen.view.mac.show-three-dot-menu=true", "widget.macos.native-context-menus=true"]
["browser_popover_height_constraint.js"]
run-if = [
"os == 'mac'", # gh-12782
]
-prefs = ["widget.macos.native-context-menus=true"]
diff --git a/src/zen/tests/space_routing/browser.toml b/src/zen/tests/space_routing/browser.toml
new file mode 100644
index 000000000..f81904fa2
--- /dev/null
+++ b/src/zen/tests/space_routing/browser.toml
@@ -0,0 +1,22 @@
+# 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/.
+
+[DEFAULT]
+support-files = [
+ "head.js",
+]
+
+["browser_space_routing_crud.js"]
+
+["browser_space_routing_dialog.js"]
+
+["browser_space_routing_fuzz.js"]
+
+["browser_space_routing_on_add_tab.js"]
+
+["browser_space_routing_redirect_navigation.js"]
+
+["browser_space_routing_route_matching.js"]
+
+["browser_space_routing_route_uri.js"]
diff --git a/src/zen/tests/space_routing/browser_space_routing_crud.js b/src/zen/tests/space_routing/browser_space_routing_crud.js
new file mode 100644
index 000000000..afff1e152
--- /dev/null
+++ b/src/zen/tests/space_routing/browser_space_routing_crud.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async function () {
+ clearAllRoutes();
+ const savedDefault = gZenSpaceRoutingManager.getDefaultExternalRoute();
+ registerCleanupFunction(() => {
+ clearAllRoutes();
+ gZenSpaceRoutingManager.setDefaultExternalRoute(savedDefault);
+ });
+});
+
+add_task(async function test_empty_route_shape_and_unique_ids() {
+ const a = gZenSpaceRoutingManager.getEmptyRoute();
+ const b = gZenSpaceRoutingManager.getEmptyRoute();
+
+ Assert.equal(a.reference, "", "Empty route starts with no reference");
+ Assert.equal(
+ a.openIn,
+ "most-recent-space",
+ "Empty route defaults to most-recent-space"
+ );
+ Assert.equal(a.matchType, "contains", "Empty route defaults to 'contains'");
+ Assert.equal(typeof a.id, "string", "Empty route has a string id");
+ ok(a.id.length, "Empty route id is non-empty");
+ Assert.notEqual(a.id, b.id, "Each empty route gets a unique id");
+});
+
+add_task(async function test_create_get_update_remove_lifecycle() {
+ clearAllRoutes();
+ Assert.equal(
+ gZenSpaceRoutingManager.getAllRoutes().length,
+ 0,
+ "Precondition: no routes"
+ );
+
+ const created = gZenSpaceRoutingManager.createNewRoute();
+ Assert.equal(
+ gZenSpaceRoutingManager.getAllRoutes().length,
+ 1,
+ "createNewRoute() appends one route"
+ );
+
+ created.reference = "zen-browser.app";
+ created.openIn = "ws-42";
+ created.matchType = "equal-to";
+ gZenSpaceRoutingManager.updateRoute(created);
+
+ const fetched = gZenSpaceRoutingManager.getRoute(created.id);
+ Assert.equal(fetched.reference, "zen-browser.app", "reference persisted");
+ Assert.equal(fetched.openIn, "ws-42", "openIn persisted");
+ Assert.equal(fetched.matchType, "equal-to", "matchType persisted");
+
+ gZenSpaceRoutingManager.removeRoute(created.id);
+ Assert.equal(
+ gZenSpaceRoutingManager.getAllRoutes().length,
+ 0,
+ "removeRoute() deletes the route"
+ );
+});
+
+add_task(async function test_remove_only_targets_the_given_id() {
+ clearAllRoutes();
+ const keep1 = addRoute({ reference: "a" });
+ const drop = addRoute({ reference: "b" });
+ const keep2 = addRoute({ reference: "c" });
+
+ gZenSpaceRoutingManager.removeRoute(drop.id);
+
+ const ids = gZenSpaceRoutingManager.getAllRoutes().map(r => r.id);
+ Assert.deepEqual(
+ ids,
+ [keep1.id, keep2.id],
+ "Only the targeted route is removed; order of the rest is preserved"
+ );
+});
+
+add_task(async function test_reads_return_copies_not_internal_refs() {
+ clearAllRoutes();
+ const created = gZenSpaceRoutingManager.createNewRoute();
+
+ const fromGet = gZenSpaceRoutingManager.getRoute(created.id);
+ fromGet.reference = "mutated-via-getRoute";
+ Assert.equal(
+ gZenSpaceRoutingManager.getRoute(created.id).reference,
+ "",
+ "getRoute() returns a copy; external mutation does not leak"
+ );
+
+ const all = gZenSpaceRoutingManager.getAllRoutes();
+ all[0].reference = "mutated-via-getAllRoutes";
+ Assert.equal(
+ gZenSpaceRoutingManager.getRoute(created.id).reference,
+ "",
+ "getAllRoutes() returns copies; external mutation does not leak"
+ );
+});
+
+add_task(async function test_default_external_route_getter_setter() {
+ gZenSpaceRoutingManager.setDefaultExternalRoute("ws-default");
+ Assert.equal(
+ gZenSpaceRoutingManager.getDefaultExternalRoute(),
+ "ws-default",
+ "setDefaultExternalRoute() round-trips through the getter"
+ );
+
+ gZenSpaceRoutingManager.setDefaultExternalRoute("most-recent-space");
+ Assert.equal(
+ gZenSpaceRoutingManager.getDefaultExternalRoute(),
+ "most-recent-space",
+ "The default external route can be changed again"
+ );
+});
diff --git a/src/zen/tests/space_routing/browser_space_routing_dialog.js b/src/zen/tests/space_routing/browser_space_routing_dialog.js
new file mode 100644
index 000000000..ee186f1d9
--- /dev/null
+++ b/src/zen/tests/space_routing/browser_space_routing_dialog.js
@@ -0,0 +1,255 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async function () {
+ clearAllRoutes();
+ const savedDefault = gZenSpaceRoutingManager.getDefaultExternalRoute();
+ registerCleanupFunction(() => {
+ clearAllRoutes();
+ gZenSpaceRoutingManager.setDefaultExternalRoute(savedDefault);
+ });
+});
+
+add_task(async function test_empty_placeholder_and_add_route() {
+ clearAllRoutes();
+ const dlg = await openRoutingDialog();
+ try {
+ const doc = dlg.document;
+ const emptyText = doc.getElementById("sr-empty-content");
+ const content = doc.getElementById("sr-content");
+
+ Assert.notEqual(
+ emptyText.style.display,
+ "none",
+ "The empty-state placeholder is visible when there are no routes"
+ );
+
+ doc.getElementById("sr-new-route").click();
+
+ Assert.equal(
+ content.querySelectorAll(".sr-rule-container").length,
+ 1,
+ "Clicking 'New Route' injects one route element"
+ );
+ Assert.equal(
+ emptyText.style.display,
+ "none",
+ "The empty-state placeholder is hidden once a route exists"
+ );
+ Assert.equal(
+ gZenSpaceRoutingManager.getAllRoutes().length,
+ 1,
+ "The new route is persisted into the manager"
+ );
+ } finally {
+ await closeRoutingDialog(dlg);
+ clearAllRoutes();
+ }
+});
+
+add_task(async function test_remove_route_via_ui() {
+ clearAllRoutes();
+ addRoute({ reference: "github.com" });
+ const dlg = await openRoutingDialog();
+ try {
+ const doc = dlg.document;
+ Assert.equal(
+ doc.querySelectorAll(".sr-rule-container").length,
+ 1,
+ "Existing route is rendered on open"
+ );
+
+ doc.querySelector(".sr-remove").click();
+
+ Assert.equal(
+ doc.querySelectorAll(".sr-rule-container").length,
+ 0,
+ "The route element is removed from the DOM"
+ );
+ Assert.equal(
+ gZenSpaceRoutingManager.getAllRoutes().length,
+ 0,
+ "The route is removed from the manager"
+ );
+ Assert.equal(
+ doc.getElementById("sr-empty-content").style.display,
+ "flex",
+ "The empty-state placeholder returns after the last route is removed"
+ );
+ } finally {
+ await closeRoutingDialog(dlg);
+ clearAllRoutes();
+ }
+});
+
+add_task(async function test_match_type_updates_placeholder_and_store() {
+ clearAllRoutes();
+ const route = addRoute({ reference: "", matchType: "contains" });
+ const dlg = await openRoutingDialog();
+ try {
+ const doc = dlg.document;
+ const menulist = doc.querySelector(".sr-rule-container .match-type-select");
+ const input = doc.querySelector(".sr-rule-container .input");
+
+ Assert.equal(
+ input.placeholder,
+ "zen-browser.app",
+ "The 'contains' placeholder is the plain domain"
+ );
+
+ menulist.value = "regex";
+ menulist.dispatchEvent(new Event("command", { bubbles: true }));
+
+ Assert.equal(
+ input.placeholder,
+ "zen-browser\\.app",
+ "Switching to 'regex' updates the placeholder to an escaped pattern"
+ );
+ Assert.equal(
+ gZenSpaceRoutingManager.getRoute(route.id).matchType,
+ "regex",
+ "The match type change is persisted to the manager"
+ );
+ } finally {
+ await closeRoutingDialog(dlg);
+ clearAllRoutes();
+ }
+});
+
+add_task(async function test_invalid_regex_is_flagged_and_not_saved() {
+ clearAllRoutes();
+ const route = addRoute({ reference: "", matchType: "regex" });
+ const dlg = await openRoutingDialog();
+ try {
+ const doc = dlg.document;
+ const input = doc.querySelector(".sr-rule-container .input");
+
+ input.value = "([";
+ input.dispatchEvent(new Event("input", { bubbles: true }));
+
+ ok(
+ input.classList.contains("invalid"),
+ "An invalid regex marks the input as invalid"
+ );
+ Assert.equal(
+ gZenSpaceRoutingManager.getRoute(route.id).reference,
+ "",
+ "An invalid regex is NOT written to the route"
+ );
+
+ input.value = "zen.*app";
+ input.dispatchEvent(new Event("input", { bubbles: true }));
+
+ ok(
+ !input.classList.contains("invalid"),
+ "A subsequently valid regex clears the invalid state"
+ );
+ Assert.equal(
+ gZenSpaceRoutingManager.getRoute(route.id).reference,
+ "zen.*app",
+ "A valid regex is written to the route"
+ );
+ } finally {
+ await closeRoutingDialog(dlg);
+ clearAllRoutes();
+ }
+});
+
+add_task(async function test_default_external_select_updates_store() {
+ clearAllRoutes();
+ await gZenWorkspaces.promiseInitialized;
+ gZenSpaceRoutingManager.setDefaultExternalRoute("most-recent-space");
+
+ const dlg = await openRoutingDialog();
+ try {
+ const doc = dlg.document;
+ const select = doc.getElementById("sr-default-external-open-in");
+
+ await TestUtils.waitForCondition(
+ () => select.querySelectorAll("menuitem").length > 1,
+ "External-default options were populated"
+ );
+
+ const workspaceUuid = gZenWorkspaces.getWorkspaces()[0].uuid;
+ select.value = workspaceUuid;
+ select.dispatchEvent(new Event("command", { bubbles: true }));
+
+ Assert.equal(
+ gZenSpaceRoutingManager.getDefaultExternalRoute(),
+ workspaceUuid,
+ "Changing the external-default select updates the manager"
+ );
+ } finally {
+ await closeRoutingDialog(dlg);
+ gZenSpaceRoutingManager.setDefaultExternalRoute("most-recent-space");
+ }
+});
+
+add_task(async function test_routes_are_saved_on_close() {
+ clearAllRoutes();
+ const dlg = await openRoutingDialog();
+
+ let saveCalls = 0;
+ const realSave = gZenSpaceRoutingManager.saveRoutes;
+ gZenSpaceRoutingManager.saveRoutes = function () {
+ saveCalls++;
+ return realSave.call(this);
+ };
+
+ try {
+ const closed = promiseRoutingDialogClosed();
+ dlg.close();
+ await TestUtils.waitForCondition(
+ () => saveCalls > 0,
+ "Closing the dialog flushes routes to disk via saveRoutes()"
+ );
+ await closed;
+ } finally {
+ delete gZenSpaceRoutingManager.saveRoutes;
+ }
+});
+
+add_task(async function test_open_broadcasts_kill_to_other_instances() {
+ clearAllRoutes();
+
+ let killNotified = false;
+ const observer = {
+ observe(_subject, topic) {
+ if (topic === "zen-space-routing-kill") {
+ killNotified = true;
+ }
+ },
+ };
+ Services.obs.addObserver(observer, "zen-space-routing-kill");
+
+ let dlg;
+ try {
+ dlg = await openRoutingDialog();
+ ok(
+ killNotified,
+ "Opening a dialog broadcasts 'zen-space-routing-kill' so others can close"
+ );
+ } finally {
+ Services.obs.removeObserver(observer, "zen-space-routing-kill");
+ if (dlg) {
+ await closeRoutingDialog(dlg);
+ }
+ }
+});
+
+add_task(async function test_kill_notification_closes_dialog() {
+ clearAllRoutes();
+ await openRoutingDialog();
+
+ const closed = promiseRoutingDialogClosed();
+ Services.obs.notifyObservers(null, "zen-space-routing-kill");
+ await closed;
+
+ const container = document.getElementById("window-modal-dialog");
+ ok(
+ !container.open && !container.hasChildNodes(),
+ "A 'zen-space-routing-kill' notification closes the dialog"
+ );
+});
diff --git a/src/zen/tests/space_routing/browser_space_routing_fuzz.js b/src/zen/tests/space_routing/browser_space_routing_fuzz.js
new file mode 100644
index 000000000..f74d74c58
--- /dev/null
+++ b/src/zen/tests/space_routing/browser_space_routing_fuzz.js
@@ -0,0 +1,239 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Seeded fuzzing for the pure routing decision functions. The point is not to
+// assert a particular routing outcome but to prove robustness invariants under
+// adversarial input: the functions must never throw, must always return the
+// declared type, and routeUri must only ever return a value it is allowed to.
+//
+// The RNG is seeded so any failure is reproducible: re-run with the logged seed.
+
+const FUZZ_SEED = 0x5eed1234;
+
+// mulberry32 — small, fast, deterministic PRNG.
+function makeRng(seed) {
+ let s = seed >>> 0;
+ return function rng() {
+ s = (s + 0x6d2b79f5) | 0;
+ let t = Math.imul(s ^ (s >>> 15), 1 | s);
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+ };
+}
+
+const DOMAIN_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-.";
+const REGEX_CHARS = ".*+?^${}()|[]\\" + DOMAIN_CHARS;
+const TRICKY_CHARS =
+ DOMAIN_CHARS + "%/?#:@!$&'()*+,;= []{}<>\"\\^`|~\tünïçødé日本語🚀";
+const SCHEMES = [
+ "http://",
+ "https://",
+ "ftp://",
+ "file://",
+ "about:",
+ "data:text/plain,",
+ "javascript:",
+ "//",
+ "",
+];
+const MATCH_TYPES = ["contains", "equal-to", "regex", "bogus-type", ""];
+
+function randInt(rng, n) {
+ return Math.floor(rng() * n);
+}
+function pick(rng, arr) {
+ return arr[randInt(rng, arr.length)];
+}
+function randString(rng, maxLen, charset) {
+ const len = randInt(rng, maxLen + 1);
+ let out = "";
+ for (let i = 0; i < len; i++) {
+ out += charset[randInt(rng, charset.length)];
+ }
+ return out;
+}
+
+function randomUrl(rng) {
+ const scheme = pick(rng, SCHEMES);
+ const host = randString(rng, 30, DOMAIN_CHARS + "ünïçødé");
+ const port = rng() < 0.2 ? ":" + randInt(rng, 99999) : "";
+ const path = rng() < 0.7 ? "/" + randString(rng, 40, TRICKY_CHARS) : "";
+ return scheme + host + port + path;
+}
+
+function randomReference(rng) {
+ switch (randInt(rng, 5)) {
+ case 0:
+ return "";
+ case 1:
+ return " ";
+ case 2:
+ return randString(rng, 30, DOMAIN_CHARS);
+ case 3:
+ // Deliberately regex-flavoured to exercise the "regex" match path.
+ return randString(rng, 20, REGEX_CHARS);
+ default:
+ return randString(rng, 50, TRICKY_CHARS);
+ }
+}
+
+function randomRoute(rng, openIn = "most-recent-space") {
+ return {
+ id: "fuzz-" + randInt(rng, 1e9),
+ reference: randomReference(rng),
+ openIn,
+ matchType: pick(rng, MATCH_TYPES),
+ };
+}
+
+add_setup(async function () {
+ clearAllRoutes();
+ registerCleanupFunction(() => clearAllRoutes());
+ info(`Space Routing fuzz seed: 0x${FUZZ_SEED.toString(16)}`);
+});
+
+add_task(async function fuzz_isRouteMatching_never_throws() {
+ const rng = makeRng(FUZZ_SEED);
+ const ITERATIONS = 5000;
+
+ for (let i = 0; i < ITERATIONS; i++) {
+ const url = randomUrl(rng);
+ const route = randomRoute(rng);
+
+ let result;
+ try {
+ result = gZenSpaceRoutingManager.isRouteMatching(url, route);
+ } catch (e) {
+ ok(
+ false,
+ `isRouteMatching threw on url=${JSON.stringify(
+ url
+ )} route=${JSON.stringify(route)}: ${e}`
+ );
+ continue;
+ }
+
+ is(
+ typeof result,
+ "boolean",
+ `isRouteMatching must return a boolean (iter ${i})`
+ );
+
+ // An empty / whitespace reference can never match.
+ if (typeof route.reference !== "string" || route.reference.trim() === "") {
+ ok(!result, "Empty reference never matches");
+ }
+ }
+});
+
+add_task(async function fuzz_routeUri_returns_only_valid_destinations() {
+ const rng = makeRng(FUZZ_SEED ^ 0x1111);
+ clearAllRoutes();
+
+ // Populate the manager with a mix of routes pointing at a few destinations.
+ const destinations = ["most-recent-space", "ws-a", "ws-b", "ws-c"];
+ for (let i = 0; i < 200; i++) {
+ const r = randomRoute(rng, pick(rng, destinations));
+ addRoute({
+ reference: r.reference,
+ openIn: r.openIn,
+ matchType: r.matchType,
+ });
+ }
+
+ const allowed = new Set(
+ gZenSpaceRoutingManager.getAllRoutes().map(r => r.openIn)
+ );
+ allowed.add("most-recent-space");
+ const defaultExternal = gZenSpaceRoutingManager.getDefaultExternalRoute();
+ allowed.add(defaultExternal);
+
+ const ITERATIONS = 4000;
+ for (let i = 0; i < ITERATIONS; i++) {
+ const url = randomUrl(rng);
+ const fromExternal = rng() < 0.5;
+
+ let result;
+ try {
+ result = gZenSpaceRoutingManager.routeUri(url, { fromExternal });
+ } catch (e) {
+ ok(false, `routeUri threw on url=${JSON.stringify(url)}: ${e}`);
+ continue;
+ }
+
+ is(typeof result, "string", `routeUri must return a string (iter ${i})`);
+ ok(
+ allowed.has(result),
+ `routeUri returned an out-of-set destination: ${JSON.stringify(result)}`
+ );
+ }
+
+ clearAllRoutes();
+});
+
+add_task(async function fuzz_shouldRedirectNavigation_invariants() {
+ const rng = makeRng(FUZZ_SEED ^ 0x2222);
+ clearAllRoutes();
+
+ const workspaces = [
+ { uuid: "ws-a", containerTabId: 1 },
+ { uuid: "ws-b", containerTabId: 2 },
+ ];
+ const win = makeFakeWindow({ workspaces });
+
+ for (let i = 0; i < 120; i++) {
+ const r = randomRoute(
+ rng,
+ pick(rng, ["ws-a", "ws-b", "most-recent-space"])
+ );
+ addRoute({
+ reference: r.reference,
+ openIn: r.openIn,
+ matchType: r.matchType,
+ });
+ }
+
+ const ITERATIONS = 4000;
+ const currentChoices = ["ws-a", "ws-b", "ws-other", "", null];
+
+ for (let i = 0; i < ITERATIONS; i++) {
+ const url = randomUrl(rng);
+ const currentWorkspaceId = pick(rng, currentChoices);
+
+ let result;
+ try {
+ result = gZenSpaceRoutingManager.shouldRedirectNavigation(
+ url,
+ currentWorkspaceId,
+ win
+ );
+ } catch (e) {
+ ok(
+ false,
+ `shouldRedirectNavigation threw on url=${JSON.stringify(url)}: ${e}`
+ );
+ continue;
+ }
+
+ is(typeof result, "boolean", "shouldRedirectNavigation returns a boolean");
+
+ if (result) {
+ // If we decided to redirect, the target must be a real, *different* space.
+ const target = gZenSpaceRoutingManager.routeUri(url, {
+ fromExternal: false,
+ });
+ ok(
+ target !== "most-recent-space" && target !== currentWorkspaceId,
+ `Redirect target must differ from current space (url=${url})`
+ );
+ ok(
+ !!win.gZenWorkspaces.getWorkspaceFromId(target),
+ "Redirect target must be an existing workspace"
+ );
+ }
+ }
+
+ clearAllRoutes();
+});
diff --git a/src/zen/tests/space_routing/browser_space_routing_on_add_tab.js b/src/zen/tests/space_routing/browser_space_routing_on_add_tab.js
new file mode 100644
index 000000000..8fa66ceaa
--- /dev/null
+++ b/src/zen/tests/space_routing/browser_space_routing_on_add_tab.js
@@ -0,0 +1,363 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TARGET_WS = { uuid: "ws-target", containerTabId: 7 };
+
+add_setup(async function () {
+ clearAllRoutes();
+ registerCleanupFunction(() => clearAllRoutes());
+});
+
+add_task(async function test_onBeforeAddTab_resolves_container_for_match() {
+ clearAllRoutes();
+ addRoute({
+ reference: "github.com",
+ matchType: "contains",
+ openIn: TARGET_WS.uuid,
+ });
+ const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
+
+ const result = gZenSpaceRoutingManager.onBeforeAddTab(
+ "https://github.com/zen",
+ {},
+ win
+ );
+
+ Assert.deepEqual(
+ result,
+ {
+ shouldEarlyExit: false,
+ userContextId: TARGET_WS.containerTabId,
+ isRouteFound: true,
+ targetRoute: TARGET_WS.uuid,
+ },
+ "A matching route resolves to the workspace's containerTabId"
+ );
+});
+
+add_task(async function test_onBeforeAddTab_no_match_returns_no_route() {
+ clearAllRoutes();
+ const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
+
+ const result = gZenSpaceRoutingManager.onBeforeAddTab(
+ "https://example.com",
+ {},
+ win
+ );
+
+ Assert.deepEqual(
+ result,
+ {
+ shouldEarlyExit: false,
+ userContextId: null,
+ isRouteFound: false,
+ targetRoute: "most-recent-space",
+ },
+ "An unmatched URL (most-recent-space) reports no container and no route"
+ );
+});
+
+add_task(async function test_onBeforeAddTab_route_to_missing_workspace() {
+ clearAllRoutes();
+ addRoute({
+ reference: "github.com",
+ matchType: "contains",
+ openIn: "ws-does-not-exist",
+ });
+ const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
+
+ const result = gZenSpaceRoutingManager.onBeforeAddTab(
+ "https://github.com",
+ {},
+ win
+ );
+
+ Assert.deepEqual(
+ result,
+ {
+ shouldEarlyExit: false,
+ userContextId: null,
+ isRouteFound: false,
+ targetRoute: "ws-does-not-exist",
+ },
+ "A route to a non-existent workspace yields no container and no route"
+ );
+});
+
+add_task(async function test_onBeforeAddTab_skips_special_tab_options() {
+ clearAllRoutes();
+ addRoute({
+ reference: "github.com",
+ matchType: "contains",
+ openIn: TARGET_WS.uuid,
+ });
+ const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
+
+ for (const skipOption of ["skipRoute", "pinned", "tabGroup"]) {
+ const result = gZenSpaceRoutingManager.onBeforeAddTab(
+ "https://github.com/zen",
+ { [skipOption]: true },
+ win
+ );
+ Assert.deepEqual(
+ result,
+ {
+ shouldEarlyExit: false,
+ userContextId: null,
+ isRouteFound: false,
+ targetRoute: null,
+ },
+ `Option '${skipOption}' skips routing even though a rule matches`
+ );
+ }
+});
+
+add_task(async function test_onBeforeAddTab_skips_until_startup_ready() {
+ clearAllRoutes();
+ addRoute({
+ reference: "github.com",
+ matchType: "contains",
+ openIn: TARGET_WS.uuid,
+ });
+ const win = makeFakeWindow({ ready: false, workspaces: [TARGET_WS] });
+
+ const result = gZenSpaceRoutingManager.onBeforeAddTab(
+ "https://github.com/zen",
+ {},
+ win
+ );
+
+ Assert.deepEqual(
+ result,
+ {
+ shouldEarlyExit: false,
+ userContextId: null,
+ isRouteFound: false,
+ targetRoute: null,
+ },
+ "While gZenStartup.isReady is false (session restore), routing is skipped"
+ );
+});
+
+add_task(async function test_onAfterAddTab_moves_tab_on_non_origin_window() {
+ clearAllRoutes();
+ addRoute({
+ reference: "github.com",
+ matchType: "contains",
+ openIn: TARGET_WS.uuid,
+ });
+ const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
+ const fakeTab = { parentNode: {} };
+
+ gZenSpaceRoutingManager.onAfterAddTab(
+ "https://github.com/zen",
+ fakeTab,
+ {},
+ win,
+ { targetRoute: TARGET_WS.uuid }
+ );
+
+ await TestUtils.waitForCondition(
+ () => win.gZenWorkspaces.moveCalls.length === 1,
+ "moveTabToWorkspace was called once"
+ );
+ Assert.equal(
+ win.gZenWorkspaces.moveCalls[0].uuid,
+ TARGET_WS.uuid,
+ "The tab is moved to the matched workspace"
+ );
+ Assert.equal(
+ win.gZenWorkspaces.moveCalls[0].tab,
+ fakeTab,
+ "The correct tab element is moved"
+ );
+ Assert.equal(
+ win.gZenWorkspaces.changeCalls.length,
+ 0,
+ "A non-originating window does not switch the active workspace"
+ );
+});
+
+add_task(async function test_onAfterAddTab_reuses_before_result() {
+ clearAllRoutes();
+ const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
+ const fakeTab = { parentNode: {} };
+
+ // No routes exist, so a fresh routeUri() would yield "most-recent-space" and
+ // move nothing. The tab is still moved to TARGET_WS, proving onAfterAddTab
+ // routes purely from the precomputed result rather than recomputing.
+ gZenSpaceRoutingManager.onAfterAddTab(
+ "https://example.com",
+ fakeTab,
+ {},
+ win,
+ { targetRoute: TARGET_WS.uuid }
+ );
+
+ await TestUtils.waitForCondition(
+ () => win.gZenWorkspaces.moveCalls.length === 1,
+ "moveTabToWorkspace used the precomputed route"
+ );
+ Assert.equal(
+ win.gZenWorkspaces.moveCalls[0].uuid,
+ TARGET_WS.uuid,
+ "onAfterAddTab routes using the precomputed targetRoute, not a fresh routeUri()"
+ );
+});
+
+add_task(async function test_onAfterAddTab_ignores_detached_tab() {
+ clearAllRoutes();
+ const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
+
+ gZenSpaceRoutingManager.onAfterAddTab(
+ "https://github.com/zen",
+ { parentNode: null },
+ {},
+ win,
+ { targetRoute: TARGET_WS.uuid }
+ );
+ await flushEventLoop();
+
+ Assert.equal(
+ win.gZenWorkspaces.moveCalls.length,
+ 0,
+ "A detached tab (no parentNode) is never moved"
+ );
+});
+
+add_task(
+ async function test_onAfterAddTab_does_nothing_for_most_recent_space() {
+ clearAllRoutes();
+ const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
+
+ gZenSpaceRoutingManager.onAfterAddTab(
+ "https://example.com",
+ { parentNode: {} },
+ {},
+ win,
+ { targetRoute: "most-recent-space" }
+ );
+ await flushEventLoop();
+
+ Assert.equal(
+ win.gZenWorkspaces.moveCalls.length,
+ 0,
+ "A 'most-recent-space' route does not move the tab"
+ );
+ }
+);
+
+add_task(async function test_onAfterAddTab_does_nothing_when_skipped() {
+ clearAllRoutes();
+ const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
+
+ // onBeforeAddTab reports targetRoute null for skipped/unready tabs; without a
+ // route there is nothing for onAfterAddTab to do.
+ gZenSpaceRoutingManager.onAfterAddTab(
+ "https://github.com/zen",
+ { parentNode: {} },
+ {},
+ win,
+ { targetRoute: null }
+ );
+ await flushEventLoop();
+
+ Assert.equal(
+ win.gZenWorkspaces.moveCalls.length,
+ 0,
+ "A null targetRoute (skipped tab) is not routed"
+ );
+});
+
+add_task(async function test_onAfterAddTab_ignores_missing_before_result() {
+ clearAllRoutes();
+ const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] });
+
+ gZenSpaceRoutingManager.onAfterAddTab(
+ "https://github.com/zen",
+ { parentNode: {} },
+ {},
+ win
+ );
+ await flushEventLoop();
+
+ Assert.equal(
+ win.gZenWorkspaces.moveCalls.length,
+ 0,
+ "Without a beforeResult there is no precomputed route, so nothing is moved"
+ );
+});
+
+add_task(async function test_onAfterAddTab_activates_workspace_on_origin() {
+ clearAllRoutes();
+ await gZenWorkspaces.promiseInitialized;
+
+ await gZenWorkspaces.createAndSaveWorkspace("SR Origin Test");
+ const workspaces = gZenWorkspaces.getWorkspaces();
+ const target = workspaces[workspaces.length - 1];
+
+ const isOriginating =
+ window === Services.wm.getMostRecentWindow("navigator:browser");
+ ok(isOriginating, "Precondition: the test window is the most-recent window");
+
+ const ws = window.gZenWorkspaces;
+ const origMove = ws.moveTabToWorkspace;
+ const origChange = ws.changeWorkspace;
+ const origLastSelected = ws.lastSelectedWorkspaceTabs;
+
+ let moved = null;
+ let changedTo = null;
+ ws.lastSelectedWorkspaceTabs = {};
+ ws.moveTabToWorkspace = (tab, uuid) => {
+ moved = { tab, uuid };
+ };
+ ws.changeWorkspace = workspace => {
+ changedTo = workspace;
+ return Promise.resolve();
+ };
+
+ const tab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ skipAnimation: true,
+ skipRoute: true,
+ });
+
+ try {
+ gZenSpaceRoutingManager.onAfterAddTab(
+ "https://github.com/zen",
+ tab,
+ {},
+ window,
+ { targetRoute: target.uuid }
+ );
+
+ await TestUtils.waitForCondition(
+ () => moved,
+ "moveTabToWorkspace was called"
+ );
+ Assert.equal(moved.uuid, target.uuid, "Moved to the matched workspace");
+ Assert.equal(moved.tab, tab, "Moved the tab we passed in");
+
+ await TestUtils.waitForCondition(
+ () => changedTo,
+ "changeWorkspace was called on the originating window"
+ );
+ Assert.equal(
+ changedTo.uuid,
+ target.uuid,
+ "Activated the matched workspace"
+ );
+ Assert.equal(
+ ws.lastSelectedWorkspaceTabs[target.uuid],
+ tab,
+ "The moved tab is remembered as the workspace's last-selected tab"
+ );
+ } finally {
+ ws.moveTabToWorkspace = origMove;
+ ws.changeWorkspace = origChange;
+ ws.lastSelectedWorkspaceTabs = origLastSelected;
+ BrowserTestUtils.removeTab(tab);
+ await gZenWorkspaces.removeWorkspace(target.uuid);
+ }
+});
diff --git a/src/zen/tests/space_routing/browser_space_routing_redirect_navigation.js b/src/zen/tests/space_routing/browser_space_routing_redirect_navigation.js
new file mode 100644
index 000000000..ba22536d1
--- /dev/null
+++ b/src/zen/tests/space_routing/browser_space_routing_redirect_navigation.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Exercises nsZenSpaceRoutingManager.shouldRedirectNavigation: an in-place
+// navigation is only redirected into a new tab when its rule points at a space
+// that differs from the one the navigating tab already lives in.
+
+const TARGET_WS = { uuid: "ws-target", containerTabId: 7 };
+
+add_setup(async function () {
+ clearAllRoutes();
+ registerCleanupFunction(() => clearAllRoutes());
+});
+
+add_task(async function test_redirect_when_route_targets_other_space() {
+ clearAllRoutes();
+ addRoute({
+ reference: "github.com",
+ matchType: "contains",
+ openIn: TARGET_WS.uuid,
+ });
+ const win = makeFakeWindow({ workspaces: [TARGET_WS] });
+
+ ok(
+ gZenSpaceRoutingManager.shouldRedirectNavigation(
+ "https://github.com/zen",
+ "ws-current",
+ win
+ ),
+ "Navigating to a routed site from a different space redirects"
+ );
+});
+
+add_task(async function test_no_redirect_when_already_in_target_space() {
+ clearAllRoutes();
+ addRoute({
+ reference: "github.com",
+ matchType: "contains",
+ openIn: TARGET_WS.uuid,
+ });
+ const win = makeFakeWindow({ workspaces: [TARGET_WS] });
+
+ ok(
+ !gZenSpaceRoutingManager.shouldRedirectNavigation(
+ "https://github.com/zen",
+ TARGET_WS.uuid,
+ win
+ ),
+ "Already in the destination space navigates in place (and avoids a loop)"
+ );
+});
+
+add_task(async function test_no_redirect_when_no_rule_matches() {
+ clearAllRoutes();
+ const win = makeFakeWindow({ workspaces: [TARGET_WS] });
+
+ ok(
+ !gZenSpaceRoutingManager.shouldRedirectNavigation(
+ "https://example.com",
+ "ws-current",
+ win
+ ),
+ "An unmatched URL is never redirected"
+ );
+});
+
+add_task(async function test_no_redirect_when_rule_targets_most_recent() {
+ clearAllRoutes();
+ addRoute({
+ reference: "github.com",
+ matchType: "contains",
+ openIn: "most-recent-space",
+ });
+ const win = makeFakeWindow({ workspaces: [TARGET_WS] });
+
+ ok(
+ !gZenSpaceRoutingManager.shouldRedirectNavigation(
+ "https://github.com",
+ "ws-current",
+ win
+ ),
+ "A rule that opens in the most recent space is not redirected"
+ );
+});
+
+add_task(async function test_no_redirect_when_target_workspace_missing() {
+ clearAllRoutes();
+ addRoute({
+ reference: "github.com",
+ matchType: "contains",
+ openIn: "ws-does-not-exist",
+ });
+ const win = makeFakeWindow({ workspaces: [TARGET_WS] });
+
+ ok(
+ !gZenSpaceRoutingManager.shouldRedirectNavigation(
+ "https://github.com",
+ "ws-current",
+ win
+ ),
+ "A rule pointing at a missing workspace is not redirected"
+ );
+});
+
+add_task(async function test_no_redirect_when_workspaces_disabled() {
+ clearAllRoutes();
+ addRoute({
+ reference: "github.com",
+ matchType: "contains",
+ openIn: TARGET_WS.uuid,
+ });
+ const win = makeFakeWindow({
+ workspaces: [TARGET_WS],
+ workspaceEnabled: false,
+ });
+
+ ok(
+ !gZenSpaceRoutingManager.shouldRedirectNavigation(
+ "https://github.com",
+ "ws-current",
+ win
+ ),
+ "Nothing is redirected when workspaces are disabled"
+ );
+});
diff --git a/src/zen/tests/space_routing/browser_space_routing_route_matching.js b/src/zen/tests/space_routing/browser_space_routing_route_matching.js
new file mode 100644
index 000000000..5db7a422c
--- /dev/null
+++ b/src/zen/tests/space_routing/browser_space_routing_route_matching.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_contains_is_case_insensitive_substring() {
+ const route = { reference: "GitHub", matchType: "contains" };
+
+ ok(
+ gZenSpaceRoutingManager.isRouteMatching("https://github.com/zen", route),
+ "'contains' matches a substring regardless of case"
+ );
+ ok(
+ gZenSpaceRoutingManager.isRouteMatching("https://api.GITHUB.com/v3", route),
+ "'contains' matches when the URL casing differs from the reference"
+ );
+ ok(
+ !gZenSpaceRoutingManager.isRouteMatching("https://gitlab.com/zen", route),
+ "'contains' rejects a URL that does not include the reference"
+ );
+});
+
+add_task(async function test_equal_to_normalizes_protocol_and_www() {
+ const route = { reference: "github.com", matchType: "equal-to" };
+
+ ok(
+ gZenSpaceRoutingManager.isRouteMatching("https://www.github.com/", route),
+ "'equal-to' ignores https://, www. and a trailing slash"
+ );
+ ok(
+ gZenSpaceRoutingManager.isRouteMatching("HTTP://GitHub.com", route),
+ "'equal-to' is case-insensitive and strips http://"
+ );
+ ok(
+ !gZenSpaceRoutingManager.isRouteMatching("https://github.com/zen", route),
+ "'equal-to' does not match when a path is present (not an exact host)"
+ );
+ ok(
+ !gZenSpaceRoutingManager.isRouteMatching("https://notgithub.com", route),
+ "'equal-to' requires the whole normalized URL to be equal"
+ );
+});
+
+add_task(async function test_regex_match_is_case_sensitive_on_raw_uri() {
+ ok(
+ gZenSpaceRoutingManager.isRouteMatching("https://zen-browser.app", {
+ reference: "^https://.*\\.app$",
+ matchType: "regex",
+ }),
+ "'regex' matches against the raw URI"
+ );
+
+ ok(
+ !gZenSpaceRoutingManager.isRouteMatching("https://github.com", {
+ reference: "GitHub",
+ matchType: "regex",
+ }),
+ "'regex' is case-sensitive (no implicit lower-casing like 'contains')"
+ );
+});
+
+add_task(async function test_invalid_regex_is_swallowed() {
+ let threw = false;
+ let result;
+ try {
+ result = gZenSpaceRoutingManager.isRouteMatching(
+ "https://zen-browser.app",
+ {
+ reference: "([",
+ matchType: "regex",
+ }
+ );
+ } catch (e) {
+ threw = true;
+ }
+
+ ok(!threw, "An invalid regex does not throw out of isRouteMatching");
+ Assert.strictEqual(result, false, "An invalid regex never matches");
+});
+
+add_task(async function test_empty_reference_never_matches() {
+ for (const matchType of ["contains", "equal-to", "regex"]) {
+ ok(
+ !gZenSpaceRoutingManager.isRouteMatching("https://github.com", {
+ reference: "",
+ matchType,
+ }),
+ `An empty reference never matches (${matchType})`
+ );
+ ok(
+ !gZenSpaceRoutingManager.isRouteMatching("https://github.com", {
+ reference: " ",
+ matchType,
+ }),
+ `A whitespace-only reference never matches (${matchType})`
+ );
+ }
+});
+
+add_task(async function test_unknown_match_type_does_not_match() {
+ ok(
+ !gZenSpaceRoutingManager.isRouteMatching("https://github.com", {
+ reference: "github.com",
+ matchType: "starts-with",
+ }),
+ "An unsupported match type falls through to no match"
+ );
+});
diff --git a/src/zen/tests/space_routing/browser_space_routing_route_uri.js b/src/zen/tests/space_routing/browser_space_routing_route_uri.js
new file mode 100644
index 000000000..2763116ef
--- /dev/null
+++ b/src/zen/tests/space_routing/browser_space_routing_route_uri.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async function () {
+ clearAllRoutes();
+ const savedDefault = gZenSpaceRoutingManager.getDefaultExternalRoute();
+ registerCleanupFunction(() => {
+ clearAllRoutes();
+ gZenSpaceRoutingManager.setDefaultExternalRoute(savedDefault);
+ });
+});
+
+add_task(async function test_no_match_returns_most_recent_space() {
+ clearAllRoutes();
+ addRoute({ reference: "github.com", matchType: "contains", openIn: "ws-1" });
+
+ Assert.equal(
+ gZenSpaceRoutingManager.routeUri("https://example.com", {}),
+ "most-recent-space",
+ "A non-matching, non-external URL routes to most-recent-space"
+ );
+});
+
+add_task(async function test_first_matching_route_wins() {
+ clearAllRoutes();
+ addRoute({ reference: "github", matchType: "contains", openIn: "ws-first" });
+ addRoute({ reference: "github", matchType: "contains", openIn: "ws-second" });
+
+ Assert.equal(
+ gZenSpaceRoutingManager.routeUri("https://github.com/zen", {}),
+ "ws-first",
+ "The openIn of the first matching route is returned, later matches ignored"
+ );
+});
+
+add_task(async function test_external_default_only_applies_without_match() {
+ clearAllRoutes();
+ gZenSpaceRoutingManager.setDefaultExternalRoute("ws-external");
+ addRoute({ reference: "github", matchType: "contains", openIn: "ws-rule" });
+
+ Assert.equal(
+ gZenSpaceRoutingManager.routeUri("https://github.com", {
+ fromExternal: true,
+ }),
+ "ws-rule",
+ "A matching rule wins even for external links"
+ );
+
+ Assert.equal(
+ gZenSpaceRoutingManager.routeUri("https://example.com", {
+ fromExternal: true,
+ }),
+ "ws-external",
+ "An unmatched external link uses the default external route"
+ );
+
+ Assert.equal(
+ gZenSpaceRoutingManager.routeUri("https://example.com", {
+ fromExternal: false,
+ }),
+ "most-recent-space",
+ "An unmatched internal link ignores the external default"
+ );
+});
diff --git a/src/zen/tests/space_routing/head.js b/src/zen/tests/space_routing/head.js
new file mode 100644
index 000000000..943fb364f
--- /dev/null
+++ b/src/zen/tests/space_routing/head.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { gZenSpaceRoutingManager } = ChromeUtils.importESModule(
+ "resource:///modules/zen/spacerouting/ZenSpaceRoutingManager.sys.mjs"
+);
+
+const SR_DIALOG_URI =
+ "chrome://browser/content/zen-components/windows/zen-space-routing.xhtml";
+
+function clearAllRoutes() {
+ for (const route of gZenSpaceRoutingManager.getAllRoutes()) {
+ gZenSpaceRoutingManager.removeRoute(route.id);
+ }
+}
+
+function addRoute({
+ reference = "",
+ openIn = "most-recent-space",
+ matchType = "contains",
+} = {}) {
+ const route = gZenSpaceRoutingManager.createNewRoute();
+ route.reference = reference;
+ route.openIn = openIn;
+ route.matchType = matchType;
+ gZenSpaceRoutingManager.updateRoute(route);
+ return route;
+}
+
+function makeFakeWindow({
+ ready = true,
+ workspaces = [],
+ workspaceEnabled = true,
+} = {}) {
+ return {
+ gZenStartup: { isReady: ready },
+ gZenWorkspaces: {
+ workspaceEnabled,
+ moveCalls: [],
+ changeCalls: [],
+ lastSelectedWorkspaceTabs: {},
+ getWorkspaceFromId(id) {
+ return workspaces.find(w => w.uuid === id) || null;
+ },
+ moveTabToWorkspace(tab, uuid) {
+ this.moveCalls.push({ tab, uuid });
+ },
+ changeWorkspace(workspace) {
+ this.changeCalls.push(workspace);
+ return Promise.resolve();
+ },
+ },
+ };
+}
+
+async function flushEventLoop() {
+ for (let i = 0; i < 5; i++) {
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+ }
+}
+
+async function openRoutingDialog() {
+ // openSpaceRoutingDialog() presents an in-window modal through gDialogBox, so
+ // the dialog is a subdialog rather than a separate top-level window.
+ const dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ null,
+ SR_DIALOG_URI,
+ { isSubDialog: true }
+ );
+ // gDialogBox.open() only resolves once the dialog is dismissed, so kick it off
+ // without awaiting and wait on the open notification instead.
+ executeSoon(() => gZenSpaceRoutingManager.openSpaceRoutingDialog(window));
+ const dialogWin = await dialogPromise;
+ await TestUtils.waitForCondition(
+ () => dialogWin.spaceroutingDialog?.initialized,
+ "Space Routing dialog finished initializing"
+ );
+ return dialogWin;
+}
+
+// Resolves once the gDialogBox subdialog has fully torn down. Use this instead
+// of BrowserTestUtils.domWindowClosed(), which only fires for separate
+// top-level windows and so never resolves for an in-window subdialog.
+function promiseRoutingDialogClosed() {
+ const container = document.getElementById("window-modal-dialog");
+ if (!container?.open) {
+ return Promise.resolve();
+ }
+ return BrowserTestUtils.waitForMutationCondition(
+ container,
+ { childList: true, attributes: true },
+ () => !container.hasChildNodes() && !container.open
+ );
+}
+
+async function closeRoutingDialog(dialogWin) {
+ const closed = promiseRoutingDialogClosed();
+ dialogWin.close();
+ await closed;
+}
diff --git a/src/zen/tests/spaces/browser.toml b/src/zen/tests/spaces/browser.toml
index 99c7db003..a6bdae3ce 100644
--- a/src/zen/tests/spaces/browser.toml
+++ b/src/zen/tests/spaces/browser.toml
@@ -26,6 +26,8 @@ support-files = [
["browser_private_mode_startup.js"]
+["browser_select_tab_switches_space.js"]
+
["browser_unload_all_other_spaces.js"]
["browser_workspace_bookmarks.js"]
diff --git a/src/zen/tests/spaces/browser_select_tab_switches_space.js b/src/zen/tests/spaces/browser_select_tab_switches_space.js
new file mode 100644
index 000000000..4fd738f17
--- /dev/null
+++ b/src/zen/tests/spaces/browser_select_tab_switches_space.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function fakeTab(workspaceId) {
+ return {
+ getAttribute(name) {
+ return name === "zen-workspace-id" ? workspaceId : null;
+ },
+ };
+}
+
+function withRecordedSwitch(fn) {
+ const calls = [];
+ gZenWorkspaces.changeWorkspaceWithID = id => {
+ calls.push(id);
+ };
+ try {
+ fn(calls);
+ } finally {
+ // Remove the own property so the prototype method shows through again.
+ delete gZenWorkspaces.changeWorkspaceWithID;
+ }
+}
+
+add_task(function test_switches_when_tab_in_other_space() {
+ withRecordedSwitch(calls => {
+ const otherSpace = gZenWorkspaces.activeWorkspace + "-different";
+ gZenWorkspaces.onBeforeTabSelect(fakeTab(otherSpace));
+ Assert.deepEqual(
+ calls,
+ [otherSpace],
+ "Selecting a tab from another space switches to that space"
+ );
+ });
+});
+
+add_task(function test_no_switch_when_tab_in_active_space() {
+ withRecordedSwitch(calls => {
+ const active = gZenWorkspaces.activeWorkspace;
+ Assert.ok(active, "Test relies on a non-empty active workspace");
+ gZenWorkspaces.onBeforeTabSelect(fakeTab(active));
+ Assert.deepEqual(
+ calls,
+ [],
+ "Selecting a tab already in the active space does not switch"
+ );
+ });
+});
+
+add_task(function test_no_switch_when_tab_has_no_space() {
+ withRecordedSwitch(calls => {
+ gZenWorkspaces.onBeforeTabSelect(fakeTab(null));
+ Assert.deepEqual(
+ calls,
+ [],
+ "A tab with no zen-workspace-id does not switch spaces"
+ );
+ });
+});
+
+add_task(function test_handles_missing_tab() {
+ withRecordedSwitch(calls => {
+ gZenWorkspaces.onBeforeTabSelect(null);
+ gZenWorkspaces.onBeforeTabSelect(undefined);
+ Assert.deepEqual(calls, [], "A missing tab is ignored without throwing");
+ });
+});
diff --git a/src/zen/tests/spaces/head.js b/src/zen/tests/spaces/head.js
index d8d160c20..4ecb614ee 100644
--- a/src/zen/tests/spaces/head.js
+++ b/src/zen/tests/spaces/head.js
@@ -528,7 +528,7 @@ function setScrollPosition(bc, x, y) {
content.addEventListener(
"mozvisualscroll",
function onScroll(event) {
- if (content.document.ownerGlobal.visualViewport == event.target) {
+ if (content.document.documentGlobal.visualViewport == event.target) {
content.removeEventListener("mozvisualscroll", onScroll, {
mozSystemGroup: true,
});
@@ -576,7 +576,7 @@ function setPropertyOfFormField(browserContext, selector, propName, newValue) {
node[propNameChild] = newValueChild;
let event = node.ownerDocument.createEvent("UIEvents");
- event.initUIEvent("input", true, true, node.ownerGlobal, 0);
+ event.initUIEvent("input", true, true, node.documentGlobal, 0);
node.dispatchEvent(event);
}
);
@@ -678,7 +678,7 @@ async function openTabMenuFor(tab) {
EventUtils.synthesizeMouseAtCenter(
tab,
{ type: "contextmenu" },
- tab.ownerGlobal
+ tab.documentGlobal
);
await tabMenuShown;
diff --git a/src/zen/tests/tabs/head.js b/src/zen/tests/tabs/head.js
index 76104f295..b4934d105 100644
--- a/src/zen/tests/tabs/head.js
+++ b/src/zen/tests/tabs/head.js
@@ -610,7 +610,7 @@ async function removeTabGroup(group) {
* @returns {Promise}
*/
async function getContextMenu(triggerNode, contextMenuId) {
- let win = triggerNode.ownerGlobal;
+ let win = triggerNode.documentGlobal;
triggerNode.scrollIntoView({ behavior: "instant" });
const contextMenu = win.document.getElementById(contextMenuId);
const contextMenuShown = BrowserTestUtils.waitForPopupEvent(
diff --git a/src/zen/tests/urlbar/head.js b/src/zen/tests/urlbar/head.js
index 29eaebf2c..18c5bbefe 100644
--- a/src/zen/tests/urlbar/head.js
+++ b/src/zen/tests/urlbar/head.js
@@ -19,28 +19,28 @@ function selectWithMouseDrag(fromX, toX, win = window) {
fromX,
rect.height / 2,
{ type: "mousemove" },
- target.ownerGlobal
+ target.documentGlobal
);
EventUtils.synthesizeMouse(
target,
fromX,
rect.height / 2,
{ type: "mousedown" },
- target.ownerGlobal
+ target.documentGlobal
);
EventUtils.synthesizeMouse(
target,
toX,
rect.height / 2,
{ type: "mousemove" },
- target.ownerGlobal
+ target.documentGlobal
);
EventUtils.synthesizeMouse(
target,
toX,
rect.height / 2,
{ type: "mouseup" },
- target.ownerGlobal
+ target.documentGlobal
);
return promise;
}
diff --git a/src/zen/tests/welcome/browser_welcome.js b/src/zen/tests/welcome/browser_welcome.js
index 54b74a8dc..aeedb2482 100644
--- a/src/zen/tests/welcome/browser_welcome.js
+++ b/src/zen/tests/welcome/browser_welcome.js
@@ -93,7 +93,6 @@ add_task(async function test_Welcome_Steps() {
"Welcome page content should have more than 3 essentials after clicking next action"
);
await EventUtils.synthesizeMouseAtCenter(essentials[0], {});
- await EventUtils.synthesizeMouseAtCenter(essentials[1], {});
await EventUtils.synthesizeMouseAtCenter(essentials[2], {});
ok(
diff --git a/src/zen/urlbar/ZenSiteDataPanel.sys.mjs b/src/zen/urlbar/ZenSiteDataPanel.sys.mjs
index 90e037b09..f781f134f 100644
--- a/src/zen/urlbar/ZenSiteDataPanel.sys.mjs
+++ b/src/zen/urlbar/ZenSiteDataPanel.sys.mjs
@@ -142,7 +142,9 @@ export class nsZenSiteDataPanel {
this.anchor.removeAttribute("boosting");
}
// Force a reflow to ensure the attribute change is applied before any potential animation.
- this.anchor.getBoundingClientRect();
+ if (this.unifiedPanel.state === "open") {
+ this.anchor.getBoundingClientRect();
+ }
}
#initCopyUrlButton() {
diff --git a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs
index 5fb7d1c4c..6814e1261 100644
--- a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs
+++ b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs
@@ -550,7 +550,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
const result = details.result;
const payload = result.payload;
const command = payload.zenCommand;
- const ownerGlobal = details.element.ownerGlobal;
+ const ownerGlobal = details.element.documentGlobal;
ownerGlobal.gBrowser.selectedBrowser.focus();
if (typeof command === "function") {
command(ownerGlobal);
diff --git a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs
index bfc789bb5..613c0073f 100644
--- a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs
+++ b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs
@@ -81,6 +81,11 @@ const globalActionsTemplate = [
return !tab.hasAttribute("zen-empty-tab") && tab.pinned;
},
},
+ {
+ label: "Open Space Routing",
+ command: "cmd_zenOpenSpaceRoutingSettings",
+ icon: "chrome://browser/skin/zen-icons/selectable/airplane.svg",
+ },
{
label: "New Boost",
icon: "chrome://browser/skin/zen-icons/boost.svg",
diff --git a/src/zen/welcome/ZenWelcome.mjs b/src/zen/welcome/ZenWelcome.mjs
index 533657b4d..d57446cc2 100644
--- a/src/zen/welcome/ZenWelcome.mjs
+++ b/src/zen/welcome/ZenWelcome.mjs
@@ -675,7 +675,7 @@
.appendChild(anchor);
gZenThemePicker.panel.setAttribute("noautohide", "true");
gZenThemePicker.panel.setAttribute("consumeoutsideclicks", "false");
- gZenThemePicker.panel.setAttribute("nonnativepopover", "true");
+ gZenThemePicker.panel.setAttribute("nonnative", "");
gZenThemePicker.panel.addEventListener(
"popupshowing",
() => {
@@ -694,7 +694,7 @@
async fadeOut() {
gZenThemePicker.panel.removeAttribute("noautohide");
gZenThemePicker.panel.removeAttribute("consumeoutsideclicks");
- gZenThemePicker.panel.removeAttribute("nonnativepopover");
+ gZenThemePicker.panel.removeAttribute("nonnative");
await animate(gZenThemePicker.panel, { opacity: [1, 0] });
gZenThemePicker.panel.hidePopup();
gZenThemePicker.panel.removeAttribute("style");
diff --git a/src/zen/zen.globals.mjs b/src/zen/zen.globals.mjs
index 058a4d413..204cf37b7 100644
--- a/src/zen/zen.globals.mjs
+++ b/src/zen/zen.globals.mjs
@@ -40,6 +40,8 @@ export default [
"gZenViewSplitter",
+ "gZenSpaceRoutingManager",
+
"Ci",
"Cu",
"Cc",
diff --git a/surfer.json b/surfer.json
index 479ccbe91..b1104a111 100644
--- a/surfer.json
+++ b/surfer.json
@@ -5,8 +5,8 @@
"binaryName": "zen",
"version": {
"product": "firefox",
- "version": "151.0.3",
- "candidate": "151.0.3",
+ "version": "152.0.1",
+ "candidate": "152.0.1",
"candidateBuild": 1
},
"buildOptions": {
@@ -20,7 +20,7 @@
"brandShortName": "Zen",
"brandFullName": "Zen Browser",
"release": {
- "displayVersion": "1.20.2b",
+ "displayVersion": "1.21.3b",
"github": {
"repo": "zen-browser/desktop"
},
@@ -40,7 +40,7 @@
"brandShortName": "Twilight",
"brandFullName": "Zen Twilight",
"release": {
- "displayVersion": "1.21t",
+ "displayVersion": "1.22t",
"github": {
"repo": "zen-browser/desktop"
}