diff options
Diffstat (limited to '')
30 files changed, 2742 insertions, 0 deletions
diff --git a/browser/components/tests/browser/browser.ini b/browser/components/tests/browser/browser.ini new file mode 100644 index 0000000000..3fabbc6954 --- /dev/null +++ b/browser/components/tests/browser/browser.ini @@ -0,0 +1,24 @@ +[DEFAULT] +support-files = + head.js + ../../../../dom/security/test/csp/dummy.pdf + +[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_to_handle_telemetry.js] +run-if = os == 'win' +[browser_quit_multiple_tabs.js] +[browser_startup_homepage.js] +[browser_quit_disabled.js] +# On macOS we can't change browser.quitShortcut.disabled during runtime. +skip-if = os == 'mac' +[browser_quit_shortcut_warning.js] +[browser_browserGlue_showModal_trigger.js] 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..769159bc12 --- /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.import( + "resource://testing-common/NimbusTestUtils.jsm" +); +const { NimbusFeatures, ExperimentAPI } = ChromeUtils.import( + "resource://nimbus/ExperimentAPI.jsm" +); +const { OnboardingMessageProvider } = ChromeUtils.import( + "resource://activity-stream/lib/OnboardingMessageProvider.jsm" +); + +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({}, true); +}); 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..10092a8b54 --- /dev/null +++ b/browser/components/tests/browser/browser_contentpermissionprompt.js @@ -0,0 +1,182 @@ +/** + * These tests test nsBrowserGlue's nsIContentPermissionPrompt + * implementation behaviour with various types of + * nsIContentPermissionRequests. + */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { Integration } = ChromeUtils.importESModule( + "resource://gre/modules/Integration.sys.mjs" +); + +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..ca29a21bcb --- /dev/null +++ b/browser/components/tests/browser/browser_default_bookmark_toolbar_visibility.js @@ -0,0 +1,82 @@ +/* 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_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)), + ]); + + 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..d532e810cd --- /dev/null +++ b/browser/components/tests/browser/browser_default_browser_prompt.js @@ -0,0 +1,134 @@ +/* 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..de486bfeb0 --- /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..c6cb4543d5 --- /dev/null +++ b/browser/components/tests/browser/browser_startup_homepage.js @@ -0,0 +1,122 @@ +/* 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_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..8276806584 --- /dev/null +++ b/browser/components/tests/browser/head.js @@ -0,0 +1,93 @@ +/* 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", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + sinon: "resource://testing-common/Sinon.jsm", +}); + +// 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.ini b/browser/components/tests/browser/whats_new_page/browser.ini new file mode 100644 index 0000000000..c2d50c5c51 --- /dev/null +++ b/browser/components/tests/browser/whats_new_page/browser.ini @@ -0,0 +1,18 @@ +[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/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..9bd9f7d414 --- /dev/null +++ b/browser/components/tests/unit/head.js @@ -0,0 +1,6 @@ +/* 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.import("resource://testing-common/Sinon.jsm"); +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_social_cleanup.js b/browser/components/tests/unit/test_browserGlue_migration_social_cleanup.js new file mode 100644 index 0000000000..00b59202a4 --- /dev/null +++ b/browser/components/tests/unit/test_browserGlue_migration_social_cleanup.js @@ -0,0 +1,30 @@ +const UI_VERSION = 69; +const TOPIC_BROWSERGLUE_TEST = "browser-glue-test"; +const TOPICDATA_BROWSERGLUE_TEST = "force-ui-migration"; + +var gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService( + Ci.nsIObserver +); + +Services.prefs.setIntPref("browser.migration.version", UI_VERSION - 1); + +add_task(async function test_check_cleanup_social_prefs() { + Services.prefs.setStringPref("social.manifest.example-com", "example.com"); + + // Simulate a migration. + gBrowserGlue.observe( + null, + TOPIC_BROWSERGLUE_TEST, + TOPICDATA_BROWSERGLUE_TEST + ); + + Assert.ok( + !Services.prefs.prefHasUserValue("social.manifest.example-com"), + "should have cleared old social preference 'social.manifest.example-com'" + ); +}); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.migration.version"); + Services.prefs.clearUserPref("social.manifest.example-com"); +}); diff --git a/browser/components/tests/unit/test_distribution.js b/browser/components/tests/unit/test_distribution.js new file mode 100644 index 0000000000..7b46f826eb --- /dev/null +++ b/browser/components/tests/unit/test_distribution.js @@ -0,0 +1,215 @@ +/* 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 { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); + +// This test causes BrowserGlue to start but not fully initialise, when the +// AddonManager shuts down BrowserGlue will then try to uninit which will +// cause AutoComplete.jsm to throw an error. +// TODO: Fix in https://bugzilla.mozilla.org/show_bug.cgi?id=1543112. +PromiseTestUtils.allowMatchingRejectionsGlobally(/A request was aborted/); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /The operation failed for reasons unrelated/ +); + +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..192687e68f --- /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.import( + "resource:///modules/distribution.js" + ); + 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.ini b/browser/components/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..7c7257282e --- /dev/null +++ b/browser/components/tests/unit/xpcshell.ini @@ -0,0 +1,15 @@ +[DEFAULT] +head = head.js +firefox-appdir = browser +skip-if = toolkit == 'android' # bug 1730213 +support-files = + distribution.ini + +[test_browserGlue_migration_formautofill.js] +[test_browserGlue_migration_places_xulstore.js] +[test_browserGlue_migration_ctrltab_recently_used_order.js] +[test_distribution.js] +run-sequentially = very high failure rate in parallel +[test_distribution_cachedexistence.js] +[test_browserGlue_migration_no_errors.js] +[test_browserGlue_migration_social_cleanup.js] |