summaryrefslogtreecommitdiffstats
path: root/browser/components/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /browser/components/tests
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/tests')
-rw-r--r--browser/components/tests/browser/browser.toml40
-rw-r--r--browser/components/tests/browser/browser_browserGlue_showModal_trigger.js47
-rw-r--r--browser/components/tests/browser/browser_browserGlue_telemetry.js114
-rw-r--r--browser/components/tests/browser/browser_browserGlue_upgradeDialog_trigger.js204
-rw-r--r--browser/components/tests/browser/browser_bug538331.js228
-rw-r--r--browser/components/tests/browser/browser_contentpermissionprompt.js175
-rw-r--r--browser/components/tests/browser/browser_default_bookmark_toolbar_visibility.js89
-rw-r--r--browser/components/tests/browser/browser_default_browser_prompt.js125
-rw-r--r--browser/components/tests/browser/browser_initial_tab_remoteType.js156
-rw-r--r--browser/components/tests/browser/browser_quit_disabled.js62
-rw-r--r--browser/components/tests/browser/browser_quit_multiple_tabs.js110
-rw-r--r--browser/components/tests/browser/browser_quit_shortcut_warning.js54
-rw-r--r--browser/components/tests/browser/browser_startup_homepage.js121
-rw-r--r--browser/components/tests/browser/browser_system_notification_telemetry.js54
-rw-r--r--browser/components/tests/browser/browser_to_handle_telemetry.js199
-rw-r--r--browser/components/tests/browser/head.js90
-rw-r--r--browser/components/tests/browser/whats_new_page/active-update.xml1
-rw-r--r--browser/components/tests/browser/whats_new_page/browser.toml21
-rw-r--r--browser/components/tests/browser/whats_new_page/browser_whats_new_page.js108
-rw-r--r--browser/components/tests/browser/whats_new_page/config_localhost_update_url.json5
-rw-r--r--browser/components/tests/browser/whats_new_page/updates/0/update.status1
-rw-r--r--browser/components/tests/marionette/manifest.toml4
-rw-r--r--browser/components/tests/marionette/test_no_errors_clean_profile.py186
-rw-r--r--browser/components/tests/unit/distribution.ini60
-rw-r--r--browser/components/tests/unit/head.js8
-rw-r--r--browser/components/tests/unit/test_browserGlue_migration_ctrltab_recently_used_order.js111
-rw-r--r--browser/components/tests/unit/test_browserGlue_migration_formautofill.js142
-rw-r--r--browser/components/tests/unit/test_browserGlue_migration_no_errors.js34
-rw-r--r--browser/components/tests/unit/test_browserGlue_migration_places_xulstore.js54
-rw-r--r--browser/components/tests/unit/test_browserGlue_migration_remove_pref.js26
-rw-r--r--browser/components/tests/unit/test_browserGlue_migration_resetDefaults.js109
-rw-r--r--browser/components/tests/unit/test_distribution.js202
-rw-r--r--browser/components/tests/unit/test_distribution_cachedexistence.js131
-rw-r--r--browser/components/tests/unit/xpcshell.toml22
34 files changed, 3093 insertions, 0 deletions
diff --git a/browser/components/tests/browser/browser.toml b/browser/components/tests/browser/browser.toml
new file mode 100644
index 0000000000..ffb3012e72
--- /dev/null
+++ b/browser/components/tests/browser/browser.toml
@@ -0,0 +1,40 @@
+[DEFAULT]
+support-files = [
+ "head.js",
+ "../../../../dom/security/test/csp/dummy.pdf",
+]
+
+["browser_browserGlue_showModal_trigger.js"]
+
+["browser_browserGlue_telemetry.js"]
+
+["browser_browserGlue_upgradeDialog_trigger.js"]
+
+["browser_bug538331.js"]
+skip-if = ["!updater"]
+reason = "test depends on update channel"
+
+["browser_contentpermissionprompt.js"]
+
+["browser_default_bookmark_toolbar_visibility.js"]
+
+["browser_default_browser_prompt.js"]
+
+["browser_initial_tab_remoteType.js"]
+https_first_disabled = true
+
+["browser_quit_disabled.js"]
+# On macOS we can't change browser.quitShortcut.disabled during runtime.
+skip-if = ["os == 'mac'"]
+
+["browser_quit_multiple_tabs.js"]
+
+["browser_quit_shortcut_warning.js"]
+
+["browser_startup_homepage.js"]
+
+["browser_system_notification_telemetry.js"]
+run-if = ["os == 'win'"]
+
+["browser_to_handle_telemetry.js"]
+run-if = ["os == 'win'"]
diff --git a/browser/components/tests/browser/browser_browserGlue_showModal_trigger.js b/browser/components/tests/browser/browser_browserGlue_showModal_trigger.js
new file mode 100644
index 0000000000..eb753bf796
--- /dev/null
+++ b/browser/components/tests/browser/browser_browserGlue_showModal_trigger.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetters(this, {
+ BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"],
+});
+
+async function showAboutWelcomeModal() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.aboutwelcome.showModal", true]],
+ });
+
+ BrowserHandler.firstRunProfile = true;
+
+ const data = [
+ {
+ id: "TEST_SCREEN",
+ content: {
+ position: "split",
+ logo: {},
+ title: "test",
+ },
+ },
+ ];
+
+ return {
+ data,
+ async cleanup() {
+ await SpecialPowers.popPrefEnv();
+ BrowserHandler.firstRunProfile = false;
+ },
+ };
+}
+
+add_task(async function show_about_welcome_modal() {
+ const { data } = await showAboutWelcomeModal();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.aboutwelcome.screens", JSON.stringify(data)]],
+ });
+ BROWSER_GLUE._maybeShowDefaultBrowserPrompt();
+ const [win] = await TestUtils.topicObserved("subdialog-loaded");
+ const modal = win.document.querySelector(".onboardingContainer");
+ ok(!!modal, "About Welcome modal shown");
+ win.close();
+});
diff --git a/browser/components/tests/browser/browser_browserGlue_telemetry.js b/browser/components/tests/browser/browser_browserGlue_telemetry.js
new file mode 100644
index 0000000000..3320d0b361
--- /dev/null
+++ b/browser/components/tests/browser/browser_browserGlue_telemetry.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that telemetry reports Firefox is not pinned on any OS at startup.
+add_task(function check_startup_pinned_telemetry() {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+
+ // Check the appropriate telemetry is set or not reported by platform.
+ switch (AppConstants.platform) {
+ case "win":
+ if (
+ AppConstants.platform === "win" &&
+ Services.sysinfo.getProperty("hasWinPackageId")
+ ) {
+ TelemetryTestUtils.assertScalarUnset(
+ scalars,
+ "os.environment.is_taskbar_pinned"
+ );
+ TelemetryTestUtils.assertScalarUnset(
+ scalars,
+ "os.environment.is_taskbar_pinned_private"
+ );
+ } else {
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "os.environment.is_taskbar_pinned",
+ false,
+ "Pin set on win"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "os.environment.is_taskbar_pinned_private",
+ false,
+ "Pin private set on win"
+ );
+ }
+ TelemetryTestUtils.assertScalarUnset(
+ scalars,
+ "os.environment.is_kept_in_dock"
+ );
+ break;
+ case "macosx":
+ TelemetryTestUtils.assertScalarUnset(
+ scalars,
+ "os.environment.is_taskbar_pinned"
+ );
+ TelemetryTestUtils.assertScalarUnset(
+ scalars,
+ "os.environment.is_taskbar_pinned_private"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "os.environment.is_kept_in_dock",
+ false,
+ "Dock set on mac"
+ );
+ break;
+ default:
+ TelemetryTestUtils.assertScalarUnset(
+ scalars,
+ "os.environment.is_taskbar_pinned"
+ );
+ TelemetryTestUtils.assertScalarUnset(
+ scalars,
+ "os.environment.is_taskbar_pinned_private"
+ );
+ TelemetryTestUtils.assertScalarUnset(
+ scalars,
+ "os.environment.is_kept_in_dock"
+ );
+ break;
+ }
+});
+
+// Check that telemetry reports whether Firefox is the default PDF handler.
+// This is safe without any explicit coordination because idle tasks are
+// guaranteed to have been invokedbefore the test harness invokes the test. See
+// https://searchfox.org/mozilla-central/rev/1674b86019a96f076e0f98f1d0f5f3ab9d4e9020/browser/components/BrowserGlue.jsm#2320-2324
+// and
+// https://searchfox.org/mozilla-central/rev/1674b86019a96f076e0f98f1d0f5f3ab9d4e9020/browser/base/content/browser.js#2364.
+add_task(function check_is_default_handler_telemetry() {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+
+ // Check the appropriate telemetry is set or not reported by platform.
+ switch (AppConstants.platform) {
+ case "win":
+ // We should always set whether we're the default PDF handler.
+ Assert.ok("os.environment.is_default_handler" in scalars);
+ Assert.deepEqual(
+ [".pdf"],
+ Object.keys(scalars["os.environment.is_default_handler"])
+ );
+
+ if (Cu.isInAutomation) {
+ // But only in automation can we assume we're not the default handler.
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "os.environment.is_default_handler",
+ ".pdf",
+ false,
+ "Not default PDF handler on Windows"
+ );
+ }
+ break;
+ default:
+ TelemetryTestUtils.assertScalarUnset(
+ scalars,
+ "os.environment.is_default_handler"
+ );
+ break;
+ }
+});
diff --git a/browser/components/tests/browser/browser_browserGlue_upgradeDialog_trigger.js b/browser/components/tests/browser/browser_browserGlue_upgradeDialog_trigger.js
new file mode 100644
index 0000000000..88004525c8
--- /dev/null
+++ b/browser/components/tests/browser/browser_browserGlue_upgradeDialog_trigger.js
@@ -0,0 +1,204 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ExperimentFakes } = ChromeUtils.importESModule(
+ "resource://testing-common/NimbusTestUtils.sys.mjs"
+);
+const { ExperimentAPI } = ChromeUtils.importESModule(
+ "resource://nimbus/ExperimentAPI.sys.mjs"
+);
+const { OnboardingMessageProvider } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/OnboardingMessageProvider.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetters(this, {
+ BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"],
+});
+
+add_setup(() => {
+ Services.telemetry.clearEvents();
+});
+
+async function forceMajorUpgrade() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.startup.homepage_override.mstone", "88.0"]],
+ });
+
+ void BrowserHandler.defaultArgs;
+
+ return async () => {
+ await SpecialPowers.popPrefEnv();
+ BrowserHandler.majorUpgrade = false;
+ Services.prefs.clearUserPref("browser.startup.upgradeDialog.version");
+ };
+}
+
+add_task(async function not_major_upgrade() {
+ await BROWSER_GLUE._maybeShowDefaultBrowserPrompt();
+
+ AssertEvents("Not major upgrade for upgrade dialog requirements", [
+ "trigger",
+ "reason",
+ "not-major",
+ ]);
+});
+
+add_task(async function remote_disabled() {
+ await ExperimentAPI.ready();
+ let doCleanup = await ExperimentFakes.enrollWithRollout({
+ featureId: NimbusFeatures.upgradeDialog.featureId,
+ value: {
+ enabled: false,
+ },
+ });
+
+ // Simulate starting from a previous version.
+ let cleanupUpgrade = await forceMajorUpgrade();
+
+ await BROWSER_GLUE._maybeShowDefaultBrowserPrompt();
+
+ AssertEvents("Feature disabled for upgrade dialog requirements", [
+ "trigger",
+ "reason",
+ "disabled",
+ ]);
+
+ await doCleanup();
+ await cleanupUpgrade();
+});
+
+add_task(async function enterprise_disabled() {
+ const defaultPrefs = Services.prefs.getDefaultBranch("");
+ const pref = "browser.aboutwelcome.enabled";
+ const orig = defaultPrefs.getBoolPref(pref, true);
+ defaultPrefs.setBoolPref(pref, false);
+
+ let cleanupUpgrade = await forceMajorUpgrade();
+
+ await BROWSER_GLUE._maybeShowDefaultBrowserPrompt();
+
+ AssertEvents("Welcome disabled like enterprise policy", [
+ "trigger",
+ "reason",
+ "no-welcome",
+ ]);
+
+ await cleanupUpgrade();
+ defaultPrefs.setBoolPref(pref, orig);
+});
+
+add_task(async function show_major_upgrade() {
+ const defaultPrefs = Services.prefs.getDefaultBranch("");
+ const pref = "browser.startup.upgradeDialog.enabled";
+ const orig = defaultPrefs.getBoolPref(pref, true);
+ defaultPrefs.setBoolPref(pref, true);
+
+ let cleanupUpgrade = await forceMajorUpgrade();
+
+ await BROWSER_GLUE._maybeShowDefaultBrowserPrompt();
+ const [win] = await TestUtils.topicObserved("subdialog-loaded");
+ const data = await OnboardingMessageProvider.getUpgradeMessage();
+ Assert.equal(data.id, "FX_MR_106_UPGRADE", "MR 106 Upgrade Dialog Shown");
+ win.close();
+
+ AssertEvents("Upgrade dialog opened from major upgrade", [
+ "trigger",
+ "reason",
+ "satisfied",
+ ]);
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ await BROWSER_GLUE._maybeShowDefaultBrowserPrompt();
+
+ AssertEvents("Shouldn't reshow for upgrade dialog requirements", [
+ "trigger",
+ "reason",
+ "already-shown",
+ ]);
+
+ defaultPrefs.setBoolPref(pref, orig);
+ await cleanupUpgrade();
+});
+
+add_task(async function test_mr2022_upgradeDialogEnabled() {
+ const FALLBACK_PREF = "browser.startup.upgradeDialog.enabled";
+
+ async function runMajorReleaseTest(
+ { onboarding = undefined, enabled = undefined, fallbackPref = undefined },
+ expected
+ ) {
+ info("Testing upgradeDialog with:");
+ info(` majorRelease2022.onboarding=${onboarding}`);
+ info(` upgradeDialog.enabled=${enabled}`);
+ info(` ${FALLBACK_PREF}=${fallbackPref}`);
+
+ let mr2022Cleanup = async () => {};
+ let upgradeDialogCleanup = async () => {};
+
+ if (typeof onboarding !== "undefined") {
+ mr2022Cleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "majorRelease2022",
+ value: { onboarding },
+ });
+ }
+
+ if (typeof enabled !== "undefined") {
+ upgradeDialogCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "upgradeDialog",
+ value: { enabled },
+ });
+ }
+
+ if (typeof fallbackPref !== "undefined") {
+ await SpecialPowers.pushPrefEnv({
+ set: [[FALLBACK_PREF, fallbackPref]],
+ });
+ }
+
+ const cleanupForcedUpgrade = await forceMajorUpgrade();
+
+ try {
+ await BROWSER_GLUE._maybeShowDefaultBrowserPrompt();
+ AssertEvents(`Upgrade dialog ${expected ? "shown" : "not shown"}`, [
+ "trigger",
+ "reason",
+ expected ? "satisfied" : "disabled",
+ ]);
+
+ if (expected) {
+ const [win] = await TestUtils.topicObserved("subdialog-loaded");
+ win.close();
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ } finally {
+ await cleanupForcedUpgrade();
+ if (typeof fallbackPref !== "undefined") {
+ await SpecialPowers.popPrefEnv();
+ }
+ await upgradeDialogCleanup();
+ await mr2022Cleanup();
+ }
+ }
+
+ await runMajorReleaseTest({ onboarding: true }, true);
+ await runMajorReleaseTest({ onboarding: true, enabled: false }, true);
+ await runMajorReleaseTest({ onboarding: true, fallbackPref: false }, true);
+
+ await runMajorReleaseTest({ onboarding: false }, false);
+ await runMajorReleaseTest({ onboarding: false, enabled: true }, false);
+ await runMajorReleaseTest({ onboarding: false, fallbackPref: true }, false);
+
+ await runMajorReleaseTest({ enabled: true }, true);
+ await runMajorReleaseTest({ enabled: true, fallbackPref: false }, true);
+ await runMajorReleaseTest({ fallbackPref: true }, true);
+
+ await runMajorReleaseTest({ enabled: false }, false);
+ await runMajorReleaseTest({ enabled: false, fallbackPref: true }, false);
+ await runMajorReleaseTest({ fallbackPref: false }, false);
+
+ // Test the default configuration.
+ await runMajorReleaseTest({}, false);
+});
diff --git a/browser/components/tests/browser/browser_bug538331.js b/browser/components/tests/browser/browser_bug538331.js
new file mode 100644
index 0000000000..874b4aecbe
--- /dev/null
+++ b/browser/components/tests/browser/browser_bug538331.js
@@ -0,0 +1,228 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const PREF_MSTONE = "browser.startup.homepage_override.mstone";
+const PREF_OVERRIDE_URL = "startup.homepage_override_url";
+
+const DEFAULT_PREF_URL = "http://pref.example.com/";
+const DEFAULT_UPDATE_URL = "http://example.com/";
+
+const XML_EMPTY =
+ '<?xml version="1.0"?><updates xmlns=' +
+ '"http://www.mozilla.org/2005/app-update"></updates>';
+
+const XML_PREFIX =
+ '<updates xmlns="http://www.mozilla.org/2005/app-update"' +
+ '><update appVersion="1.0" buildID="20080811053724" ' +
+ 'channel="nightly" displayVersion="Version 1.0" ' +
+ 'installDate="1238441400314" isCompleteUpdate="true" ' +
+ 'name="Update Test 1.0" type="minor" detailsURL=' +
+ '"http://example.com/" previousAppVersion="1.0" ' +
+ 'serviceURL="https://example.com/" ' +
+ 'statusText="The Update was successfully installed" ' +
+ 'foregroundDownload="true"';
+
+const XML_SUFFIX =
+ '><patch type="complete" URL="http://example.com/" ' +
+ 'size="775" selected="true" state="succeeded"/>' +
+ "</update></updates>";
+
+// nsBrowserContentHandler.js defaultArgs tests
+const BCH_TESTS = [
+ {
+ description: "no mstone change and no update",
+ noMstoneChange: true,
+ },
+ {
+ description: "mstone changed and no update",
+ prefURL: DEFAULT_PREF_URL,
+ },
+ {
+ description: "no mstone change and update with 'showURL' for actions",
+ actions: "showURL",
+ noMstoneChange: true,
+ },
+ {
+ description: "update without actions",
+ prefURL: DEFAULT_PREF_URL,
+ },
+ {
+ description: "update with 'showURL' for actions",
+ actions: "showURL",
+ prefURL: DEFAULT_PREF_URL,
+ },
+ {
+ description: "update with 'showURL' for actions and openURL",
+ actions: "showURL",
+ openURL: DEFAULT_UPDATE_URL,
+ },
+ {
+ description: "update with 'extra showURL' for actions",
+ actions: "extra showURL",
+ prefURL: DEFAULT_PREF_URL,
+ },
+ {
+ description: "update with 'extra showURL' for actions and openURL",
+ actions: "extra showURL",
+ openURL: DEFAULT_UPDATE_URL,
+ },
+ {
+ description: "update with 'silent' for actions",
+ actions: "silent",
+ },
+ {
+ description: "update with 'silent showURL extra' for actions and openURL",
+ actions: "silent showURL extra",
+ },
+];
+
+add_task(async function test_bug538331() {
+ // Reset the startup page pref since it may have been set by other tests
+ // and we will assume it is (non-test) default.
+ await SpecialPowers.pushPrefEnv({
+ clear: [["browser.startup.page"]],
+ });
+
+ let originalMstone = Services.prefs.getCharPref(PREF_MSTONE);
+
+ // Set the preferences needed for the test: they will be cleared up
+ // after it runs.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_MSTONE, originalMstone],
+ [PREF_OVERRIDE_URL, DEFAULT_PREF_URL],
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ let activeUpdateFile = getActiveUpdateFile();
+ activeUpdateFile.remove(false);
+ reloadUpdateManagerData(true);
+ });
+
+ // Clear any pre-existing override in defaultArgs that are hanging around.
+ // This will also set the browser.startup.homepage_override.mstone preference
+ // if it isn't already set.
+ Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler).defaultArgs;
+
+ for (let i = 0; i < BCH_TESTS.length; i++) {
+ let testCase = BCH_TESTS[i];
+ ok(
+ true,
+ "Test nsBrowserContentHandler " + (i + 1) + ": " + testCase.description
+ );
+
+ if (testCase.actions) {
+ let actionsXML = ' actions="' + testCase.actions + '"';
+ if (testCase.openURL) {
+ actionsXML += ' openURL="' + testCase.openURL + '"';
+ }
+ writeUpdatesToXMLFile(XML_PREFIX + actionsXML + XML_SUFFIX);
+ } else {
+ writeUpdatesToXMLFile(XML_EMPTY);
+ }
+
+ reloadUpdateManagerData(false);
+
+ let noOverrideArgs = Cc["@mozilla.org/browser/clh;1"].getService(
+ Ci.nsIBrowserHandler
+ ).defaultArgs;
+
+ let overrideArgs = "";
+ if (testCase.prefURL) {
+ overrideArgs = testCase.prefURL;
+ } else if (testCase.openURL) {
+ overrideArgs = testCase.openURL;
+ }
+
+ if (overrideArgs == "" && noOverrideArgs) {
+ overrideArgs = noOverrideArgs;
+ } else if (noOverrideArgs) {
+ overrideArgs += "|" + noOverrideArgs;
+ }
+
+ if (testCase.noMstoneChange === undefined) {
+ Services.prefs.setCharPref(PREF_MSTONE, "PreviousMilestone");
+ }
+
+ let defaultArgs = Cc["@mozilla.org/browser/clh;1"].getService(
+ Ci.nsIBrowserHandler
+ ).defaultArgs;
+ is(defaultArgs, overrideArgs, "correct value returned by defaultArgs");
+
+ if (testCase.noMstoneChange === undefined || !testCase.noMstoneChange) {
+ let newMstone = Services.prefs.getCharPref(PREF_MSTONE);
+ is(
+ originalMstone,
+ newMstone,
+ "preference " + PREF_MSTONE + " should have been updated"
+ );
+ }
+ }
+});
+
+/**
+ * Removes the updates.xml file and returns the nsIFile for the
+ * active-update.xml file.
+ *
+ * @return The nsIFile for the active-update.xml file.
+ */
+function getActiveUpdateFile() {
+ let updateRootDir = Services.dirsvc.get("UpdRootD", Ci.nsIFile);
+ let updatesFile = updateRootDir.clone();
+ updatesFile.append("updates.xml");
+ if (updatesFile.exists()) {
+ // The following is non-fatal.
+ try {
+ updatesFile.remove(false);
+ } catch (e) {}
+ }
+ let activeUpdateFile = updateRootDir.clone();
+ activeUpdateFile.append("active-update.xml");
+ return activeUpdateFile;
+}
+
+/**
+ * Reloads the update xml files.
+ *
+ * @param skipFiles (optional)
+ * If true, the update xml files will not be read and the metadata will
+ * be reset. If false (the default), the update xml files will be read
+ * to populate the update metadata.
+ */
+function reloadUpdateManagerData(skipFiles = false) {
+ Cc["@mozilla.org/updates/update-manager;1"]
+ .getService(Ci.nsIUpdateManager)
+ .QueryInterface(Ci.nsIObserver)
+ .observe(null, "um-reload-update-data", skipFiles ? "skip-files" : "");
+}
+
+/**
+ * Writes the updates specified to the active-update.xml file.
+ *
+ * @param aText
+ * The updates represented as a string to write to the active-update.xml
+ * file.
+ */
+function writeUpdatesToXMLFile(aText) {
+ const PERMS_FILE = 0o644;
+
+ const MODE_WRONLY = 0x02;
+ const MODE_CREATE = 0x08;
+ const MODE_TRUNCATE = 0x20;
+
+ let activeUpdateFile = getActiveUpdateFile();
+ if (!activeUpdateFile.exists()) {
+ activeUpdateFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
+ }
+ let fos = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ let flags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE;
+ fos.init(activeUpdateFile, flags, PERMS_FILE, 0);
+ fos.write(aText, aText.length);
+ fos.close();
+}
diff --git a/browser/components/tests/browser/browser_contentpermissionprompt.js b/browser/components/tests/browser/browser_contentpermissionprompt.js
new file mode 100644
index 0000000000..3e2eb24f62
--- /dev/null
+++ b/browser/components/tests/browser/browser_contentpermissionprompt.js
@@ -0,0 +1,175 @@
+/**
+ * These tests test nsBrowserGlue's nsIContentPermissionPrompt
+ * implementation behaviour with various types of
+ * nsIContentPermissionRequests.
+ */
+
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "ContentPermissionPrompt",
+ "@mozilla.org/content-permission/prompt;1",
+ "nsIContentPermissionPrompt"
+);
+
+/**
+ * This is a partial implementation of nsIContentPermissionType.
+ *
+ * @param {string} type
+ * The string defining what type of permission is being requested.
+ * Example: "geo", "desktop-notification".
+ * @return nsIContentPermissionType implementation.
+ */
+function MockContentPermissionType(type) {
+ this.type = type;
+}
+
+MockContentPermissionType.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionType"]),
+ // We expose the wrappedJSObject so that we can be sure
+ // in some of our tests that we're passing the right
+ // nsIContentPermissionType around.
+ wrappedJSObject: this,
+};
+
+/**
+ * This is a partial implementation of nsIContentPermissionRequest.
+ *
+ * @param {Array<nsIContentPermissionType>} typesArray
+ * The types to assign to this nsIContentPermissionRequest,
+ * in order. You probably want to use MockContentPermissionType.
+ * @return nsIContentPermissionRequest implementation.
+ */
+function MockContentPermissionRequest(typesArray) {
+ this.types = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ for (let type of typesArray) {
+ this.types.appendElement(type);
+ }
+}
+
+MockContentPermissionRequest.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionRequest"]),
+ // We expose the wrappedJSObject so that we can be sure
+ // in some of our tests that we're passing the right
+ // nsIContentPermissionRequest around.
+ wrappedJSObject: this,
+ // For some of our tests, we want to make sure that the
+ // request is cancelled, so we add some instrumentation here
+ // to check that cancel() is called.
+ cancel() {
+ this.cancelled = true;
+ },
+ cancelled: false,
+ principal: Services.scriptSecurityManager.getSystemPrincipal(),
+};
+
+/**
+ * Tests that if the nsIContentPermissionRequest has an empty
+ * types array, that NS_ERROR_UNEXPECTED is thrown, and the
+ * request is cancelled.
+ */
+add_task(async function test_empty_types() {
+ let mockRequest = new MockContentPermissionRequest([]);
+ Assert.throws(
+ () => {
+ ContentPermissionPrompt.prompt(mockRequest);
+ },
+ /NS_ERROR_UNEXPECTED/,
+ "Should have thrown NS_ERROR_UNEXPECTED."
+ );
+ Assert.ok(mockRequest.cancelled, "Should have cancelled the request.");
+});
+
+/**
+ * Tests that if the nsIContentPermissionRequest has more than
+ * one type, that NS_ERROR_UNEXPECTED is thrown, and the request
+ * is cancelled.
+ */
+add_task(async function test_multiple_types() {
+ let mockRequest = new MockContentPermissionRequest([
+ new MockContentPermissionType("test1"),
+ new MockContentPermissionType("test2"),
+ ]);
+
+ Assert.throws(() => {
+ ContentPermissionPrompt.prompt(mockRequest);
+ }, /NS_ERROR_UNEXPECTED/);
+ Assert.ok(mockRequest.cancelled, "Should have cancelled the request.");
+});
+
+/**
+ * Tests that if the nsIContentPermissionRequest has a type that
+ * does not implement nsIContentPermissionType that NS_NOINTERFACE
+ * is thrown, and the request is cancelled.
+ */
+add_task(async function test_not_permission_type() {
+ let mockRequest = new MockContentPermissionRequest([
+ { QueryInterface: ChromeUtils.generateQI([]) },
+ ]);
+
+ Assert.throws(() => {
+ ContentPermissionPrompt.prompt(mockRequest);
+ }, /NS_NOINTERFACE/);
+ Assert.ok(mockRequest.cancelled, "Should have cancelled the request.");
+});
+
+/**
+ * Tests that if the nsIContentPermissionRequest is for a type
+ * that is not recognized, that NS_ERROR_FAILURE is thrown and
+ * the request is cancelled.
+ */
+add_task(async function test_unrecognized_type() {
+ let mockRequest = new MockContentPermissionRequest([
+ new MockContentPermissionType("test1"),
+ ]);
+
+ Assert.throws(() => {
+ ContentPermissionPrompt.prompt(mockRequest);
+ }, /NS_ERROR_FAILURE/);
+ Assert.ok(mockRequest.cancelled, "Should have cancelled the request.");
+});
+
+/**
+ * Tests that if we meet the minimal requirements for a
+ * nsIContentPermissionRequest, that it will be passed to
+ * ContentPermissionIntegration's createPermissionPrompt
+ * method.
+ */
+add_task(async function test_working_request() {
+ let mockType = new MockContentPermissionType("test-permission-type");
+ let mockRequest = new MockContentPermissionRequest([mockType]);
+
+ // mockPermissionPrompt is what createPermissionPrompt
+ // will return. Returning some kind of object should be
+ // enough to convince nsBrowserGlue that everything went
+ // okay.
+ let didPrompt = false;
+ let mockPermissionPrompt = {
+ prompt() {
+ didPrompt = true;
+ },
+ };
+
+ let integration = base => ({
+ createPermissionPrompt(type, request) {
+ Assert.equal(type, "test-permission-type");
+ Assert.ok(
+ Object.is(request.wrappedJSObject, mockRequest.wrappedJSObject)
+ );
+ return mockPermissionPrompt;
+ },
+ });
+
+ // Register an integration so that we can capture the
+ // calls into ContentPermissionIntegration.
+ try {
+ Integration.contentPermission.register(integration);
+
+ ContentPermissionPrompt.prompt(mockRequest);
+ Assert.ok(!mockRequest.cancelled, "Should not have cancelled the request.");
+ Assert.ok(didPrompt, "Should have tried to show the prompt");
+ } finally {
+ Integration.contentPermission.unregister(integration);
+ }
+});
diff --git a/browser/components/tests/browser/browser_default_bookmark_toolbar_visibility.js b/browser/components/tests/browser/browser_default_bookmark_toolbar_visibility.js
new file mode 100644
index 0000000000..90dde882cc
--- /dev/null
+++ b/browser/components/tests/browser/browser_default_bookmark_toolbar_visibility.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test PlacesUIUtils.maybeToggleBookmarkToolbarVisibility() code running for new profiles.
+ * Ensure that the bookmarks toolbar is hidden in a default configuration.
+ * If new default bookmarks are added to the toolbar then the threshold of > 3
+ * in NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE may need to be adjusted there.
+ */
+
+add_setup(async function () {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.toolbars.bookmarks.visibility");
+ });
+});
+
+add_task(async function test_default_bookmark_toolbar_visibility() {
+ // The Bookmarks Toolbar visibility state should be set only after
+ // Places has notified that it's done initializing.
+ const browserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
+ Ci.nsIObserver
+ );
+
+ let placesInitCompleteObserved = TestUtils.topicObserved(
+ "places-browser-init-complete"
+ );
+
+ // If places-browser-init-complete has already notified, this will cause it
+ // to notify again. Otherwise, we wait until the notify is done.
+ browserGlue.observe(
+ null,
+ "browser-glue-test",
+ "places-browser-init-complete"
+ );
+
+ await placesInitCompleteObserved;
+
+ const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL;
+ let xulStore = Services.xulStore;
+
+ is(
+ xulStore.getValue(BROWSER_DOCURL, "PersonalToolbar", "collapsed"),
+ "",
+ "Check that @collapsed isn't persisted"
+ );
+ ok(
+ document.getElementById("PersonalToolbar").collapsed,
+ "The bookmarks toolbar should be collapsed by default"
+ );
+});
+
+/**
+ * Ensure that the bookmarks toolbar is visible in a new profile
+ * if the toolbar has > 3 (NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE) bookmarks.
+ */
+add_task(async function test_bookmark_toolbar_visible_when_populated() {
+ const { Bookmarks } = ChromeUtils.importESModule(
+ "resource://gre/modules/Bookmarks.sys.mjs"
+ );
+ const { PlacesUIUtils } = ChromeUtils.importESModule(
+ "resource:///modules/PlacesUIUtils.sys.mjs"
+ );
+
+ let bookmark = {
+ type: Bookmarks.TYPE_BOOKMARK,
+ parentGuid: Bookmarks.toolbarGuid,
+ };
+ let bookmarksInserted = await Promise.all([
+ Bookmarks.insert(Object.assign({ url: "https://example.com/1" }, bookmark)),
+ Bookmarks.insert(Object.assign({ url: "https://example.com/2" }, bookmark)),
+ Bookmarks.insert(Object.assign({ url: "https://example.com/3" }, bookmark)),
+ Bookmarks.insert(Object.assign({ url: "https://example.com/4" }, bookmark)),
+ Bookmarks.insert(Object.assign({ url: "https://example.com/5" }, bookmark)),
+ Bookmarks.insert(Object.assign({ url: "https://example.com/6" }, bookmark)),
+ ]);
+
+ await PlacesUIUtils.maybeToggleBookmarkToolbarVisibility();
+
+ const personalToolbar = document.getElementById("PersonalToolbar");
+ ok(
+ !personalToolbar.collapsed,
+ "The bookmarks toolbar should be visible since it has many bookmarks"
+ );
+
+ for (let insertedBookmark of bookmarksInserted) {
+ await Bookmarks.remove(insertedBookmark.guid);
+ }
+ personalToolbar.collapsed = true;
+});
diff --git a/browser/components/tests/browser/browser_default_browser_prompt.js b/browser/components/tests/browser/browser_default_browser_prompt.js
new file mode 100644
index 0000000000..8081b3f429
--- /dev/null
+++ b/browser/components/tests/browser/browser_default_browser_prompt.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { DefaultBrowserCheck } = ChromeUtils.importESModule(
+ "resource:///modules/BrowserGlue.sys.mjs"
+);
+const CHECK_PREF = "browser.shell.checkDefaultBrowser";
+
+function showAndWaitForModal(callback) {
+ const promise = BrowserTestUtils.promiseAlertDialog(null, undefined, {
+ callback,
+ isSubDialog: true,
+ });
+ DefaultBrowserCheck.prompt(BrowserWindowTracker.getTopWindow());
+ return promise;
+}
+
+const TELEMETRY_NAMES = ["accept check", "accept", "cancel check", "cancel"];
+function AssertHistogram(histogram, name, expect = 1) {
+ TelemetryTestUtils.assertHistogram(
+ histogram,
+ TELEMETRY_NAMES.indexOf(name),
+ expect
+ );
+}
+function getHistogram() {
+ return TelemetryTestUtils.getAndClearHistogram("BROWSER_SET_DEFAULT_RESULT");
+}
+
+add_task(async function proton_shows_prompt() {
+ mockShell();
+ ShellService._checkedThisSession = false;
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [CHECK_PREF, true],
+ ["browser.shell.didSkipDefaultBrowserCheckOnFirstRun", true],
+ ],
+ });
+
+ const willPrompt = await DefaultBrowserCheck.willCheckDefaultBrowser();
+
+ Assert.equal(
+ willPrompt,
+ !AppConstants.DEBUG,
+ "Show default browser prompt with proton on non-debug builds"
+ );
+});
+
+add_task(async function not_now() {
+ const histogram = getHistogram();
+ await showAndWaitForModal(win => {
+ win.document.querySelector("dialog").getButton("cancel").click();
+ });
+
+ Assert.equal(
+ Services.prefs.getBoolPref(CHECK_PREF),
+ true,
+ "Canceling keeps pref true"
+ );
+ AssertHistogram(histogram, "cancel");
+});
+
+add_task(async function stop_asking() {
+ const histogram = getHistogram();
+
+ await showAndWaitForModal(win => {
+ const dialog = win.document.querySelector("dialog");
+ dialog.querySelector("checkbox").click();
+ dialog.getButton("cancel").click();
+ });
+
+ Assert.equal(
+ Services.prefs.getBoolPref(CHECK_PREF),
+ false,
+ "Canceling with checkbox checked clears the pref"
+ );
+ AssertHistogram(histogram, "cancel check");
+});
+
+add_task(async function primary_default() {
+ const mock = mockShell({ isPinned: true });
+ const histogram = getHistogram();
+
+ await showAndWaitForModal(win => {
+ win.document.querySelector("dialog").getButton("accept").click();
+ });
+
+ Assert.equal(
+ mock.setAsDefault.callCount,
+ 1,
+ "Primary button sets as default"
+ );
+ Assert.equal(
+ mock.pinCurrentAppToTaskbarAsync.callCount,
+ 0,
+ "Primary button doesn't pin if already pinned"
+ );
+ AssertHistogram(histogram, "accept");
+});
+
+add_task(async function primary_pin() {
+ const mock = mockShell({ canPin: true });
+ const histogram = getHistogram();
+
+ await showAndWaitForModal(win => {
+ win.document.querySelector("dialog").getButton("accept").click();
+ });
+
+ Assert.equal(
+ mock.setAsDefault.callCount,
+ 1,
+ "Primary button sets as default"
+ );
+ if (AppConstants.platform == "win") {
+ Assert.equal(
+ mock.pinCurrentAppToTaskbarAsync.callCount,
+ 1,
+ "Primary button also pins"
+ );
+ }
+ AssertHistogram(histogram, "accept");
+});
diff --git a/browser/components/tests/browser/browser_initial_tab_remoteType.js b/browser/components/tests/browser/browser_initial_tab_remoteType.js
new file mode 100644
index 0000000000..fac4675c9d
--- /dev/null
+++ b/browser/components/tests/browser/browser_initial_tab_remoteType.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests test that the initial browser tab has the right
+ * process type assigned to it on creation, which avoids needless
+ * process flips.
+ */
+
+"use strict";
+
+const PRIVILEGEDABOUT_PROCESS_PREF =
+ "browser.tabs.remote.separatePrivilegedContentProcess";
+const PRIVILEGEDABOUT_PROCESS_ENABLED = Services.prefs.getBoolPref(
+ PRIVILEGEDABOUT_PROCESS_PREF
+);
+
+const REMOTE_BROWSER_SHOWN = "remote-browser-shown";
+
+// When the privileged content process is enabled, we expect about:home
+// to load in it. Otherwise, it's in a normal web content process.
+const EXPECTED_ABOUTHOME_REMOTE_TYPE = PRIVILEGEDABOUT_PROCESS_ENABLED
+ ? E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE
+ : E10SUtils.DEFAULT_REMOTE_TYPE;
+
+/**
+ * Test helper function that takes an nsICommandLine, and passes it
+ * into the default command line handler for the browser. It expects
+ * a new browser window to open, and then checks that the expected page
+ * loads in the initial tab in the expected remote type, without doing
+ * unnecessary process flips. The helper function then closes the window.
+ *
+ * @param aCmdLine (nsICommandLine)
+ * The command line to be processed by the default
+ * nsICommandLineHandler
+ * @param aExpectedURL (string)
+ * The URL that the initial browser tab is expected to load.
+ * @param aRemoteType (string)
+ * The expected remoteType on the initial browser tab.
+ * @returns Promise
+ * Resolves once the checks have completed, and the opened window
+ * have been closed.
+ */
+async function assertOneRemoteBrowserShown(
+ aCmdLine,
+ aExpectedURL,
+ aRemoteType
+) {
+ let shownRemoteBrowsers = 0;
+ let observer = () => {
+ shownRemoteBrowsers++;
+ };
+ Services.obs.addObserver(observer, REMOTE_BROWSER_SHOWN);
+
+ let newWinPromise = BrowserTestUtils.waitForNewWindow({
+ url: aExpectedURL,
+ });
+
+ let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService(
+ Ci.nsICommandLineHandler
+ );
+ cmdLineHandler.handle(aCmdLine);
+
+ let newWin = await newWinPromise;
+
+ Services.obs.removeObserver(observer, REMOTE_BROWSER_SHOWN);
+
+ if (aRemoteType == E10SUtils.WEB_REMOTE_TYPE) {
+ Assert.ok(
+ E10SUtils.isWebRemoteType(newWin.gBrowser.selectedBrowser.remoteType)
+ );
+ } else {
+ Assert.equal(newWin.gBrowser.selectedBrowser.remoteType, aRemoteType);
+ }
+
+ Assert.equal(
+ shownRemoteBrowsers,
+ 1,
+ "Should have only shown 1 remote browser"
+ );
+ await BrowserTestUtils.closeWindow(newWin);
+}
+
+/**
+ * Constructs an object that implements an nsICommandLine that should
+ * cause the default nsICommandLineHandler to open aURL as the initial
+ * tab in a new window. The returns nsICommandLine is stateful, and
+ * shouldn't be reused.
+ *
+ * @param aURL (string)
+ * The URL to load in the initial tab of the new window.
+ * @returns nsICommandLine
+ */
+function constructOnePageCmdLine(aURL) {
+ return Cu.createCommandLine(
+ ["-url", aURL],
+ null,
+ Ci.nsICommandLine.STATE_INITIAL_LAUNCH
+ );
+}
+
+add_setup(async function () {
+ NewTabPagePreloading.removePreloadedBrowser(window);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.newtab.preload", false],
+ ["browser.startup.homepage", "about:home"],
+ ["browser.startup.page", 1],
+ ],
+ });
+});
+
+/**
+ * This tests the default case, where no arguments are passed.
+ */
+add_task(async function test_default_args_and_homescreen() {
+ let cmdLine = Cu.createCommandLine(
+ [],
+ null,
+ Ci.nsICommandLine.STATE_INITIAL_LAUNCH
+ );
+ await assertOneRemoteBrowserShown(
+ cmdLine,
+ "about:home",
+ EXPECTED_ABOUTHOME_REMOTE_TYPE
+ );
+});
+
+/**
+ * This tests the case where about:home is passed as the lone
+ * argument.
+ */
+add_task(async function test_abouthome_arg() {
+ const URI = "about:home";
+ let cmdLine = constructOnePageCmdLine(URI);
+ await assertOneRemoteBrowserShown(
+ cmdLine,
+ URI,
+ EXPECTED_ABOUTHOME_REMOTE_TYPE
+ );
+});
+
+/**
+ * This tests the case where example.com is passed as the lone
+ * argument.
+ */
+add_task(async function test_examplecom_arg() {
+ const URI = "http://example.com/";
+ let cmdLine = constructOnePageCmdLine(URI);
+ await assertOneRemoteBrowserShown(
+ cmdLine,
+ URI,
+ E10SUtils.DEFAULT_REMOTE_TYPE
+ );
+});
diff --git a/browser/components/tests/browser/browser_quit_disabled.js b/browser/components/tests/browser/browser_quit_disabled.js
new file mode 100644
index 0000000000..3b7e99a1bf
--- /dev/null
+++ b/browser/components/tests/browser/browser_quit_disabled.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_appMenu_quit_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.quitShortcut.disabled", true]],
+ });
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let doc = win.document;
+
+ let menuButton = doc.getElementById("PanelUI-menu-button");
+ menuButton.click();
+ await BrowserTestUtils.waitForEvent(win.PanelUI.mainView, "ViewShown");
+
+ let quitButton = doc.querySelector(`[key="key_quitApplication"]`);
+ is(quitButton, null, "No quit button with shortcut key");
+
+ await BrowserTestUtils.closeWindow(win);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_quit_shortcut_disabled() {
+ async function testQuitShortcut(shouldQuit) {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ let quitRequested = false;
+ let observer = {
+ observe(subject, topic, data) {
+ is(topic, "quit-application-requested", "Right observer topic");
+ ok(shouldQuit, "Quit shortcut should NOT have worked");
+
+ // Don't actually quit the browser when testing.
+ let cancelQuit = subject.QueryInterface(Ci.nsISupportsPRBool);
+ cancelQuit.data = true;
+
+ quitRequested = true;
+ },
+ };
+ Services.obs.addObserver(observer, "quit-application-requested");
+
+ let modifiers = { accelKey: true };
+ if (AppConstants.platform == "win") {
+ modifiers.shiftKey = true;
+ }
+ EventUtils.synthesizeKey("q", modifiers, win);
+
+ await BrowserTestUtils.closeWindow(win);
+ Services.obs.removeObserver(observer, "quit-application-requested");
+
+ is(quitRequested, shouldQuit, "Expected quit state");
+ }
+
+ // Quit shortcut should work when pref is not set.
+ await testQuitShortcut(true);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.quitShortcut.disabled", true]],
+ });
+ await testQuitShortcut(false);
+});
diff --git a/browser/components/tests/browser/browser_quit_multiple_tabs.js b/browser/components/tests/browser/browser_quit_multiple_tabs.js
new file mode 100644
index 0000000000..fa0cbc7a4c
--- /dev/null
+++ b/browser/components/tests/browser/browser_quit_multiple_tabs.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Ensure that when different combinations of warnings are enabled,
+ * quitting produces the correct warning (if any), and the checkbox
+ * is also correct.
+ */
+add_task(async function test_check_right_prompt() {
+ let tests = [
+ {
+ warnOnQuitShortcut: true,
+ warnOnClose: false,
+ expectedDialog: "shortcut",
+ messageSuffix: "with shortcut but no tabs warning",
+ },
+ {
+ warnOnQuitShortcut: false,
+ warnOnClose: true,
+ expectedDialog: "tabs",
+ messageSuffix: "with tabs but no shortcut warning",
+ },
+ {
+ warnOnQuitShortcut: false,
+ warnOnClose: false,
+ messageSuffix: "with no warning",
+ expectedDialog: null,
+ },
+ {
+ warnOnQuitShortcut: true,
+ warnOnClose: true,
+ messageSuffix: "with both warnings",
+ // Note: this is somewhat arbitrary; I don't think there's a right/wrong
+ // here, so if this changes due to implementation details, updating the
+ // text expectation to be "tabs" should be OK.
+ expectedDialog: "shortcut",
+ },
+ ];
+ let tab = BrowserTestUtils.addTab(gBrowser);
+
+ function checkDialog(dialog, expectedDialog, messageSuffix) {
+ let dialogElement = dialog.document.getElementById("commonDialog");
+ let acceptLabel = dialogElement.getButton("accept").label;
+ is(
+ acceptLabel.startsWith("Quit"),
+ expectedDialog == "shortcut",
+ `dialog label ${
+ expectedDialog == "shortcut" ? "should" : "should not"
+ } start with Quit ${messageSuffix}`
+ );
+ let checkLabel = dialogElement.querySelector("checkbox").label;
+ is(
+ checkLabel.includes("before quitting with"),
+ expectedDialog == "shortcut",
+ `checkbox label ${
+ expectedDialog == "shortcut" ? "should" : "should not"
+ } be for quitting ${messageSuffix}`
+ );
+
+ dialogElement.getButton("cancel").click();
+ }
+
+ let dialogOpened = false;
+ function setDialogOpened() {
+ dialogOpened = true;
+ }
+ Services.obs.addObserver(setDialogOpened, "common-dialog-loaded");
+ for (let {
+ warnOnClose,
+ warnOnQuitShortcut,
+ expectedDialog,
+ messageSuffix,
+ } of tests) {
+ dialogOpened = false;
+ let promise = null;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.warnOnClose", warnOnClose],
+ ["browser.warnOnQuitShortcut", warnOnQuitShortcut],
+ ["browser.warnOnQuit", true],
+ ],
+ });
+ if (expectedDialog) {
+ promise = BrowserTestUtils.promiseAlertDialogOpen("", undefined, {
+ callback(win) {
+ checkDialog(win, expectedDialog, messageSuffix);
+ },
+ });
+ }
+ is(
+ !canQuitApplication(undefined, "shortcut"),
+ !!expectedDialog,
+ `canQuitApplication ${
+ expectedDialog ? "should" : "should not"
+ } block ${messageSuffix}.`
+ );
+ await promise;
+ is(
+ dialogOpened,
+ !!expectedDialog,
+ `Should ${
+ expectedDialog ? "" : "not "
+ }have opened a dialog ${messageSuffix}.`
+ );
+ }
+ Services.obs.removeObserver(setDialogOpened, "common-dialog-loaded");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/tests/browser/browser_quit_shortcut_warning.js b/browser/components/tests/browser/browser_quit_shortcut_warning.js
new file mode 100644
index 0000000000..7bc67d8562
--- /dev/null
+++ b/browser/components/tests/browser/browser_quit_shortcut_warning.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks that the quit dialog appears correctly when the browser.warnOnQuitShortcut
+// preference is set and the quit keyboard shortcut is pressed.
+add_task(async function test_quit_shortcut() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.warnOnQuit", true],
+ ["browser.warnOnQuitShortcut", true],
+ ],
+ });
+
+ function checkDialog(dialog) {
+ let dialogElement = dialog.document.getElementById("commonDialog");
+ let acceptLabel = dialogElement.getButton("accept").label;
+ is(acceptLabel.indexOf("Quit"), 0, "dialog label");
+ dialogElement.getButton("cancel").click();
+ }
+
+ let dialogOpened = false;
+ function setDialogOpened() {
+ dialogOpened = true;
+ }
+ Services.obs.addObserver(setDialogOpened, "common-dialog-loaded");
+
+ // Test 1: quit using the shortcut key with the preference enabled.
+ let quitPromise = BrowserTestUtils.promiseAlertDialog("cancel", undefined, {
+ callback: checkDialog,
+ });
+ ok(!canQuitApplication(undefined, "shortcut"), "can quit with dialog");
+
+ ok(dialogOpened, "confirmation prompt should have opened");
+
+ await quitPromise;
+
+ // Test 2: quit without using the shortcut key with the preference enabled.
+ dialogOpened = false;
+ ok(canQuitApplication(undefined, ""), "can quit with no dialog");
+ ok(!dialogOpened, "confirmation prompt should not have opened");
+
+ // Test 3: quit using the shortcut key with the preference disabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.warnOnQuitShortcut", false]],
+ });
+
+ dialogOpened = false;
+ ok(canQuitApplication(undefined, "shortcut"), "can quit with no dialog");
+
+ ok(!dialogOpened, "confirmation prompt should not have opened");
+ Services.obs.removeObserver(setDialogOpened, "common-dialog-loaded");
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/tests/browser/browser_startup_homepage.js b/browser/components/tests/browser/browser_startup_homepage.js
new file mode 100644
index 0000000000..f4687e544e
--- /dev/null
+++ b/browser/components/tests/browser/browser_startup_homepage.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function checkArgs(message, expect, prefs = {}) {
+ info(`Setting prefs: ${JSON.stringify(prefs)}`);
+ await SpecialPowers.pushPrefEnv({
+ set: Object.entries(prefs).map(keyVal => {
+ if (typeof keyVal[1] == "object") {
+ keyVal[1] = JSON.stringify(keyVal[1]);
+ }
+ return keyVal;
+ }),
+ });
+
+ // Check the defaultArgs for startup behavior
+ Assert.equal(
+ Cc["@mozilla.org/browser/clh;1"]
+ .getService(Ci.nsIBrowserHandler)
+ .wrappedJSObject.getArgs(true),
+ expect,
+ message
+ );
+}
+
+add_task(async function test_once_expire() {
+ const url = "https://www.mozilla.org/";
+ await checkArgs("no expiration", url, {
+ "browser.startup.homepage_override.once": { url },
+ });
+
+ await checkArgs("expired", "about:blank", {
+ "browser.startup.homepage_override.once": { expire: 0, url },
+ });
+
+ await checkArgs("not expired", url, {
+ "browser.startup.homepage_override.once": { expire: Date.now() * 2, url },
+ });
+});
+
+add_task(async function test_once_invalid() {
+ await checkArgs("not json", "about:blank", {
+ "browser.startup.homepage_override.once": "https://not.json",
+ });
+
+ await checkArgs("not string", "about:blank", {
+ "browser.startup.homepage_override.once": { url: 5 },
+ });
+
+ await checkArgs("not https", "about:blank", {
+ "browser.startup.homepage_override.once": {
+ url: "http://www.mozilla.org/",
+ },
+ });
+
+ await checkArgs("not portless", "about:blank", {
+ "browser.startup.homepage_override.once": {
+ url: "https://www.mozilla.org:123/",
+ },
+ });
+
+ await checkArgs("invalid protocol", "about:blank", {
+ "browser.startup.homepage_override.once": {
+ url: "data:text/plain,hello world",
+ },
+ });
+
+ await checkArgs("invalid domain", "about:blank", {
+ "browser.startup.homepage_override.once": {
+ url: "https://wwwmozilla.org/",
+ },
+ });
+
+ await checkArgs(
+ "invalid second domain",
+ "https://valid.firefox.com/|https://mozilla.org/",
+ {
+ "browser.startup.homepage_override.once": {
+ url: "https://valid.firefox.com|https://invalidfirefox.com|https://mozilla.org",
+ },
+ }
+ );
+});
+
+add_task(async function test_once() {
+ await checkArgs("initial test prefs (no homepage)", "about:blank");
+
+ const url = "https://www.mozilla.org/";
+ await checkArgs("override once", url, {
+ "browser.startup.homepage_override.once": { url },
+ });
+
+ await checkArgs("once cleared", "about:blank");
+
+ await checkArgs("formatted", "https://www.mozilla.org/en-US", {
+ "browser.startup.homepage_override.once": {
+ url: "https://www.mozilla.org/%LOCALE%",
+ },
+ });
+
+ await checkArgs("use homepage", "about:home", {
+ "browser.startup.page": 1,
+ });
+
+ await checkArgs("once with homepage", `${url}|about:home`, {
+ "browser.startup.homepage_override.once": { url },
+ });
+
+ await checkArgs("once cleared again", "about:home");
+
+ await checkArgs("prefer major version override", `about:welcome|about:home`, {
+ "browser.startup.homepage_override.mstone": "1.0",
+ "browser.startup.homepage_override.once": { url },
+ "startup.homepage_override_url": "about:welcome",
+ });
+
+ await checkArgs("once after major", `${url}|about:home`);
+
+ await checkArgs("once cleared yet again", "about:home");
+});
diff --git a/browser/components/tests/browser/browser_system_notification_telemetry.js b/browser/components/tests/browser/browser_system_notification_telemetry.js
new file mode 100644
index 0000000000..6cc8d12165
--- /dev/null
+++ b/browser/components/tests/browser/browser_system_notification_telemetry.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function handleCommandLine(args, state) {
+ let newWinPromise;
+ let target = Services.urlFormatter.formatURLPref(
+ "browser.shell.defaultBrowserAgent.thanksURL"
+ );
+
+ const EXISTING_FILE = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ EXISTING_FILE.initWithPath(getTestFilePath("dummy.pdf"));
+
+ if (state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
+ newWinPromise = BrowserTestUtils.waitForNewWindow({
+ url: target, // N.b.: trailing slashes matter when matching.
+ });
+ }
+
+ let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService(
+ Ci.nsICommandLineHandler
+ );
+
+ let fakeCmdLine = Cu.createCommandLine(args, EXISTING_FILE.parent, state);
+ cmdLineHandler.handle(fakeCmdLine);
+
+ if (newWinPromise) {
+ let newWin = await newWinPromise;
+ await BrowserTestUtils.closeWindow(newWin);
+ } else {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+}
+
+// Launching from the WDBA should open the "thanks" page and should send a
+// telemetry event.
+add_task(async function test_launched_to_handle_default_browser_agent() {
+ await handleCommandLine(
+ ["-to-handle-default-browser-agent"],
+ Ci.nsICommandLine.STATE_INITIAL_LAUNCH
+ );
+
+ TelemetryTestUtils.assertEvents(
+ [{ extra: { name: "default-browser-agent" } }],
+ {
+ category: "browser.launched_to_handle",
+ method: "system_notification",
+ object: "toast",
+ }
+ );
+});
diff --git a/browser/components/tests/browser/browser_to_handle_telemetry.js b/browser/components/tests/browser/browser_to_handle_telemetry.js
new file mode 100644
index 0000000000..92c8202e94
--- /dev/null
+++ b/browser/components/tests/browser/browser_to_handle_telemetry.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function handleCommandLine(args, state) {
+ let newWinPromise;
+ let target = args[args.length - 1];
+
+ const EXISTING_FILE = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ EXISTING_FILE.initWithPath(getTestFilePath("dummy.pdf"));
+
+ if (!target.includes("://")) {
+ // For simplicity, we handle only absolute paths. We could resolve relative
+ // paths, but that would itself require the functionality of the
+ // `nsICommandLine` instance we produce using this input.
+ const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(target);
+ target = Services.io.newFileURI(file).spec;
+ }
+
+ if (state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
+ newWinPromise = BrowserTestUtils.waitForNewWindow({
+ url: target, // N.b.: trailing slashes matter when matching.
+ });
+ }
+
+ let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService(
+ Ci.nsICommandLineHandler
+ );
+
+ let fakeCmdLine = Cu.createCommandLine(args, EXISTING_FILE.parent, state);
+ cmdLineHandler.handle(fakeCmdLine);
+
+ if (newWinPromise) {
+ let newWin = await newWinPromise;
+ await BrowserTestUtils.closeWindow(newWin);
+ } else {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+}
+
+function assertToHandleTelemetry(assertions) {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+
+ const { invoked, launched, ...unknown } = assertions;
+ if (Object.keys(unknown).length) {
+ throw Error(
+ `Unknown keys given to assertToHandleTelemetry: ${JSON.stringify(
+ unknown
+ )}`
+ );
+ }
+ if (invoked === undefined && launched === undefined) {
+ throw Error("No known keys given to assertToHandleTelemetry");
+ }
+
+ for (let scalar of ["invoked", "launched"]) {
+ if (scalar in assertions) {
+ const { handled, not_handled } = assertions[scalar] || {};
+ if (handled) {
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ `os.environment.${scalar}_to_handle`,
+ handled,
+ 1,
+ `${scalar} to handle '${handled}' 1 times`
+ );
+ // Intentionally nested.
+ if (not_handled) {
+ Assert.equal(
+ not_handled in scalars[`os.environment.${scalar}_to_handle`],
+ false,
+ `${scalar} to handle '${not_handled}' 0 times`
+ );
+ }
+ } else {
+ TelemetryTestUtils.assertScalarUnset(
+ scalars,
+ `os.environment.${scalar}_to_handle`
+ );
+
+ if (not_handled) {
+ throw new Error(
+ `In ${scalar}, 'not_handled' is only valid with 'handled'`
+ );
+ }
+ }
+ }
+ }
+}
+
+add_task(async function test_invoked_to_handle_registered_file_type() {
+ await handleCommandLine(
+ [
+ "-osint",
+ "-url",
+ getTestFilePath("../../../../dom/security/test/csp/dummy.pdf"),
+ ],
+ Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
+ );
+
+ assertToHandleTelemetry({
+ invoked: { handled: ".pdf", not_handled: ".html" },
+ launched: null,
+ });
+});
+
+add_task(async function test_invoked_to_handle_unregistered_file_type() {
+ await handleCommandLine(
+ ["-osint", "-url", getTestFilePath("browser.ini")],
+ Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
+ );
+
+ assertToHandleTelemetry({
+ invoked: { handled: ".<other extension>", not_handled: ".ini" },
+ launched: null,
+ });
+});
+
+add_task(async function test_invoked_to_handle_registered_protocol() {
+ await handleCommandLine(
+ ["-osint", "-url", "https://example.com/"],
+ Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
+ );
+
+ assertToHandleTelemetry({
+ invoked: { handled: "https", not_handled: "mailto" },
+ launched: null,
+ });
+});
+
+add_task(async function test_invoked_to_handle_unregistered_protocol() {
+ // Truly unknown protocols get "URI fixed up" to search provider queries.
+ // `ftp` does not get fixed up.
+ await handleCommandLine(
+ ["-osint", "-url", "ftp://example.com/"],
+ Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
+ );
+
+ assertToHandleTelemetry({
+ invoked: { handled: "<other protocol>", not_handled: "ftp" },
+ launched: null,
+ });
+});
+
+add_task(async function test_launched_to_handle_registered_protocol() {
+ await handleCommandLine(
+ ["-osint", "-url", "https://example.com/"],
+ Ci.nsICommandLine.STATE_INITIAL_LAUNCH
+ );
+
+ assertToHandleTelemetry({
+ invoked: null,
+ launched: { handled: "https", not_handled: "mailto" },
+ });
+});
+
+add_task(async function test_launched_to_handle_registered_file_type() {
+ await handleCommandLine(
+ [
+ "-osint",
+ "-url",
+ getTestFilePath("../../../../dom/security/test/csp/dummy.pdf"),
+ ],
+ Ci.nsICommandLine.STATE_INITIAL_LAUNCH
+ );
+
+ assertToHandleTelemetry({
+ invoked: null,
+ launched: { handled: ".pdf", not_handled: ".html" },
+ });
+});
+
+add_task(async function test_invoked_no_osint() {
+ await handleCommandLine(
+ ["-url", "https://example.com/"],
+ Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
+ );
+
+ assertToHandleTelemetry({
+ invoked: null,
+ launched: null,
+ });
+});
+
+add_task(async function test_launched_no_osint() {
+ await handleCommandLine(
+ ["-url", "https://example.com/"],
+ Ci.nsICommandLine.STATE_INITIAL_LAUNCH
+ );
+
+ assertToHandleTelemetry({
+ invoked: null,
+ launched: null,
+ });
+});
diff --git a/browser/components/tests/browser/head.js b/browser/components/tests/browser/head.js
new file mode 100644
index 0000000000..89c8df8613
--- /dev/null
+++ b/browser/components/tests/browser/head.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+// Helpers for testing telemetry events.
+
+// Tests can change the category to filter for different events.
+var gTelemetryCategory = "upgrade_dialog";
+
+function AssertEvents(message, ...events) {
+ info(`Checking telemetry events: ${message}`);
+ TelemetryTestUtils.assertEvents(
+ events.map(event => [gTelemetryCategory, ...event]),
+ { category: gTelemetryCategory }
+ );
+}
+
+const BROWSER_GLUE =
+ Cc["@mozilla.org/browser/browserglue;1"].getService().wrappedJSObject;
+
+// Helpers for mocking various shell states.
+
+let didMockShell = false;
+function mockShell(overrides = {}) {
+ if (!didMockShell) {
+ sinon.stub(window, "getShellService");
+ registerCleanupFunction(() => {
+ getShellService.restore();
+ });
+ didMockShell = true;
+ }
+
+ const sharedPinStub = sinon.stub().resolves(undefined);
+ let mock = {
+ canPin: false,
+ isDefault: false,
+ isPinned: false,
+
+ async checkPinCurrentAppToTaskbarAsync(privateBrowsing = false) {
+ if (!this.canPin) {
+ throw Error;
+ }
+ },
+ get isAppInDock() {
+ return this.isPinned;
+ },
+ isCurrentAppPinnedToTaskbarAsync(privateBrowsing = false) {
+ return Promise.resolve(this.isPinned);
+ },
+ isDefaultBrowser() {
+ return this.isDefault;
+ },
+ get macDockSupport() {
+ return this;
+ },
+ // eslint-disable-next-line mozilla/use-chromeutils-generateqi
+ QueryInterface() {
+ return this;
+ },
+ get shellService() {
+ return this;
+ },
+
+ ensureAppIsPinnedToDock: sharedPinStub,
+ pinCurrentAppToTaskbarAsync: sharedPinStub,
+ setAsDefault: sinon.stub(),
+ ...overrides,
+ };
+
+ // Prefer the mocked implementation and fall back to the original version,
+ // which can call back into the mocked version (via this.shellService).
+ mock = new Proxy(mock, {
+ get(target, prop) {
+ return (prop in target ? target : ShellService)[prop];
+ },
+ set(target, prop, val) {
+ (prop in target ? target : ShellService)[prop] = val;
+ return true;
+ },
+ });
+
+ getShellService.returns(mock);
+ return mock;
+}
diff --git a/browser/components/tests/browser/whats_new_page/active-update.xml b/browser/components/tests/browser/whats_new_page/active-update.xml
new file mode 100644
index 0000000000..6e32eb1be2
--- /dev/null
+++ b/browser/components/tests/browser/whats_new_page/active-update.xml
@@ -0,0 +1 @@
+<?xml version="1.0"?><updates xmlns="http://www.mozilla.org/2005/app-update"><update xmlns="http://www.mozilla.org/2005/app-update" appVersion="99999999.0" buildID="20990101111111" channel="test" detailsURL="https://127.0.0.1/" displayVersion="1.0" installDate="1555716429454" isCompleteUpdate="true" name="What's New Page Test" previousAppVersion="60.0" serviceURL="https://127.0.0.1/update.xml" type="minor" platformVersion="99999999.0" actions="showURL" openURL="https://example.com/|https://example.com/"><patch size="1" type="complete" URL="https://127.0.0.1/complete.mar" selected="true" state="pending"/></update></updates>
diff --git a/browser/components/tests/browser/whats_new_page/browser.toml b/browser/components/tests/browser/whats_new_page/browser.toml
new file mode 100644
index 0000000000..b8b9b93d4a
--- /dev/null
+++ b/browser/components/tests/browser/whats_new_page/browser.toml
@@ -0,0 +1,21 @@
+[DEFAULT]
+skip-if = [
+ "verify",
+ "os == 'win' && msix", # Updater is disabled in MSIX builds; what's new pages therefore have no meaning.
+]
+reason = "This is a startup test. Verify runs tests multiple times after startup."
+support-files = [
+ "active-update.xml",
+ "updates/0/update.status",
+ "config_localhost_update_url.json",
+]
+prefs = [
+ "app.update.altUpdateDirPath='<test-root>/browser/components/tests/browser/whats_new_page'",
+ "app.update.disabledForTesting=false",
+ "browser.aboutwelcome.enabled=false",
+ "browser.startup.homepage_override.mstone='60.0'",
+ "browser.startup.upgradeDialog.enabled=false",
+ "browser.policies.alternatePath='<test-root>/browser/components/tests/browser/whats_new_page/config_localhost_update_url.json'",
+]
+
+["browser_whats_new_page.js"]
diff --git a/browser/components/tests/browser/whats_new_page/browser_whats_new_page.js b/browser/components/tests/browser/whats_new_page/browser_whats_new_page.js
new file mode 100644
index 0000000000..2fdec5e7e1
--- /dev/null
+++ b/browser/components/tests/browser/whats_new_page/browser_whats_new_page.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function whats_new_page() {
+ // The test harness will use the current tab and remove the tab's history.
+ // Since the page that is tested is opened prior to the test harness taking
+ // over the current tab the active-update.xml specifies two pages to open by
+ // having 'https://example.com/|https://example.com/' for the value of openURL
+ // and then uses the first tab for the test.
+ gBrowser.selectedTab = gBrowser.tabs[0];
+ // The test harness also changes the page to about:blank so go back to the
+ // page that was originally opened.
+ gBrowser.goBack();
+ // Wait for the page to go back to the original page.
+ await TestUtils.waitForCondition(
+ () =>
+ gBrowser.selectedBrowser &&
+ gBrowser.selectedBrowser.currentURI &&
+ gBrowser.selectedBrowser.currentURI.spec == "https://example.com/",
+ "Waiting for the expected page to reopen"
+ );
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "https://example.com/",
+ "The what's new page's url should equal https://example.com/"
+ );
+ gBrowser.removeTab(gBrowser.selectedTab);
+
+ let um = Cc["@mozilla.org/updates/update-manager;1"].getService(
+ Ci.nsIUpdateManager
+ );
+ await TestUtils.waitForCondition(
+ () => !um.readyUpdate,
+ "Waiting for the ready update to be removed"
+ );
+ ok(!um.readyUpdate, "There should not be a ready update");
+ await TestUtils.waitForCondition(
+ () => !!um.getUpdateAt(0),
+ "Waiting for the ready update to be moved to the update history"
+ );
+ ok(!!um.getUpdateAt(0), "There should be an update in the update history");
+
+ // Leave no trace. Since this test modifies its support files put them back in
+ // their original state.
+ let alternatePath = Services.prefs.getCharPref("app.update.altUpdateDirPath");
+ let testRoot = Services.prefs.getCharPref("mochitest.testRoot");
+ let relativePath = alternatePath.substring("<test-root>".length);
+ if (AppConstants.platform == "win") {
+ relativePath = relativePath.replace(/\//g, "\\");
+ }
+ alternatePath = testRoot + relativePath;
+ let updateDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ updateDir.initWithPath(alternatePath);
+
+ let activeUpdateFile = updateDir.clone();
+ activeUpdateFile.append("active-update.xml");
+ await TestUtils.waitForCondition(
+ () => !activeUpdateFile.exists(),
+ "Waiting until the active-update.xml file does not exist"
+ );
+
+ let updatesFile = updateDir.clone();
+ updatesFile.append("updates.xml");
+ await TestUtils.waitForCondition(
+ () => updatesFile.exists(),
+ "Waiting until the updates.xml file exists"
+ );
+
+ let fos = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ let flags =
+ FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE;
+
+ let stateSucceeded = "succeeded\n";
+ let updateStatusFile = updateDir.clone();
+ updateStatusFile.append("updates");
+ updateStatusFile.append("0");
+ updateStatusFile.append("update.status");
+ updateStatusFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ fos.init(updateStatusFile, flags, FileUtils.PERMS_FILE, 0);
+ fos.write(stateSucceeded, stateSucceeded.length);
+ fos.close();
+
+ let xmlContents =
+ '<?xml version="1.0"?><updates xmlns="http://www.mozilla.org/2005/' +
+ 'app-update"><update xmlns="http://www.mozilla.org/2005/app-update" ' +
+ 'appVersion="99999999.0" buildID="20990101111111" channel="test" ' +
+ 'detailsURL="https://127.0.0.1/" displayVersion="1.0" installDate="' +
+ '1555716429454" isCompleteUpdate="true" name="What\'s New Page Test" ' +
+ 'previousAppVersion="60.0" serviceURL="https://127.0.0.1/update.xml" ' +
+ 'type="minor" platformVersion="99999999.0" actions="showURL" ' +
+ 'openURL="https://example.com/|https://example.com/"><patch size="1" ' +
+ 'type="complete" URL="https://127.0.0.1/complete.mar" ' +
+ 'selected="true" state="pending"/></update></updates>\n';
+ activeUpdateFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ fos.init(activeUpdateFile, flags, FileUtils.PERMS_FILE, 0);
+ fos.write(xmlContents, xmlContents.length);
+ fos.close();
+
+ updatesFile.remove(false);
+ Cc["@mozilla.org/updates/update-manager;1"]
+ .getService(Ci.nsIUpdateManager)
+ .QueryInterface(Ci.nsIObserver)
+ .observe(null, "um-reload-update-data", "");
+});
diff --git a/browser/components/tests/browser/whats_new_page/config_localhost_update_url.json b/browser/components/tests/browser/whats_new_page/config_localhost_update_url.json
new file mode 100644
index 0000000000..4766b6a3fd
--- /dev/null
+++ b/browser/components/tests/browser/whats_new_page/config_localhost_update_url.json
@@ -0,0 +1,5 @@
+{
+ "policies": {
+ "AppUpdateURL": "http://127.0.0.1:8888/update.xml"
+ }
+}
diff --git a/browser/components/tests/browser/whats_new_page/updates/0/update.status b/browser/components/tests/browser/whats_new_page/updates/0/update.status
new file mode 100644
index 0000000000..774a5c0df4
--- /dev/null
+++ b/browser/components/tests/browser/whats_new_page/updates/0/update.status
@@ -0,0 +1 @@
+succeeded
diff --git a/browser/components/tests/marionette/manifest.toml b/browser/components/tests/marionette/manifest.toml
new file mode 100644
index 0000000000..158bb0e819
--- /dev/null
+++ b/browser/components/tests/marionette/manifest.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+tags = "local"
+
+["test_no_errors_clean_profile.py"]
diff --git a/browser/components/tests/marionette/test_no_errors_clean_profile.py b/browser/components/tests/marionette/test_no_errors_clean_profile.py
new file mode 100644
index 0000000000..efa75fcb47
--- /dev/null
+++ b/browser/components/tests/marionette/test_no_errors_clean_profile.py
@@ -0,0 +1,186 @@
+# 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/.
+
+import time
+from unittest.util import safe_repr
+
+from marionette_driver.by import By
+from marionette_driver.keys import Keys
+from marionette_harness import MarionetteTestCase
+
+# This list shouldn't exist!
+# DO NOT ADD NEW EXCEPTIONS HERE! (unless they are specifically caused by
+# being run under marionette rather than in a "real" profile, or only occur
+# for browser developers)
+# The only reason this exists is that when this test was written we already
+# created a bunch of errors on startup, and it wasn't feasible to fix all
+# of them before landing the test.
+known_errors = [
+ {
+ # Disabling Shield because app.normandy.api_url is not set.
+ # (Marionette-only error, bug 1826314)
+ "message": "app.normandy.api_url is not set",
+ },
+ {
+ # From Remote settings, because it's intercepted by our test
+ # infrastructure which serves text/plain rather than JSON.
+ # Even if we fixed that we'd probably see a different error,
+ # unless we mock a full-blown remote settings server in the
+ # test infra, which doesn't seem worth it.
+ # Either way this wouldn't happen on "real" profiles.
+ "message": 'Error: Unexpected content-type "text/plain',
+ "filename": "RemoteSettingsClient",
+ },
+ {
+ # Triggered as soon as anything tries to use shortcut keys.
+ # The browser toolbox shortcut is not portable.
+ "message": "key_browserToolbox",
+ },
+ {
+ # Triggered as soon as anything tries to use shortcut keys.
+ # The developer-only restart shortcut is not portable.
+ "message": "key_quickRestart",
+ },
+ {
+ # Triggered as soon as anything tries to use shortcut keys.
+ # The reader mode shortcut is not portable on Linux.
+ # Bug 1825431 to fix this.
+ "message": "key_toggleReaderMode",
+ },
+ {
+ # Triggered on Linux because it doesn't implement the
+ # secondsSinceLastOSRestart property at all.
+ "message": "(NS_ERROR_NOT_IMPLEMENTED) [nsIAppStartup.secondsSinceLastOSRestart]",
+ "filename": "BrowserGlue",
+ },
+]
+
+# Same rules apply here - please don't add anything! - but headless runs
+# produce more errors that aren't normal in regular runs, so we've separated
+# them out.
+headless_errors = [{"message": "TelemetryEnvironment::_isDefaultBrowser"}]
+
+
+class TestNoErrorsNewProfile(MarionetteTestCase):
+ def setUp(self):
+ super(MarionetteTestCase, self).setUp()
+
+ self.maxDiff = None
+ self.marionette.set_context("chrome")
+
+ # Create a fresh profile.
+ self.marionette.restart(in_app=False, clean=True)
+
+ def ensure_proper_startup(self):
+ # First wait for the browser to settle:
+ self.marionette.execute_async_script(
+ """
+ let resolve = arguments[0];
+ let { BrowserInitState } = ChromeUtils.importESModule("resource:///modules/BrowserGlue.sys.mjs");
+ let promises = [
+ BrowserInitState.startupIdleTaskPromise,
+ gBrowserInit.idleTasksFinishedPromise,
+ ];
+ Promise.all(promises).then(resolve);
+ """
+ )
+
+ if self.marionette.session_capabilities["platformName"] == "mac":
+ self.mod_key = Keys.META
+ else:
+ self.mod_key = Keys.CONTROL
+ # Focus the URL bar by keyboard
+ url_bar = self.marionette.find_element(By.ID, "urlbar-input")
+ url_bar.send_keys(self.mod_key, "l")
+ # and open a tab by mouse:
+ new_tab_button = self.marionette.find_element(By.ID, "new-tab-button")
+ new_tab_button.click()
+
+ # Wait a bit more for async tasks to complete...
+ time.sleep(5)
+
+ def get_all_errors(self):
+ return self.marionette.execute_async_script(
+ """
+ let resolve = arguments[0];
+ // Get all the messages from the console service,
+ // and then get all of the ones from the console API storage.
+ let msgs = Services.console.getMessageArray();
+
+ const ConsoleAPIStorage = Cc[
+ "@mozilla.org/consoleAPI-storage;1"
+ ].getService(Ci.nsIConsoleAPIStorage);
+ const getCircularReplacer = () => {
+ const seen = new WeakSet();
+ return (key, value) => {
+ if (typeof value === "object" && value !== null) {
+ if (seen.has(value)) {
+ return "<circular ref>";
+ }
+ seen.add(value);
+ }
+ return value;
+ };
+ };
+ // Take cyclical values out, add a simplified 'message' prop
+ // that matches how things work for the console service objects.
+ const consoleApiMessages = ConsoleAPIStorage.getEvents().map(ev => {
+ let rv;
+ try {
+ rv = structuredClone(ev);
+ } catch (ex) {
+ rv = JSON.parse(JSON.stringify(ev, getCircularReplacer()));
+ }
+ delete rv.wrappedJSObject;
+ rv.message = ev.arguments.join(", ");
+ return rv;
+ });
+ resolve(msgs.concat(consoleApiMessages));
+ """
+ )
+
+ def should_ignore_error(self, error):
+ if not "message" in error:
+ print("Unparsable error:")
+ print(safe_repr(error))
+ return False
+
+ error_filename = error.get("filename", "")
+ error_msg = error["message"]
+ headless = self.marionette.session_capabilities["moz:headless"]
+ all_known_errors = known_errors + (headless_errors if headless else [])
+
+ for known_error in all_known_errors:
+ known_filename = known_error.get("filename", "")
+ known_msg = known_error["message"]
+ if known_msg in error_msg and known_filename in error_filename:
+ print(
+ "Known error seen: %s (%s)"
+ % (error["message"], error.get("filename", "no filename"))
+ )
+ return True
+
+ return False
+
+ def short_error_display(self, errors):
+ rv = []
+ for error in errors:
+ rv += [
+ {
+ "message": error.get("message", "No message!?"),
+ "filename": error.get("filename", "No filename!?"),
+ }
+ ]
+ return rv
+
+ def test_no_errors(self):
+ self.ensure_proper_startup()
+ errors = self.get_all_errors()
+ errors[:] = [error for error in errors if not self.should_ignore_error(error)]
+ if len(errors) > 0:
+ print("Unexpected errors encountered:")
+ # Hack to get nice printing:
+ for error in errors:
+ print(safe_repr(error))
+ self.assertEqual(self.short_error_display(errors), [])
diff --git a/browser/components/tests/unit/distribution.ini b/browser/components/tests/unit/distribution.ini
new file mode 100644
index 0000000000..83fe19b2cc
--- /dev/null
+++ b/browser/components/tests/unit/distribution.ini
@@ -0,0 +1,60 @@
+# Distribution Configuration File
+# Test of distribution preferences
+
+[Global]
+id=disttest
+version=1.0
+about=Test distribution file
+about.en-US=Tèƨƭ δïƨƭřïβúƭïôñ ƒïℓè
+
+[Preferences]
+distribution.test.string="Test String"
+distribution.test.string.noquotes=Test String
+distribution.test.int=777
+distribution.test.bool.true=true
+distribution.test.bool.false=false
+distribution.test.empty=
+
+distribution.test.pref.locale="%LOCALE%"
+distribution.test.pref.language.reset="Preference Set"
+distribution.test.pref.locale.reset="Preference Set"
+distribution.test.pref.locale.set="Preference Set"
+distribution.test.pref.language.set="Preference Set"
+
+general.useragent.locale=en-US
+
+[Preferences-en]
+distribution.test.pref.language.en="en"
+distribution.test.pref.language.reset=
+distribution.test.pref.language.set="Language Set"
+distribution.test.pref.locale.set="Language Set"
+
+[Preferences-en-US]
+distribution.test.pref.locale.en-US="en-US"
+distribution.test.pref.locale.reset=
+distribution.test.pref.locale.set="Locale Set"
+
+
+[Preferences-de]
+distribution.test.pref.language.de="de"
+
+[LocalizablePreferences]
+distribution.test.locale="%LOCALE%"
+distribution.test.language.reset="Preference Set"
+distribution.test.locale.reset="Preference Set"
+distribution.test.locale.set="Preference Set"
+distribution.test.language.set="Preference Set"
+
+[LocalizablePreferences-en]
+distribution.test.language.en="en"
+distribution.test.language.reset=
+distribution.test.language.set="Language Set"
+distribution.test.locale.set="Language Set"
+
+[LocalizablePreferences-en-US]
+distribution.test.locale.en-US="en-US"
+distribution.test.locale.reset=
+distribution.test.locale.set="Locale Set"
+
+[LocalizablePreferences-de]
+distribution.test.language.de="de"
diff --git a/browser/components/tests/unit/head.js b/browser/components/tests/unit/head.js
new file mode 100644
index 0000000000..2fb39adbd4
--- /dev/null
+++ b/browser/components/tests/unit/head.js
@@ -0,0 +1,8 @@
+/* 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/. */
+
+var { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+var gProfD = do_get_profile().QueryInterface(Ci.nsIFile);
diff --git a/browser/components/tests/unit/test_browserGlue_migration_ctrltab_recently_used_order.js b/browser/components/tests/unit/test_browserGlue_migration_ctrltab_recently_used_order.js
new file mode 100644
index 0000000000..119d32996b
--- /dev/null
+++ b/browser/components/tests/unit/test_browserGlue_migration_ctrltab_recently_used_order.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
+const TOPICDATA_BROWSERGLUE_TEST = "force-ui-migration";
+const RECENTLY_USED_ORDER_DEFAULT = false;
+const UI_VERSION = 107;
+
+const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
+ Ci.nsIObserver
+);
+
+add_task(async function has_not_used_ctrl_tab_and_its_off() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
+ Services.prefs.setBoolPref("browser.engagement.ctrlTab.has-used", false);
+ Services.prefs.setBoolPref("browser.ctrlTab.recentlyUsedOrder", false);
+
+ // Simulate a migration.
+ gBrowserGlue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_BROWSERGLUE_TEST
+ );
+
+ Assert.equal(
+ RECENTLY_USED_ORDER_DEFAULT,
+ Services.prefs.getBoolPref("browser.ctrlTab.sortByRecentlyUsed")
+ );
+});
+
+add_task(async function has_not_used_ctrl_tab_and_its_on() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
+ Services.prefs.setBoolPref("browser.engagement.ctrlTab.has-used", false);
+ Services.prefs.setBoolPref("browser.ctrlTab.recentlyUsedOrder", true);
+
+ // Simulate a migration.
+ gBrowserGlue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_BROWSERGLUE_TEST
+ );
+
+ Assert.equal(
+ RECENTLY_USED_ORDER_DEFAULT,
+ Services.prefs.getBoolPref("browser.ctrlTab.sortByRecentlyUsed")
+ );
+});
+
+add_task(async function has_used_ctrl_tab_and_its_off() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
+ Services.prefs.setBoolPref("browser.engagement.ctrlTab.has-used", true);
+ Services.prefs.setBoolPref("browser.ctrlTab.recentlyUsedOrder", false);
+
+ // Simulate a migration.
+ gBrowserGlue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_BROWSERGLUE_TEST
+ );
+
+ Assert.equal(
+ false,
+ Services.prefs.getBoolPref("browser.ctrlTab.sortByRecentlyUsed")
+ );
+});
+
+add_task(async function has_used_ctrl_tab_and_its_on() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
+ Services.prefs.setBoolPref("browser.engagement.ctrlTab.has-used", true);
+ Services.prefs.setBoolPref("browser.ctrlTab.recentlyUsedOrder", true);
+
+ // Simulate a migration.
+ gBrowserGlue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_BROWSERGLUE_TEST
+ );
+
+ Assert.equal(
+ true,
+ Services.prefs.getBoolPref("browser.ctrlTab.sortByRecentlyUsed")
+ );
+});
+
+add_task(async function has_used_ctrl_tab_and_its_default() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
+ Services.prefs.setBoolPref("browser.engagement.ctrlTab.has-used", true);
+ Services.prefs.clearUserPref("browser.ctrlTab.recentlyUsedOrder");
+
+ // Simulate a migration.
+ gBrowserGlue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_BROWSERGLUE_TEST
+ );
+
+ // Default had been true
+ Assert.equal(
+ true,
+ Services.prefs.getBoolPref("browser.ctrlTab.sortByRecentlyUsed")
+ );
+});
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.migration.version");
+ Services.prefs.clearUserPref("browser.engagement.ctrlTab.has-used");
+ Services.prefs.clearUserPref("browser.ctrlTab.recentlyUsedOrder");
+ Services.prefs.clearUserPref("browser.ctrlTab.sortByRecentlyUsed");
+});
diff --git a/browser/components/tests/unit/test_browserGlue_migration_formautofill.js b/browser/components/tests/unit/test_browserGlue_migration_formautofill.js
new file mode 100644
index 0000000000..7c97fa2279
--- /dev/null
+++ b/browser/components/tests/unit/test_browserGlue_migration_formautofill.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
+const TOPICDATA_BROWSERGLUE_TEST = "force-ui-migration";
+
+const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
+ Ci.nsIObserver
+);
+const UI_VERSION = 124;
+
+function ensureOldPrefsAreCleared() {
+ Assert.ok(
+ !Services.prefs.prefHasUserValue("extensions.formautofill.available"),
+ "main module available pref should have been cleared"
+ );
+ Assert.ok(
+ !Services.prefs.prefHasUserValue(
+ "extensions.formautofill.creditCards.available"
+ ),
+ "old credit card available pref should have been cleared"
+ );
+}
+
+add_task(async function setup() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.migration.version");
+ Services.prefs.clearUserPref("extensions.formautofill.available");
+ Services.prefs.clearUserPref(
+ "extensions.formautofill.creditCards.available"
+ );
+ Services.prefs.clearUserPref(
+ "extensions.formautofill.creditCards.supported"
+ );
+ });
+});
+
+add_task(async function test_check_form_autofill_module_detect() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION - 1);
+ Services.prefs.setCharPref("extensions.formautofill.available", "detect");
+ // Simulate a migration.
+ gBrowserGlue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_BROWSERGLUE_TEST
+ );
+ // old credit card available should migrate to "detect" due to
+ // "extensions.formautofill.available" being "detect".
+ Assert.equal(
+ Services.prefs.getCharPref("extensions.formautofill.creditCards.supported"),
+ "detect"
+ );
+ // old address available pref follows the main module pref
+ Assert.equal(
+ Services.prefs.getCharPref("extensions.formautofill.addresses.supported"),
+ "detect"
+ );
+ ensureOldPrefsAreCleared();
+});
+
+add_task(async function test_check_old_form_autofill_module_off() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION - 1);
+ Services.prefs.setCharPref("extensions.formautofill.available", "off");
+
+ // Simulate a migration.
+ gBrowserGlue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_BROWSERGLUE_TEST
+ );
+
+ // old credit card available should migrate to off due to
+ // "extensions.formautofill.available" being off.
+ Assert.equal(
+ Services.prefs.getCharPref("extensions.formautofill.creditCards.supported"),
+ "off"
+ );
+ // old address available pref follows the main module pref
+ Assert.equal(
+ Services.prefs.getCharPref("extensions.formautofill.addresses.supported"),
+ "off"
+ );
+ ensureOldPrefsAreCleared();
+});
+
+add_task(async function test_check_old_form_autofill_module_on_cc_on() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION - 1);
+ Services.prefs.setCharPref("extensions.formautofill.available", "on");
+ Services.prefs.setBoolPref(
+ "extensions.formautofill.creditCards.available",
+ true
+ );
+
+ // Simulate a migration.
+ gBrowserGlue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_BROWSERGLUE_TEST
+ );
+
+ // old credit card available should migrate to "on" due to
+ // "extensions.formautofill.available" being on and
+ // "extensions.formautofill.creditCards.available" having a default value of true.
+ Assert.equal(
+ Services.prefs.getCharPref("extensions.formautofill.creditCards.supported"),
+ "on"
+ );
+ // old address available pref follows the main module pref
+ Assert.equal(
+ Services.prefs.getCharPref("extensions.formautofill.addresses.supported"),
+ "on"
+ );
+ ensureOldPrefsAreCleared();
+});
+
+add_task(async function test_check_old_form_autofill_module_on_cc_off() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION - 1);
+ Services.prefs.setCharPref("extensions.formautofill.available", "on");
+ Services.prefs.setBoolPref(
+ "extensions.formautofill.creditCards.available",
+ false
+ );
+
+ // Simulate a migration.
+ gBrowserGlue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_BROWSERGLUE_TEST
+ );
+
+ // old credit card available should migrate to "off" due to
+ // "extensions.formautofill.available" being on and
+ // "extensions.formautofill.creditCards.available" having a user set value of false.
+ Assert.equal(
+ Services.prefs.getCharPref("extensions.formautofill.creditCards.supported"),
+ "off"
+ );
+
+ ensureOldPrefsAreCleared();
+});
diff --git a/browser/components/tests/unit/test_browserGlue_migration_no_errors.js b/browser/components/tests/unit/test_browserGlue_migration_no_errors.js
new file mode 100644
index 0000000000..aed88c95fe
--- /dev/null
+++ b/browser/components/tests/unit/test_browserGlue_migration_no_errors.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Since various migrations in BrowserGlue are sometimes trivial, e.g. just
+ * clearing a pref, it does not feel necessary to write tests for each of those.
+ *
+ * However, ensuring we have at least some coverage to check for errors, e.g.
+ * typos, is a good idea, hence this test.
+ *
+ * If your migration is more complex that clearing a couple of prefs, you
+ * should consider adding your own BrowserGlue migration test.
+ */
+const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
+const TOPICDATA_BROWSERGLUE_TEST = "force-ui-migration";
+
+const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
+ Ci.nsIObserver
+);
+
+// Set the migration value to 1, to ensure that the migration code is called,
+// and so that this doesn't need updating every time we obsolete old tests.
+Services.prefs.setIntPref("browser.migration.version", 1);
+
+add_task(async function test_no_errors() {
+ // Simulate a migration.
+ gBrowserGlue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_BROWSERGLUE_TEST
+ );
+
+ Assert.ok(true, "should have run the migration with no errors");
+});
diff --git a/browser/components/tests/unit/test_browserGlue_migration_places_xulstore.js b/browser/components/tests/unit/test_browserGlue_migration_places_xulstore.js
new file mode 100644
index 0000000000..7e2ae93bfe
--- /dev/null
+++ b/browser/components/tests/unit/test_browserGlue_migration_places_xulstore.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
+const TOPICDATA_BROWSERGLUE_TEST = "force-ui-migration";
+const UI_VERSION = 120;
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { PlacesUIUtils } = ChromeUtils.importESModule(
+ "resource:///modules/PlacesUIUtils.sys.mjs"
+);
+
+add_task(async function has_not_used_ctrl_tab_and_its_off() {
+ const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
+ Ci.nsIObserver
+ );
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.migration.version");
+ });
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
+
+ Services.xulStore.setValue(
+ AppConstants.BROWSER_CHROME_URL,
+ "place:test",
+ "open",
+ "true"
+ );
+
+ // Simulate a migration.
+ gBrowserGlue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_BROWSERGLUE_TEST
+ );
+
+ Assert.equal(
+ Services.xulStore.getValue(
+ AppConstants.BROWSER_CHROME_URL,
+ PlacesUIUtils.obfuscateUrlForXulStore("place:test"),
+ "open"
+ ),
+ "true"
+ );
+
+ Assert.greater(
+ Services.prefs.getIntPref("browser.migration.version"),
+ UI_VERSION,
+ "Check migration version has been bumped up"
+ );
+});
diff --git a/browser/components/tests/unit/test_browserGlue_migration_remove_pref.js b/browser/components/tests/unit/test_browserGlue_migration_remove_pref.js
new file mode 100644
index 0000000000..1178a2bd37
--- /dev/null
+++ b/browser/components/tests/unit/test_browserGlue_migration_remove_pref.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
+ Ci.nsIObserver
+);
+
+add_setup(() => {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.fixup.alternate.enabled");
+ });
+});
+
+add_task(async function browser_fixup_alternate_enabled() {
+ Services.prefs.setBoolPref("browser.fixup.alternate.enabled", true);
+ Services.prefs.setIntPref("browser.migration.version", 139);
+
+ gBrowserGlue.observe(null, "browser-glue-test", "force-ui-migration");
+
+ Assert.ok(
+ !Services.prefs.getBoolPref("browser.fixup.alternate.enabled", false),
+ "browser.fixup.alternate.enabled pref should be cleared"
+ );
+});
diff --git a/browser/components/tests/unit/test_browserGlue_migration_resetDefaults.js b/browser/components/tests/unit/test_browserGlue_migration_resetDefaults.js
new file mode 100644
index 0000000000..89de18d554
--- /dev/null
+++ b/browser/components/tests/unit/test_browserGlue_migration_resetDefaults.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
+const TOPICDATA_BROWSERGLUE_TEST = "force-ui-migration";
+const UI_VERSION = 138;
+
+const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
+ Ci.nsIObserver
+);
+
+function checkConstraint(state, origin, type) {
+ Assert.equal(
+ state,
+ Services.perms.testExactPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin),
+ type
+ ),
+ `${origin} of type ${type} was set to: ${state}`
+ );
+}
+
+// Test to check if migration resets default permissions properly.
+add_task(async function test_resettingDefaults() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.migration.version");
+ Services.perms.removeAll();
+ });
+
+ Services.perms.removeAll();
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
+
+ let pm = Services.perms;
+
+ // Origin infos for default permissions in the format [origin, type].
+ let originInfos = [
+ ["https://www.mozilla.org", "uitour"],
+ ["https://support.mozilla.org", "uitour"],
+ ["about:home", "uitour"],
+ ["about:newtab", "uitour"],
+ ["https://addons.mozilla.org", "install"],
+ ["https://support.mozilla.org", "remote-troubleshooting"],
+ ["about:welcome", "autoplay-media"],
+ ];
+
+ // Override all default permissions.
+ for (let originInfo of originInfos) {
+ pm.addFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ originInfo[0]
+ ),
+ originInfo[1],
+ pm.UNKNOWN_ACTION
+ );
+ }
+
+ // Check if the default permissions were set to UNKNOWN_ACTION.
+ for (let originInfo of originInfos) {
+ checkConstraint(pm.UNKNOWN_ACTION, originInfo[0], originInfo[1]);
+ }
+
+ // Simulate a migration.
+ gBrowserGlue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_BROWSERGLUE_TEST
+ );
+
+ // Check if the default permissions were reset.
+ for (let originInfo of originInfos) {
+ checkConstraint(pm.ALLOW_ACTION, originInfo[0], originInfo[1]);
+ }
+});
+
+// Test to check if user set permissions don't get
+// reset during migration.
+add_task(async function test_resettingDenyAction() {
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.migration.version");
+ Services.perms.removeAll();
+ });
+
+ Services.perms.removeAll();
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
+
+ let pm = Services.perms;
+ // Reset one default perm to DENY_ACTION.
+ const origin = "https://www.mozilla.org";
+ const type = "uitour";
+
+ pm.addFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin),
+ type,
+ pm.DENY_ACTION
+ );
+
+ // Check if permission was set correctly.
+ checkConstraint(pm.DENY_ACTION, origin, type);
+
+ // Simulate a migration.
+ gBrowserGlue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_BROWSERGLUE_TEST
+ );
+
+ // We expect the permission to remain unchanged.
+ checkConstraint(pm.DENY_ACTION, origin, type);
+});
diff --git a/browser/components/tests/unit/test_distribution.js b/browser/components/tests/unit/test_distribution.js
new file mode 100644
index 0000000000..a64bafb2b9
--- /dev/null
+++ b/browser/components/tests/unit/test_distribution.js
@@ -0,0 +1,202 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that preferences are properly set by distribution.ini
+ */
+
+const TOPICDATA_DISTRIBUTION_CUSTOMIZATION = "force-distribution-customization";
+const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
+
+registerCleanupFunction(async function () {
+ // Remove the distribution dir, even if the test failed, otherwise all
+ // next tests will use it.
+ let folderPath = PathUtils.join(PathUtils.profileDir, "distribution");
+ await IOUtils.remove(folderPath, { ignoreAbsent: true, recursive: true });
+ Assert.ok(!(await IOUtils.exists(folderPath)));
+ Services.prefs.clearUserPref("distribution.testing.loadFromProfile");
+});
+
+add_task(async function () {
+ // Set special pref to load distribution.ini from the profile folder.
+ Services.prefs.setBoolPref("distribution.testing.loadFromProfile", true);
+
+ // Copy distribution.ini file to the profile dir.
+ let distroDir = gProfD.clone();
+ distroDir.leafName = "distribution";
+ let iniFile = distroDir.clone();
+ iniFile.append("distribution.ini");
+ if (iniFile.exists()) {
+ iniFile.remove(false);
+ print("distribution.ini already exists, did some test forget to cleanup?");
+ }
+
+ let testDistributionFile = do_get_cwd().clone();
+ testDistributionFile.append("distribution.ini");
+ testDistributionFile.copyTo(distroDir, "distribution.ini");
+ Assert.ok(testDistributionFile.exists());
+});
+
+add_task(async function () {
+ // Force distribution.
+ let glue = Cc["@mozilla.org/browser/browserglue;1"].getService(
+ Ci.nsIObserver
+ );
+ glue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_DISTRIBUTION_CUSTOMIZATION
+ );
+
+ var defaultBranch = Services.prefs.getDefaultBranch(null);
+
+ Assert.equal(defaultBranch.getCharPref("distribution.id"), "disttest");
+ Assert.equal(defaultBranch.getCharPref("distribution.version"), "1.0");
+ Assert.equal(
+ defaultBranch.getStringPref("distribution.about"),
+ "Tèƨƭ δïƨƭřïβúƭïôñ ƒïℓè"
+ );
+
+ Assert.equal(
+ defaultBranch.getCharPref("distribution.test.string"),
+ "Test String"
+ );
+ Assert.equal(
+ defaultBranch.getCharPref("distribution.test.string.noquotes"),
+ "Test String"
+ );
+ Assert.equal(defaultBranch.getIntPref("distribution.test.int"), 777);
+ Assert.equal(defaultBranch.getBoolPref("distribution.test.bool.true"), true);
+ Assert.equal(
+ defaultBranch.getBoolPref("distribution.test.bool.false"),
+ false
+ );
+
+ Assert.throws(
+ () => defaultBranch.getCharPref("distribution.test.empty"),
+ /NS_ERROR_UNEXPECTED/
+ );
+ Assert.throws(
+ () => defaultBranch.getIntPref("distribution.test.empty"),
+ /NS_ERROR_UNEXPECTED/
+ );
+ Assert.throws(
+ () => defaultBranch.getBoolPref("distribution.test.empty"),
+ /NS_ERROR_UNEXPECTED/
+ );
+
+ Assert.equal(
+ defaultBranch.getCharPref("distribution.test.pref.locale"),
+ "en-US"
+ );
+ Assert.equal(
+ defaultBranch.getCharPref("distribution.test.pref.language.en"),
+ "en"
+ );
+ Assert.equal(
+ defaultBranch.getCharPref("distribution.test.pref.locale.en-US"),
+ "en-US"
+ );
+ Assert.throws(
+ () => defaultBranch.getCharPref("distribution.test.pref.language.de"),
+ /NS_ERROR_UNEXPECTED/
+ );
+ // This value was never set because of the empty language specific pref
+ Assert.throws(
+ () => defaultBranch.getCharPref("distribution.test.pref.language.reset"),
+ /NS_ERROR_UNEXPECTED/
+ );
+ // This value was never set because of the empty locale specific pref
+ Assert.throws(
+ () => defaultBranch.getCharPref("distribution.test.pref.locale.reset"),
+ /NS_ERROR_UNEXPECTED/
+ );
+ // This value was overridden by a locale specific setting
+ Assert.equal(
+ defaultBranch.getCharPref("distribution.test.pref.locale.set"),
+ "Locale Set"
+ );
+ // This value was overridden by a language specific setting
+ Assert.equal(
+ defaultBranch.getCharPref("distribution.test.pref.language.set"),
+ "Language Set"
+ );
+ // Language should not override locale
+ Assert.notEqual(
+ defaultBranch.getCharPref("distribution.test.pref.locale.set"),
+ "Language Set"
+ );
+
+ Assert.equal(
+ defaultBranch.getComplexValue(
+ "distribution.test.locale",
+ Ci.nsIPrefLocalizedString
+ ).data,
+ "en-US"
+ );
+ Assert.equal(
+ defaultBranch.getComplexValue(
+ "distribution.test.language.en",
+ Ci.nsIPrefLocalizedString
+ ).data,
+ "en"
+ );
+ Assert.equal(
+ defaultBranch.getComplexValue(
+ "distribution.test.locale.en-US",
+ Ci.nsIPrefLocalizedString
+ ).data,
+ "en-US"
+ );
+ Assert.throws(
+ () =>
+ defaultBranch.getComplexValue(
+ "distribution.test.language.de",
+ Ci.nsIPrefLocalizedString
+ ),
+ /NS_ERROR_UNEXPECTED/
+ );
+ // This value was never set because of the empty language specific pref
+ Assert.throws(
+ () =>
+ defaultBranch.getComplexValue(
+ "distribution.test.language.reset",
+ Ci.nsIPrefLocalizedString
+ ),
+ /NS_ERROR_UNEXPECTED/
+ );
+ // This value was never set because of the empty locale specific pref
+ Assert.throws(
+ () =>
+ defaultBranch.getComplexValue(
+ "distribution.test.locale.reset",
+ Ci.nsIPrefLocalizedString
+ ),
+ /NS_ERROR_UNEXPECTED/
+ );
+ // This value was overridden by a locale specific setting
+ Assert.equal(
+ defaultBranch.getComplexValue(
+ "distribution.test.locale.set",
+ Ci.nsIPrefLocalizedString
+ ).data,
+ "Locale Set"
+ );
+ // This value was overridden by a language specific setting
+ Assert.equal(
+ defaultBranch.getComplexValue(
+ "distribution.test.language.set",
+ Ci.nsIPrefLocalizedString
+ ).data,
+ "Language Set"
+ );
+ // Language should not override locale
+ Assert.notEqual(
+ defaultBranch.getComplexValue(
+ "distribution.test.locale.set",
+ Ci.nsIPrefLocalizedString
+ ).data,
+ "Language Set"
+ );
+ Assert.equal(defaultBranch.getCharPref("intl.locale.requested"), "en-US");
+});
diff --git a/browser/components/tests/unit/test_distribution_cachedexistence.js b/browser/components/tests/unit/test_distribution_cachedexistence.js
new file mode 100644
index 0000000000..e2d0caf2c6
--- /dev/null
+++ b/browser/components/tests/unit/test_distribution_cachedexistence.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that DistributionCustomizer correctly caches the existence
+ * of the distribution.ini file and just rechecks it after a version
+ * update.
+ */
+
+const PREF_CACHED_FILE_EXISTENCE = "distribution.iniFile.exists.value";
+const PREF_CACHED_FILE_APPVERSION = "distribution.iniFile.exists.appversion";
+const PREF_LOAD_FROM_PROFILE = "distribution.testing.loadFromProfile";
+
+const gTestDir = do_get_cwd();
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+add_task(async function () {
+ // Start with a clean slate of the prefs that control this feature.
+ Services.prefs.clearUserPref(PREF_CACHED_FILE_APPVERSION);
+ Services.prefs.clearUserPref(PREF_CACHED_FILE_EXISTENCE);
+ setupTest();
+
+ let { DistributionCustomizer } = ChromeUtils.importESModule(
+ "resource:///modules/distribution.sys.mjs"
+ );
+ let distribution = new DistributionCustomizer();
+
+ copyDistributionToProfile();
+
+ // Check that checking for distribution.ini returns the right value and sets up
+ // the cached prefs.
+ let exists = distribution._hasDistributionIni;
+
+ Assert.ok(exists);
+ Assert.equal(
+ Services.prefs.getBoolPref(PREF_CACHED_FILE_EXISTENCE, undefined),
+ true
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(PREF_CACHED_FILE_APPVERSION, "unknown"),
+ AppConstants.MOZ_APP_VERSION
+ );
+
+ // Check that calling _hasDistributionIni again will use the cached value. We do
+ // this by deleting the file and expecting it to still return true instead of false.
+ // Also, we need to delete _hasDistributionIni from the object because the getter
+ // was replaced with a stored value.
+ deleteDistribution();
+ delete distribution._hasDistributionIni;
+
+ exists = distribution._hasDistributionIni;
+ Assert.ok(exists);
+
+ // Now let's invalidate the PREF_CACHED_FILE_EXISTENCE pref to make sure the
+ // value gets recomputed correctly.
+ Services.prefs.clearUserPref(PREF_CACHED_FILE_EXISTENCE);
+ delete distribution._hasDistributionIni;
+ exists = distribution._hasDistributionIni;
+
+ // It now should return false, as well as storing false in the pref.
+ Assert.ok(!exists);
+ Assert.equal(
+ Services.prefs.getBoolPref(PREF_CACHED_FILE_EXISTENCE, undefined),
+ false
+ );
+
+ // Check now that it will use the new cached value instead of returning true in
+ // the presence of the file.
+ copyDistributionToProfile();
+ delete distribution._hasDistributionIni;
+ exists = distribution._hasDistributionIni;
+
+ Assert.ok(!exists);
+
+ // Now let's do the same, but invalidating the App Version, as if a version
+ // update occurred.
+ Services.prefs.setStringPref(PREF_CACHED_FILE_APPVERSION, "older version");
+ delete distribution._hasDistributionIni;
+ exists = distribution._hasDistributionIni;
+
+ Assert.ok(exists);
+ Assert.equal(
+ Services.prefs.getBoolPref(PREF_CACHED_FILE_EXISTENCE, undefined),
+ true
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(PREF_CACHED_FILE_APPVERSION, "unknown"),
+ AppConstants.MOZ_APP_VERSION
+ );
+});
+
+/*
+ * Helper functions
+ */
+function copyDistributionToProfile() {
+ // Copy distribution.ini file to the profile dir.
+ let distroDir = gProfD.clone();
+ distroDir.leafName = "distribution";
+ let iniFile = distroDir.clone();
+ iniFile.append("distribution.ini");
+ if (iniFile.exists()) {
+ iniFile.remove(false);
+ print("distribution.ini already exists, did some test forget to cleanup?");
+ }
+
+ let testDistributionFile = gTestDir.clone();
+ testDistributionFile.append("distribution.ini");
+ testDistributionFile.copyTo(distroDir, "distribution.ini");
+ Assert.ok(testDistributionFile.exists());
+}
+
+function deleteDistribution() {
+ let distroDir = gProfD.clone();
+ distroDir.leafName = "distribution";
+ let iniFile = distroDir.clone();
+ iniFile.append("distribution.ini");
+ iniFile.remove(false);
+}
+
+function setupTest() {
+ // Set special pref to load distribution.ini from the profile folder.
+ Services.prefs.setBoolPref(PREF_LOAD_FROM_PROFILE, true);
+}
+
+registerCleanupFunction(function () {
+ deleteDistribution();
+ Services.prefs.clearUserPref(PREF_LOAD_FROM_PROFILE);
+});
diff --git a/browser/components/tests/unit/xpcshell.toml b/browser/components/tests/unit/xpcshell.toml
new file mode 100644
index 0000000000..1b566698ee
--- /dev/null
+++ b/browser/components/tests/unit/xpcshell.toml
@@ -0,0 +1,22 @@
+[DEFAULT]
+head = "head.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"] # bug 1730213
+support-files = ["distribution.ini"]
+
+["test_browserGlue_migration_ctrltab_recently_used_order.js"]
+
+["test_browserGlue_migration_formautofill.js"]
+
+["test_browserGlue_migration_no_errors.js"]
+
+["test_browserGlue_migration_places_xulstore.js"]
+
+["test_browserGlue_migration_remove_pref.js"]
+
+["test_browserGlue_migration_resetDefaults.js"]
+
+["test_distribution.js"]
+run-sequentially = "very high failure rate in parallel"
+
+["test_distribution_cachedexistence.js"]