summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/tests/browser
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/migration/tests/browser')
-rw-r--r--browser/components/migration/tests/browser/browser.toml50
-rw-r--r--browser/components/migration/tests/browser/browser_aboutwelcome_behavior.js100
-rw-r--r--browser/components/migration/tests/browser/browser_dialog_cancel_close.js55
-rw-r--r--browser/components/migration/tests/browser/browser_dialog_open.js55
-rw-r--r--browser/components/migration/tests/browser/browser_dialog_resize.js29
-rw-r--r--browser/components/migration/tests/browser/browser_disabled_migrator.js131
-rw-r--r--browser/components/migration/tests/browser/browser_do_migration.js209
-rw-r--r--browser/components/migration/tests/browser/browser_entrypoint_telemetry.js72
-rw-r--r--browser/components/migration/tests/browser/browser_extension_migration.js238
-rw-r--r--browser/components/migration/tests/browser/browser_file_migration.js306
-rw-r--r--browser/components/migration/tests/browser/browser_ie_edge_bookmarks_success_strings.js89
-rw-r--r--browser/components/migration/tests/browser/browser_misc_telemetry.js178
-rw-r--r--browser/components/migration/tests/browser/browser_no_browsers_state.js92
-rw-r--r--browser/components/migration/tests/browser/browser_only_file_migrators.js71
-rw-r--r--browser/components/migration/tests/browser/browser_permissions.js166
-rw-r--r--browser/components/migration/tests/browser/browser_safari_passwords.js468
-rw-r--r--browser/components/migration/tests/browser/browser_safari_permissions.js136
-rw-r--r--browser/components/migration/tests/browser/dummy_file.csv1
-rw-r--r--browser/components/migration/tests/browser/head.js534
19 files changed, 2980 insertions, 0 deletions
diff --git a/browser/components/migration/tests/browser/browser.toml b/browser/components/migration/tests/browser/browser.toml
new file mode 100644
index 0000000000..637c6da345
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser.toml
@@ -0,0 +1,50 @@
+[DEFAULT]
+head = "head.js"
+prefs = [
+ "browser.migrate.internal-testing.enabled=true",
+ "dom.window.sizeToContent.enabled=true",
+]
+support-files = ["../head-common.js"]
+
+["browser_aboutwelcome_behavior.js"]
+
+["browser_dialog_cancel_close.js"]
+
+["browser_dialog_open.js"]
+
+["browser_dialog_resize.js"]
+
+["browser_disabled_migrator.js"]
+
+["browser_do_migration.js"]
+
+["browser_entrypoint_telemetry.js"]
+
+["browser_extension_migration.js"]
+skip-if = ["win11_2009"] # Bug 1840718
+
+["browser_file_migration.js"]
+skip-if = [
+ "os == 'win' && debug", # Bug 1827995
+ "a11y_checks", # Bug 1858037 to investigate intermittent a11y_checks results (fails on Try, passes on Autoland)
+]
+support-files = ["dummy_file.csv"]
+
+["browser_ie_edge_bookmarks_success_strings.js"]
+skip-if = ["a11y_checks"] # Bug 1858037 to investigate intermittent a11y_checks results (fails on Try, passes on Autoland)
+
+["browser_misc_telemetry.js"]
+skip-if = ["a11y_checks"] # Bug 1858037 to investigate intermittent a11y_checks results (fails on Autoland, passes on Try)
+
+["browser_no_browsers_state.js"]
+
+["browser_only_file_migrators.js"]
+
+["browser_permissions.js"]
+skip-if = ["a11y_checks"] # Bug 1858037 and 1855492 to investigate intermittent a11y_checks results (fails on Try, passes on Autoland)
+
+["browser_safari_passwords.js"]
+run-if = ["os == 'mac'"]
+
+["browser_safari_permissions.js"]
+run-if = ["os == 'mac'"]
diff --git a/browser/components/migration/tests/browser/browser_aboutwelcome_behavior.js b/browser/components/migration/tests/browser/browser_aboutwelcome_behavior.js
new file mode 100644
index 0000000000..72c90851e2
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_aboutwelcome_behavior.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that if browser.migrate.content-modal.about-welcome-behavior
+ * is "autoclose", that closing the migration dialog when opened with the
+ * NEWTAB entrypoint (which currently only occurs from about:welcome),
+ * will result in the about:preferences tab closing too.
+ */
+add_task(async function test_autoclose_from_welcome() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.migrate.content-modal.about-welcome-behavior", "autoclose"],
+ ],
+ });
+
+ let migrationDialogPromise = waitForMigrationWizardDialogTab();
+ MigrationUtils.showMigrationWizard(window, {
+ entrypoint: MigrationUtils.MIGRATION_ENTRYPOINTS.NEWTAB,
+ });
+
+ let prefsBrowser = await migrationDialogPromise;
+ let prefsTab = gBrowser.getTabForBrowser(prefsBrowser);
+
+ let tabClosed = BrowserTestUtils.waitForTabClosing(prefsTab);
+
+ let dialog = prefsBrowser.contentDocument.querySelector(
+ "#migrationWizardDialog"
+ );
+
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close");
+ await BrowserTestUtils.synthesizeKey("VK_ESCAPE", {}, prefsBrowser);
+ await dialogClosed;
+ await tabClosed;
+ Assert.ok(true, "Preferences tab closed with autoclose behavior.");
+});
+
+/**
+ * Tests that if browser.migrate.content-modal.about-welcome-behavior
+ * is "default", that closing the migration dialog when opened with the
+ * NEWTAB entrypoint (which currently only occurs from about:welcome),
+ * will result in the about:preferences tab still staying open.
+ */
+add_task(async function test_no_autoclose_from_welcome() {
+ // Create a new blank tab which about:preferences will open into.
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.migrate.content-modal.about-welcome-behavior", "default"]],
+ });
+
+ let migrationDialogPromise = waitForMigrationWizardDialogTab();
+ MigrationUtils.showMigrationWizard(window, {
+ entrypoint: MigrationUtils.MIGRATION_ENTRYPOINTS.NEWTAB,
+ });
+
+ let prefsBrowser = await migrationDialogPromise;
+ let prefsTab = gBrowser.getTabForBrowser(prefsBrowser);
+
+ let dialog = prefsBrowser.contentDocument.querySelector(
+ "#migrationWizardDialog"
+ );
+
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close");
+ await BrowserTestUtils.synthesizeKey("VK_ESCAPE", {}, prefsBrowser);
+ await dialogClosed;
+ Assert.ok(!prefsTab.closing, "about:preferences tab is not closing.");
+
+ BrowserTestUtils.removeTab(prefsTab);
+});
+
+/**
+ * Tests that if browser.migrate.content-modal.about-welcome-behavior
+ * is "standalone", that opening the migration wizard from the NEWTAB
+ * entrypoint opens the migration wizard in a standalone top-level
+ * window.
+ */
+add_task(async function test_no_autoclose_from_welcome() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.migrate.content-modal.about-welcome-behavior", "standalone"],
+ ],
+ });
+
+ let windowOpened = BrowserTestUtils.domWindowOpened();
+ MigrationUtils.showMigrationWizard(window, {
+ entrypoint: MigrationUtils.MIGRATION_ENTRYPOINTS.NEWTAB,
+ });
+ let dialogWin = await windowOpened;
+ Assert.ok(dialogWin, "Top-level dialog window opened for the migrator.");
+ await BrowserTestUtils.waitForEvent(dialogWin, "MigrationWizard:Ready");
+
+ let dialogClosed = BrowserTestUtils.domWindowClosed(dialogWin);
+ dialogWin.close();
+ await dialogClosed;
+});
diff --git a/browser/components/migration/tests/browser/browser_dialog_cancel_close.js b/browser/components/migration/tests/browser/browser_dialog_cancel_close.js
new file mode 100644
index 0000000000..8fe510cf30
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_dialog_cancel_close.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that pressing "Cancel" from the selection page of the migration
+ * dialog closes the dialog when opened in about:preferences as an HTML5
+ * dialog.
+ */
+add_task(async function test_cancel_close() {
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialog = prefsWin.document.querySelector("#migrationWizardDialog");
+ let wizard = dialog.querySelector("migration-wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+ let cancelButton = shadow.querySelector(
+ 'div[name="page-selection"] .cancel-close'
+ );
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close");
+ cancelButton.click();
+ await dialogClosed;
+ Assert.ok(true, "Clicking the cancel button closed the dialog.");
+ });
+});
+
+/**
+ * Tests that pressing "Cancel" from the selection page of the migration
+ * dialog closes the dialog when opened in stand-alone window.
+ */
+add_task(async function test_cancel_close() {
+ let promiseWinLoaded = BrowserTestUtils.domWindowOpened().then(win => {
+ return BrowserTestUtils.waitForEvent(win, "MigrationWizard:Ready");
+ });
+
+ let win = Services.ww.openWindow(
+ window,
+ DIALOG_URL,
+ "_blank",
+ "dialog,centerscreen",
+ {}
+ );
+ await promiseWinLoaded;
+
+ win.sizeToContent();
+ let wizard = win.document.querySelector("#wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+ let cancelButton = shadow.querySelector(
+ 'div[name="page-selection"] .cancel-close'
+ );
+
+ let windowClosed = BrowserTestUtils.windowClosed(win);
+ cancelButton.click();
+ await windowClosed;
+ Assert.ok(true, "Window was closed.");
+});
diff --git a/browser/components/migration/tests/browser/browser_dialog_open.js b/browser/components/migration/tests/browser/browser_dialog_open.js
new file mode 100644
index 0000000000..1ec43f0ea6
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_dialog_open.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that we can open the migration dialog in an about:preferences
+ * HTML5 dialog when calling MigrationUtils.showMigrationWizard within a
+ * tabbrowser window execution context.
+ */
+add_task(async function test_migration_dialog_open_in_tab_dialog_box() {
+ let migrationDialogPromise = waitForMigrationWizardDialogTab();
+ MigrationUtils.showMigrationWizard(window, {});
+ let prefsBrowser = await migrationDialogPromise;
+ Assert.ok(true, "Migration dialog was opened");
+ let dialog = prefsBrowser.contentDocument.querySelector(
+ "#migrationWizardDialog"
+ );
+
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close");
+ await BrowserTestUtils.synthesizeKey("VK_ESCAPE", {}, prefsBrowser);
+ await dialogClosed;
+ BrowserTestUtils.startLoadingURIString(prefsBrowser, "about:blank");
+ await BrowserTestUtils.browserLoaded(prefsBrowser);
+});
+
+/**
+ * Tests that we can open the migration dialog in a stand-alone window
+ * when calling MigrationUtils.showMigrationWizard with a null opener
+ * argument, or a non-tabbrowser window context.
+ */
+add_task(async function test_migration_dialog_open_in_xul_window() {
+ let firstWindowOpened = BrowserTestUtils.domWindowOpened();
+ MigrationUtils.showMigrationWizard(null, {});
+ let firstDialogWin = await firstWindowOpened;
+
+ await BrowserTestUtils.waitForEvent(firstDialogWin, "MigrationWizard:Ready");
+
+ Assert.ok(true, "Migration dialog was opened");
+
+ // Now open a second migration dialog, using the first as the window
+ // argument.
+
+ let secondWindowOpened = BrowserTestUtils.domWindowOpened();
+ MigrationUtils.showMigrationWizard(firstDialogWin, {});
+ let secondDialogWin = await secondWindowOpened;
+
+ await BrowserTestUtils.waitForEvent(secondDialogWin, "MigrationWizard:Ready");
+
+ for (let dialogWin of [firstDialogWin, secondDialogWin]) {
+ let dialogClosed = BrowserTestUtils.domWindowClosed(dialogWin);
+ dialogWin.close();
+ await dialogClosed;
+ }
+});
diff --git a/browser/components/migration/tests/browser/browser_dialog_resize.js b/browser/components/migration/tests/browser/browser_dialog_resize.js
new file mode 100644
index 0000000000..8fb05faf2c
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_dialog_resize.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that if the MigrationWizard resizes when opened inside of a
+ * XUL window, that it causes the containing XUL window to resize
+ * appropriately.
+ */
+add_task(async function test_migration_dialog_resize_in_xul_window() {
+ let windowOpened = BrowserTestUtils.domWindowOpened();
+ MigrationUtils.showMigrationWizard(null, {});
+ let dialogWin = await windowOpened;
+
+ await BrowserTestUtils.waitForEvent(dialogWin, "MigrationWizard:Ready");
+
+ let wizard = dialogWin.document.body.querySelector("#wizard");
+ let height = wizard.getBoundingClientRect().height;
+
+ let windowResizePromise = BrowserTestUtils.waitForEvent(dialogWin, "resize");
+ wizard.style.height = height + 100 + "px";
+ await windowResizePromise;
+ Assert.ok(true, "Migration dialog window resized.");
+
+ let dialogClosed = BrowserTestUtils.domWindowClosed(dialogWin);
+ dialogWin.close();
+ await dialogClosed;
+});
diff --git a/browser/components/migration/tests/browser/browser_disabled_migrator.js b/browser/components/migration/tests/browser/browser_disabled_migrator.js
new file mode 100644
index 0000000000..782666f6a6
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_disabled_migrator.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { MigratorBase } = ChromeUtils.importESModule(
+ "resource:///modules/MigratorBase.sys.mjs"
+);
+
+/**
+ * Tests that the InternalTestingProfileMigrator is listed in
+ * the new migration wizard selector when enabled.
+ */
+add_task(async function test_enabled_migrator() {
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialog = prefsWin.document.querySelector("#migrationWizardDialog");
+ let wizard = dialog.querySelector("migration-wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+ let selector = shadow.querySelector("#browser-profile-selector");
+ selector.click();
+
+ await new Promise(resolve => {
+ shadow
+ .querySelector("panel-list")
+ .addEventListener("shown", resolve, { once: true });
+ });
+
+ let panelItem = shadow.querySelector(
+ `panel-item[key="${InternalTestingProfileMigrator.key}"]`
+ );
+
+ Assert.ok(
+ panelItem,
+ "The InternalTestingProfileMigrator panel-item exists."
+ );
+ panelItem.click();
+
+ Assert.ok(
+ selector.innerText.includes("Internal Testing Migrator"),
+ "Testing for enabled internal testing migrator"
+ );
+ });
+});
+
+/**
+ * Tests that the InternalTestingProfileMigrator is not listed in
+ * the new migration wizard selector when disabled.
+ */
+add_task(async function test_disabling_migrator() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.migrate.internal-testing.enabled", false]],
+ });
+
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let internalTestingMigrator = new InternalTestingProfileMigrator();
+
+ // We create a fake migrator that we know will still be present after
+ // disabling the InternalTestingProfileMigrator so that we don't switch
+ // the wizard to the NO_BROWSERS_FOUND page, which we're not testing here.
+ let fakeMigrator = new FakeMigrator();
+
+ let getMigratorStub = sandbox.stub(MigrationUtils, "getMigrator");
+ getMigratorStub
+ .withArgs("internal-testing")
+ .resolves(internalTestingMigrator);
+ getMigratorStub.withArgs("fake-migrator").resolves(fakeMigrator);
+
+ sandbox.stub(MigrationUtils, "availableMigratorKeys").get(() => {
+ return ["internal-testing", "fake-migrator"];
+ });
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialog = prefsWin.document.querySelector("#migrationWizardDialog");
+ let wizard = dialog.querySelector("migration-wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+ let selector = shadow.querySelector("#browser-profile-selector");
+ selector.click();
+
+ await new Promise(resolve => {
+ shadow
+ .querySelector("panel-list")
+ .addEventListener("shown", resolve, { once: true });
+ });
+
+ let panelItem = shadow.querySelector(
+ `panel-item[key="${InternalTestingProfileMigrator.key}"]`
+ );
+
+ Assert.ok(
+ !panelItem,
+ "The panel-item for the InternalTestingProfileMigrator does not exist"
+ );
+ });
+
+ sandbox.restore();
+});
+
+/**
+ * A stub of a migrator used for automated testing only.
+ */
+class FakeMigrator extends MigratorBase {
+ static get key() {
+ return "fake-migrator";
+ }
+
+ static get displayNameL10nID() {
+ return "migration-wizard-migrator-display-name-firefox";
+ }
+
+ // We will create a single MigratorResource for each resource type that
+ // just immediately reports a successful migration.
+ getResources() {
+ return Object.values(MigrationUtils.resourceTypes).map(type => {
+ return {
+ type,
+ migrate: callback => {
+ callback(true /* success */);
+ },
+ };
+ });
+ }
+
+ // We need to override enabled() to always return true for testing purposes.
+ get enabled() {
+ return true;
+ }
+}
diff --git a/browser/components/migration/tests/browser/browser_do_migration.js b/browser/components/migration/tests/browser/browser_do_migration.js
new file mode 100644
index 0000000000..fab9641960
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_do_migration.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the MigrationWizard can be used to successfully migrate
+ * using the InternalTestingProfileMigrator in a few scenarios.
+ */
+add_task(async function test_successful_migrations() {
+ // Scenario 1: A single resource type is available.
+ let migration = waitForTestMigration(
+ [MigrationUtils.resourceTypes.BOOKMARKS],
+ [MigrationUtils.resourceTypes.BOOKMARKS],
+ InternalTestingProfileMigrator.testProfile
+ );
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+ let selector = shadow.querySelector("#browser-profile-selector");
+
+ await new Promise(resolve => prefsWin.requestAnimationFrame(resolve));
+ Assert.equal(shadow.activeElement, selector, "Selector should be focused.");
+
+ let wizardDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:DoneMigration"
+ );
+ selectResourceTypesAndStartMigration(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS,
+ ]);
+ await migration;
+ await wizardDone;
+
+ let dialog = prefsWin.document.querySelector("#migrationWizardDialog");
+ let doneButton = shadow.querySelector(
+ "div[name='page-progress'] .done-button"
+ );
+
+ await new Promise(resolve => prefsWin.requestAnimationFrame(resolve));
+ Assert.equal(
+ shadow.activeElement,
+ doneButton,
+ "Done button should be focused."
+ );
+
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close");
+ doneButton.click();
+ await dialogClosed;
+ assertQuantitiesShown(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS,
+ ]);
+ });
+
+ // We should make sure that the migration.time_to_produce_migrator_list
+ // scalar was set, since we know that at least one migration wizard has
+ // been opened.
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+ Assert.ok(
+ scalars["migration.time_to_produce_migrator_list"] > 0,
+ "Non-zero scalar value recorded for migration.time_to_produce_migrator_list"
+ );
+
+ // Scenario 2: Several resource types are available, but only 1
+ // is checked / expected.
+ migration = waitForTestMigration(
+ [
+ MigrationUtils.resourceTypes.BOOKMARKS,
+ MigrationUtils.resourceTypes.PASSWORDS,
+ ],
+ [MigrationUtils.resourceTypes.PASSWORDS],
+ InternalTestingProfileMigrator.testProfile
+ );
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+ let selector = shadow.querySelector("#browser-profile-selector");
+
+ await new Promise(resolve => prefsWin.requestAnimationFrame(resolve));
+ Assert.equal(shadow.activeElement, selector, "Selector should be focused.");
+
+ let wizardDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:DoneMigration"
+ );
+ selectResourceTypesAndStartMigration(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS,
+ ]);
+ await migration;
+ await wizardDone;
+
+ let dialog = prefsWin.document.querySelector("#migrationWizardDialog");
+ let doneButton = shadow.querySelector(
+ "div[name='page-progress'] .done-button"
+ );
+
+ await new Promise(resolve => prefsWin.requestAnimationFrame(resolve));
+ Assert.equal(
+ shadow.activeElement,
+ doneButton,
+ "Done button should be focused."
+ );
+
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close");
+ doneButton.click();
+ await dialogClosed;
+ assertQuantitiesShown(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS,
+ ]);
+ });
+
+ // Scenario 3: Several resource types are available, all are checked.
+ let allResourceTypeStrs = Object.values(
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES
+ ).filter(resourceStr => {
+ return !MigrationWizardConstants.PROFILE_RESET_ONLY_RESOURCE_TYPES[
+ resourceStr
+ ];
+ });
+
+ let allResourceTypes = allResourceTypeStrs.map(resourceTypeStr => {
+ return MigrationUtils.resourceTypes[resourceTypeStr];
+ });
+
+ migration = waitForTestMigration(
+ allResourceTypes,
+ allResourceTypes,
+ InternalTestingProfileMigrator.testProfile
+ );
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+ let selector = shadow.querySelector("#browser-profile-selector");
+
+ await new Promise(resolve => prefsWin.requestAnimationFrame(resolve));
+ Assert.equal(shadow.activeElement, selector, "Selector should be focused.");
+
+ let wizardDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:DoneMigration"
+ );
+ selectResourceTypesAndStartMigration(wizard, allResourceTypeStrs);
+ await migration;
+ await wizardDone;
+ assertQuantitiesShown(wizard, allResourceTypeStrs);
+ });
+});
+
+/**
+ * Tests that if somehow the Migration Wizard requests to import a
+ * resource type that the migrator doesn't have the ability to import,
+ * that it's ignored and the migration completes normally.
+ */
+add_task(async function test_invalid_resource_type() {
+ let migration = waitForTestMigration(
+ [MigrationUtils.resourceTypes.BOOKMARKS],
+ [MigrationUtils.resourceTypes.BOOKMARKS],
+ InternalTestingProfileMigrator.testProfile
+ );
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+ let wizardDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:DoneMigration"
+ );
+
+ // The Migration Wizard _shouldn't_ display anything except BOOKMARKS,
+ // since that's the only resource type that the selected migrator is
+ // supposed to currently support, but we'll check the other checkboxes
+ // even though they're hidden just to see what happens.
+ selectResourceTypesAndStartMigration(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS,
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS,
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY,
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA,
+ ]);
+ await migration;
+ await wizardDone;
+
+ let dialog = prefsWin.document.querySelector("#migrationWizardDialog");
+ let shadow = wizard.openOrClosedShadowRoot;
+ let doneButton = shadow.querySelector(
+ "div[name='page-progress'] .done-button"
+ );
+
+ await new Promise(resolve => prefsWin.requestAnimationFrame(resolve));
+ Assert.equal(
+ shadow.activeElement,
+ doneButton,
+ "Done button should be focused."
+ );
+
+ assertQuantitiesShown(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS,
+ ]);
+
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close");
+ doneButton.click();
+ await dialogClosed;
+ });
+});
diff --git a/browser/components/migration/tests/browser/browser_entrypoint_telemetry.js b/browser/components/migration/tests/browser/browser_entrypoint_telemetry.js
new file mode 100644
index 0000000000..a1bb23d7fc
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_entrypoint_telemetry.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const HISTOGRAM_ID = "FX_MIGRATION_ENTRY_POINT_CATEGORICAL";
+
+async function showThenCloseMigrationWizardViaEntrypoint(entrypoint) {
+ let openedPromise = BrowserTestUtils.waitForMigrationWizard(window);
+
+ MigrationUtils.showMigrationWizard(window, {
+ entrypoint,
+ });
+
+ let wizardTab = await openedPromise;
+ Assert.ok(wizardTab, "Migration wizard opened.");
+
+ await BrowserTestUtils.removeTab(wizardTab);
+}
+
+add_setup(async () => {
+ // Load the initial tab at example.com. This makes it so that if
+ // when we load the wizard in about:preferences, we'll load the
+ // about:preferences page in a new tab rather than overtaking the
+ // initial one. This makes cleanup of the wizard more explicit, since
+ // we can just close the tab.
+ let browser = gBrowser.selectedBrowser;
+ BrowserTestUtils.startLoadingURIString(browser, "https://example.com");
+ await BrowserTestUtils.browserLoaded(browser);
+});
+
+/**
+ * Tests that the entrypoint passed to MigrationUtils.showMigrationWizard gets
+ * written to both the FX_MIGRATION_ENTRY_POINT_CATEGORICAL histogram.
+ */
+add_task(async function test_entrypoints() {
+ let histogram = TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_ID);
+
+ // Let's arbitrarily pick the "Bookmarks" entrypoint, and make sure this
+ // is recorded.
+ await showThenCloseMigrationWizardViaEntrypoint(
+ MigrationUtils.MIGRATION_ENTRYPOINTS.BOOKMARKS
+ );
+ let entrypointIndex = getEntrypointHistogramIndex(
+ MigrationUtils.MIGRATION_ENTRYPOINTS.BOOKMARKS
+ );
+
+ TelemetryTestUtils.assertHistogram(histogram, entrypointIndex, 1);
+
+ histogram = TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_ID);
+
+ // Now let's pick the "Preferences" entrypoint, and make sure this
+ // is recorded.
+ await showThenCloseMigrationWizardViaEntrypoint(
+ MigrationUtils.MIGRATION_ENTRYPOINTS.PREFERENCES
+ );
+ entrypointIndex = getEntrypointHistogramIndex(
+ MigrationUtils.MIGRATION_ENTRYPOINTS.PREFERENCES
+ );
+
+ TelemetryTestUtils.assertHistogram(histogram, entrypointIndex, 1);
+
+ histogram = TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_ID);
+
+ // Finally, check the fallback by passing in something invalid as an entrypoint.
+ await showThenCloseMigrationWizardViaEntrypoint(undefined);
+ entrypointIndex = getEntrypointHistogramIndex(
+ MigrationUtils.MIGRATION_ENTRYPOINTS.UNKNOWN
+ );
+
+ TelemetryTestUtils.assertHistogram(histogram, entrypointIndex, 1);
+});
diff --git a/browser/components/migration/tests/browser/browser_extension_migration.js b/browser/components/migration/tests/browser/browser_extension_migration.js
new file mode 100644
index 0000000000..e9c3c65e6d
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_extension_migration.js
@@ -0,0 +1,238 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let gFluentStrings = new Localization([
+ "branding/brand.ftl",
+ "browser/migrationWizard.ftl",
+]);
+
+/**
+ * Ensures that the wizard is on the progress page and that the extension
+ * resource group matches a particular state.
+ *
+ * @param {Element} wizard
+ * The <migration-wizard> element to inspect.
+ * @param {number} state
+ * One of the constants from MigrationWizardConstants.PROGRESS_VALUE,
+ * describing what state the resource group should be in.
+ * @param {object} description
+ * An object to express more details of how the resource group should be
+ * displayed.
+ * @param {string} description.message
+ * The message that should be displayed for the resource group. This message
+ * maybe be contained in different elements depending on the state.
+ * @param {string} description.linkURL
+ * The URL for the <a> element that should be displayed to the user for the
+ * particular state.
+ * @param {string} description.linkText
+ * The text content for the <a> element that should be displayed to the user
+ * for the particular state.
+ * @returns {Promise<undefined>}
+ */
+async function assertExtensionsProgressState(wizard, state, description) {
+ let shadow = wizard.openOrClosedShadowRoot;
+
+ // Make sure that we're showing the progress page first.
+ let deck = shadow.querySelector("#wizard-deck");
+ Assert.equal(
+ deck.selectedViewName,
+ `page-${MigrationWizardConstants.PAGES.PROGRESS}`
+ );
+
+ let progressGroup = shadow.querySelector(
+ `.resource-progress-group[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS}"`
+ );
+
+ let progressIcon = progressGroup.querySelector(".progress-icon");
+ let messageText = progressGroup.querySelector("span.message-text");
+ let supportLink = progressGroup.querySelector(".support-text");
+ let extensionsSuccessLink = progressGroup.querySelector(
+ "#extensions-success-link"
+ );
+
+ if (state == MigrationWizardConstants.PROGRESS_VALUE.SUCCESS) {
+ Assert.stringMatches(progressIcon.getAttribute("state"), "success");
+ Assert.stringMatches(messageText.textContent, "");
+ Assert.stringMatches(supportLink.textContent, "");
+ await assertSuccessLink(extensionsSuccessLink, description.message);
+ } else if (state == MigrationWizardConstants.PROGRESS_VALUE.WARNING) {
+ Assert.stringMatches(progressIcon.getAttribute("state"), "warning");
+ Assert.stringMatches(messageText.textContent, description.message);
+ Assert.stringMatches(supportLink.textContent, description.linkText);
+ Assert.stringMatches(supportLink.href, description.linkURL);
+ await assertSuccessLink(extensionsSuccessLink, "");
+ } else if (state == MigrationWizardConstants.PROGRESS_VALUE.INFO) {
+ Assert.stringMatches(progressIcon.getAttribute("state"), "info");
+ Assert.stringMatches(supportLink.textContent, "");
+ await assertSuccessLink(extensionsSuccessLink, description.message);
+ }
+}
+
+/**
+ * Checks that the extensions migration success link has the right
+ * text content, and if the text content is non-blank, ensures that
+ * clicking on the link opens up about:addons in a background tab.
+ *
+ * The about:addons tab will be automatically closed before proceeding.
+ *
+ * @param {Element} link
+ * The extensions migration success link element.
+ * @param {string} message
+ * The expected string to appear in the link textContent. If the
+ * link is not expected to appear, this should be the empty string.
+ * @returns {Promise<undefined>}
+ */
+async function assertSuccessLink(link, message) {
+ Assert.stringMatches(link.textContent, message);
+ if (message) {
+ let aboutAddonsOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:addons"
+ );
+ EventUtils.synthesizeMouseAtCenter(link, {}, link.ownerGlobal);
+ let tab = await aboutAddonsOpened;
+ BrowserTestUtils.removeTab(tab);
+ }
+}
+
+/**
+ * Checks the case where no extensions were matched.
+ */
+add_task(async function test_extension_migration_no_matched_extensions() {
+ let migration = waitForTestMigration(
+ [MigrationUtils.resourceTypes.EXTENSIONS],
+ [MigrationUtils.resourceTypes.EXTENSIONS],
+ InternalTestingProfileMigrator.testProfile,
+ [MigrationUtils.resourceTypes.EXTENSIONS],
+ 3 /* totalExtensions */,
+ 0 /* matchedExtensions */
+ );
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+
+ let wizardDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:DoneMigration"
+ );
+ selectResourceTypesAndStartMigration(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS,
+ ]);
+ await migration;
+ await wizardDone;
+ await assertExtensionsProgressState(
+ wizard,
+ MigrationWizardConstants.PROGRESS_VALUE.WARNING,
+ {
+ message: await gFluentStrings.formatValue(
+ "migration-wizard-progress-no-matched-extensions"
+ ),
+ linkURL: Services.urlFormatter.formatURLPref(
+ "extensions.getAddons.link.url"
+ ),
+ linkText: await gFluentStrings.formatValue(
+ "migration-wizard-progress-extensions-addons-link"
+ ),
+ }
+ );
+ });
+});
+
+/**
+ * Checks the case where some but not all extensions were matched.
+ */
+add_task(
+ async function test_extension_migration_partially_matched_extensions() {
+ const TOTAL_EXTENSIONS = 3;
+ const TOTAL_MATCHES = 1;
+
+ let migration = waitForTestMigration(
+ [MigrationUtils.resourceTypes.EXTENSIONS],
+ [MigrationUtils.resourceTypes.EXTENSIONS],
+ InternalTestingProfileMigrator.testProfile,
+ [],
+ TOTAL_EXTENSIONS,
+ TOTAL_MATCHES
+ );
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+
+ let wizardDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:DoneMigration"
+ );
+ selectResourceTypesAndStartMigration(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS,
+ ]);
+ await migration;
+ await wizardDone;
+ await assertExtensionsProgressState(
+ wizard,
+ MigrationWizardConstants.PROGRESS_VALUE.INFO,
+ {
+ message: await gFluentStrings.formatValue(
+ "migration-wizard-progress-partial-success-extensions",
+ {
+ matched: TOTAL_MATCHES,
+ quantity: TOTAL_EXTENSIONS,
+ }
+ ),
+ linkText: await gFluentStrings.formatValue(
+ "migration-wizard-progress-extensions-support-link"
+ ),
+ }
+ );
+ });
+ }
+);
+
+/**
+ * Checks the case where all extensions were matched.
+ */
+add_task(async function test_extension_migration_fully_matched_extensions() {
+ const TOTAL_EXTENSIONS = 15;
+ const TOTAL_MATCHES = TOTAL_EXTENSIONS;
+
+ let migration = waitForTestMigration(
+ [MigrationUtils.resourceTypes.EXTENSIONS],
+ [MigrationUtils.resourceTypes.EXTENSIONS],
+ InternalTestingProfileMigrator.testProfile,
+ [],
+ TOTAL_EXTENSIONS,
+ TOTAL_MATCHES
+ );
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+
+ let wizardDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:DoneMigration"
+ );
+ selectResourceTypesAndStartMigration(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS,
+ ]);
+ await migration;
+ await wizardDone;
+ await assertExtensionsProgressState(
+ wizard,
+ MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
+ {
+ message: await gFluentStrings.formatValue(
+ "migration-wizard-progress-success-extensions",
+ {
+ quantity: TOTAL_EXTENSIONS,
+ }
+ ),
+ linkURL: "",
+ linkText: "",
+ }
+ );
+ });
+});
diff --git a/browser/components/migration/tests/browser/browser_file_migration.js b/browser/components/migration/tests/browser/browser_file_migration.js
new file mode 100644
index 0000000000..04241d29d5
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_file_migration.js
@@ -0,0 +1,306 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { FileMigratorBase } = ChromeUtils.importESModule(
+ "resource:///modules/FileMigrators.sys.mjs"
+);
+
+const DUMMY_FILEMIGRATOR_KEY = "dummy-file-migrator";
+const DUMMY_FILEPICKER_TITLE = "Some dummy file picker title";
+const DUMMY_FILTER_TITLE = "Some file type";
+const DUMMY_EXTENSION_PATTERN = "*.test";
+const TEST_FILE_PATH = getTestFilePath("dummy_file.csv");
+
+/**
+ * A subclass of FileMigratorBase that doesn't do anything, but
+ * is useful for testing.
+ *
+ * Notably, the `migrate` method is not overridden here. Tests that
+ * use this class should use Sinon to stub out the migrate method.
+ */
+class DummyFileMigrator extends FileMigratorBase {
+ static get key() {
+ return DUMMY_FILEMIGRATOR_KEY;
+ }
+
+ static get displayNameL10nID() {
+ return "migration-wizard-migrator-display-name-file-password-csv";
+ }
+
+ static get brandImage() {
+ return "chrome://branding/content/document.ico";
+ }
+
+ get enabled() {
+ return true;
+ }
+
+ get progressHeaderL10nID() {
+ return "migration-passwords-from-file-progress-header";
+ }
+
+ get successHeaderL10nID() {
+ return "migration-passwords-from-file-success-header";
+ }
+
+ async getFilePickerConfig() {
+ return Promise.resolve({
+ title: DUMMY_FILEPICKER_TITLE,
+ filters: [
+ {
+ title: DUMMY_FILTER_TITLE,
+ extensionPattern: DUMMY_EXTENSION_PATTERN,
+ },
+ ],
+ });
+ }
+
+ get displayedResourceTypes() {
+ return [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS];
+ }
+}
+
+const { MockFilePicker } = SpecialPowers;
+
+add_setup(async () => {
+ // We use MockFilePicker to simulate a native file picker, and prepare it
+ // to return a dummy file pointed at TEST_FILE_PATH. The file at
+ // TEST_FILE_PATH is not required (nor expected) to exist.
+ MockFilePicker.init(window);
+ registerCleanupFunction(() => {
+ MockFilePicker.cleanup();
+ });
+});
+
+/**
+ * Tests the flow of selecting a file migrator (in this case,
+ * the DummyFileMigrator), getting the file picker opened for it,
+ * and then passing the path of the selected file to the migrator.
+ */
+add_task(async function test_file_migration() {
+ let migrator = new DummyFileMigrator();
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ // First, use Sinon to insert our DummyFileMigrator as the only available
+ // file migrator.
+ sandbox.stub(MigrationUtils, "getFileMigrator").callsFake(() => {
+ return migrator;
+ });
+ sandbox.stub(MigrationUtils, "availableFileMigrators").get(() => {
+ return [migrator];
+ });
+
+ // This is the expected success state that our DummyFileMigrator will
+ // return as the final progress update to the migration wizard.
+ const SUCCESS_STATE = {
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]:
+ "2 added",
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_UPDATED]:
+ "1 updated",
+ };
+
+ let migrateStub = sandbox.stub(migrator, "migrate").callsFake(filePath => {
+ Assert.equal(filePath, TEST_FILE_PATH);
+ return SUCCESS_STATE;
+ });
+
+ let dummyFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ dummyFile.initWithPath(TEST_FILE_PATH);
+ let filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ Assert.ok(true, "Filepicker shown.");
+ MockFilePicker.setFiles([dummyFile]);
+ resolve();
+ };
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+
+ let wizardDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:DoneMigration"
+ );
+
+ // Now select our DummyFileMigrator from the list.
+ let selector = shadow.querySelector("#browser-profile-selector");
+ selector.click();
+
+ info("Waiting for panel-list shown");
+ await new Promise(resolve => {
+ shadow
+ .querySelector("panel-list")
+ .addEventListener("shown", resolve, { once: true });
+ });
+
+ info("Panel list shown. Clicking on panel-item");
+ let panelItem = shadow.querySelector(
+ `panel-item[key="${DUMMY_FILEMIGRATOR_KEY}"]`
+ );
+ panelItem.click();
+
+ // Selecting a file migrator from the selector should automatically
+ // open the file picker, so we await it here. Once the file is
+ // selected, migration should begin immediately.
+
+ info("Waiting for file picker");
+ await filePickerShownPromise;
+ await wizardDone;
+ Assert.ok(migrateStub.called, "Migrate on DummyFileMigrator was called.");
+
+ // At this point, with migration having completed, we should be showing
+ // the PROGRESS page with the SUCCESS_STATE represented.
+ let deck = shadow.querySelector("#wizard-deck");
+ Assert.equal(
+ deck.selectedViewName,
+ `page-${MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS}`
+ );
+
+ // We expect only the displayed resource types in SUCCESS_STATE are
+ // displayed now.
+ let progressGroups = shadow.querySelectorAll(
+ "div[name='page-file-import-progress'] .resource-progress-group"
+ );
+ for (let progressGroup of progressGroups) {
+ let expectedMessageText =
+ SUCCESS_STATE[progressGroup.dataset.resourceType];
+ if (expectedMessageText) {
+ let progressIcon = progressGroup.querySelector(".progress-icon");
+ Assert.stringMatches(
+ progressIcon.getAttribute("state"),
+ "success",
+ "Should be showing completed state."
+ );
+
+ let messageText =
+ progressGroup.querySelector(".message-text").textContent;
+ Assert.equal(messageText, expectedMessageText);
+ } else {
+ Assert.ok(
+ BrowserTestUtils.isHidden(progressGroup),
+ `Resource progress group for ${progressGroup.dataset.resourceType}` +
+ ` should be hidden.`
+ );
+ }
+ }
+ });
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that the migration wizard will go back to the selection page and
+ * show an error message if the migration for a FileMigrator throws an
+ * exception.
+ */
+add_task(async function test_file_migration_error() {
+ let migrator = new DummyFileMigrator();
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ // First, use Sinon to insert our DummyFileMigrator as the only available
+ // file migrator.
+ sandbox.stub(MigrationUtils, "getFileMigrator").callsFake(() => {
+ return migrator;
+ });
+ sandbox.stub(MigrationUtils, "availableFileMigrators").get(() => {
+ return [migrator];
+ });
+
+ const ERROR_MESSAGE = "This is my error message";
+
+ let migrateStub = sandbox.stub(migrator, "migrate").callsFake(() => {
+ throw new Error(ERROR_MESSAGE);
+ });
+
+ let dummyFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ dummyFile.initWithPath(TEST_FILE_PATH);
+ let filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ Assert.ok(true, "Filepicker shown.");
+ MockFilePicker.setFiles([dummyFile]);
+ resolve();
+ };
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+
+ let wizardDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:DoneMigration"
+ );
+
+ // Now select our DummyFileMigrator from the list.
+ let selector = shadow.querySelector("#browser-profile-selector");
+ selector.click();
+
+ info("Waiting for panel-list shown");
+ await new Promise(resolve => {
+ shadow
+ .querySelector("panel-list")
+ .addEventListener("shown", resolve, { once: true });
+ });
+
+ info("Panel list shown. Clicking on panel-item");
+ let panelItem = shadow.querySelector(
+ `panel-item[key="${DUMMY_FILEMIGRATOR_KEY}"]`
+ );
+ panelItem.click();
+
+ // Selecting a file migrator from the selector should automatically
+ // open the file picker, so we await it here. Once the file is
+ // selected, migration should begin immediately.
+
+ info("Waiting for file picker");
+ await filePickerShownPromise;
+ await wizardDone;
+ Assert.ok(migrateStub.called, "Migrate on DummyFileMigrator was called.");
+
+ // At this point, with migration having completed, we should be showing
+ // the SELECTION page again with the ERROR_MESSAGE displayed.
+ let deck = shadow.querySelector("#wizard-deck");
+ await BrowserTestUtils.waitForMutationCondition(
+ deck,
+ { attributeFilter: ["selected-view"] },
+ () => {
+ return (
+ deck.getAttribute("selected-view") ==
+ "page-" + MigrationWizardConstants.PAGES.SELECTION
+ );
+ }
+ );
+
+ Assert.equal(
+ selector.selectedPanelItem.getAttribute("key"),
+ DUMMY_FILEMIGRATOR_KEY,
+ "Should have the file migrator selected."
+ );
+
+ let errorMessageContainer = shadow.querySelector(".file-import-error");
+ Assert.ok(
+ BrowserTestUtils.isVisible(errorMessageContainer),
+ "Should be showing the error message container"
+ );
+
+ let fileImportErrorMessage = shadow.querySelector(
+ "#file-import-error-message"
+ ).textContent;
+ Assert.equal(fileImportErrorMessage, ERROR_MESSAGE);
+ });
+
+ sandbox.restore();
+});
diff --git a/browser/components/migration/tests/browser/browser_ie_edge_bookmarks_success_strings.js b/browser/components/migration/tests/browser/browser_ie_edge_bookmarks_success_strings.js
new file mode 100644
index 0000000000..34e8a8de2e
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_ie_edge_bookmarks_success_strings.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the progress strings that the Migration Wizard shows
+ * during migrations for IE and Edge uses the term "Favorites" rather
+ * then "Bookmarks".
+ */
+add_task(async function test_ie_edge_bookmarks_success_strings() {
+ for (let key of ["ie", "edge", "internal-testing"]) {
+ let sandbox = sinon.createSandbox();
+
+ sandbox.stub(InternalTestingProfileMigrator, "key").get(() => {
+ return key;
+ });
+
+ sandbox.stub(MigrationUtils, "availableMigratorKeys").get(() => {
+ return key;
+ });
+
+ let testingMigrator = new InternalTestingProfileMigrator();
+ sandbox.stub(MigrationUtils, "getMigrator").callsFake(() => {
+ return Promise.resolve(testingMigrator);
+ });
+
+ let migration = waitForTestMigration(
+ [MigrationUtils.resourceTypes.BOOKMARKS],
+ [MigrationUtils.resourceTypes.BOOKMARKS],
+ InternalTestingProfileMigrator.testProfile
+ );
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+ let wizardDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:DoneMigration"
+ );
+ selectResourceTypesAndStartMigration(
+ wizard,
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS],
+ key
+ );
+ await migration;
+
+ let dialog = prefsWin.document.querySelector("#migrationWizardDialog");
+ let shadow = wizard.openOrClosedShadowRoot;
+
+ // If we were using IE or Edge (EdgeHTLM), then the success message should
+ // include the word "favorites". Otherwise, we expect it to include
+ // the word "bookmarks".
+ let bookmarksProgressGroup = shadow.querySelector(
+ `.resource-progress-group[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS}"`
+ );
+ let messageTextElement =
+ bookmarksProgressGroup.querySelector(".message-text");
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return messageTextElement.textContent.trim();
+ });
+
+ let messageText = messageTextElement.textContent.toLowerCase();
+
+ if (key == "internal-testing") {
+ Assert.ok(
+ messageText.includes("bookmarks"),
+ `Message text should refer to bookmarks: ${messageText}.`
+ );
+ } else {
+ Assert.ok(
+ messageText.includes("favorites"),
+ `Message text should refer to favorites: ${messageText}`
+ );
+ }
+
+ let doneButton = shadow.querySelector(
+ "div[name='page-progress'] .done-button"
+ );
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close");
+ doneButton.click();
+ await dialogClosed;
+ await wizardDone;
+ });
+
+ sandbox.restore();
+ }
+});
diff --git a/browser/components/migration/tests/browser/browser_misc_telemetry.js b/browser/components/migration/tests/browser/browser_misc_telemetry.js
new file mode 100644
index 0000000000..4fc6518e49
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_misc_telemetry.js
@@ -0,0 +1,178 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ChromeProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeProfileMigrator.sys.mjs"
+);
+
+/**
+ * Tests that if the migration wizard is opened when the
+ * MOZ_UNINSTALLER_PROFILE_REFRESH environment variable is defined,
+ * that the migration.uninstaller_profile_refresh scalar is set,
+ * and the environment variable is cleared.
+ */
+add_task(async function test_uninstaller_migration() {
+ if (AppConstants.platform != "win") {
+ return;
+ }
+
+ Services.env.set("MOZ_UNINSTALLER_PROFILE_REFRESH", "1");
+ let wizardPromise = BrowserTestUtils.domWindowOpened();
+ // Opening the migration wizard this way is a blocking function, so
+ // we delegate it to a runnable.
+ executeSoon(() => {
+ MigrationUtils.showMigrationWizard(null, { isStartupMigration: true });
+ });
+ let wizardWin = await wizardPromise;
+
+ await BrowserTestUtils.waitForEvent(wizardWin, "MigrationWizard:Ready");
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "migration.uninstaller_profile_refresh",
+ 1
+ );
+
+ Assert.equal(
+ Services.env.get("MOZ_UNINSTALLER_PROFILE_REFRESH"),
+ "",
+ "Cleared MOZ_UNINSTALLER_PROFILE_REFRESH environment variable."
+ );
+ await BrowserTestUtils.closeWindow(wizardWin);
+});
+
+/**
+ * Tests that we populate the migration.discovered_migrators keyed scalar
+ * with a count of discovered browsers and profiles.
+ */
+add_task(async function test_discovered_migrators_keyed_scalar() {
+ Services.telemetry.clearScalars();
+
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ // We'll pretend that this system only has the
+ // InternalTestingProfileMigrator and ChromeProfileMigrator around to
+ // start
+ sandbox.stub(MigrationUtils, "availableMigratorKeys").get(() => {
+ return ["internal-testing", "chrome"];
+ });
+
+ // The InternalTestingProfileMigrator by default returns a single profile
+ // from `getSourceProfiles`, and now we'll just prepare the
+ // ChromeProfileMigrator to return two fake profiles.
+ sandbox.stub(ChromeProfileMigrator.prototype, "getSourceProfiles").resolves([
+ { id: "chrome-test-1", name: "Chrome test profile 1" },
+ { id: "chrome-test-2", name: "Chrome test profile 2" },
+ ]);
+
+ sandbox
+ .stub(ChromeProfileMigrator.prototype, "hasPermissions")
+ .resolves(true);
+
+ // We also need to ensure that the ChromeProfileMigrator actually has
+ // some resources to migrate, otherwise it won't get listed.
+ sandbox
+ .stub(ChromeProfileMigrator.prototype, "getResources")
+ .callsFake(() => {
+ return Promise.resolve(
+ Object.values(MigrationUtils.resourceType).map(resourceType => {
+ return {
+ type: resourceType,
+ migrate: () => {},
+ };
+ })
+ );
+ });
+
+ await withMigrationWizardDialog(async () => {
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "migration.discovered_migrators",
+ InternalTestingProfileMigrator.key,
+ 1
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "migration.discovered_migrators",
+ ChromeProfileMigrator.key,
+ 2
+ );
+ });
+
+ // Now, reset, and we'll try the case where a migrator returns `null` from
+ // `getSourceProfiles` using the InternalTestingProfileMigrator again.
+ sandbox.restore();
+
+ sandbox = sinon.createSandbox();
+
+ sandbox.stub(MigrationUtils, "availableMigratorKeys").get(() => {
+ return ["internal-testing"];
+ });
+
+ sandbox
+ .stub(ChromeProfileMigrator.prototype, "getSourceProfiles")
+ .resolves(null);
+
+ await withMigrationWizardDialog(async () => {
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "migration.discovered_migrators",
+ InternalTestingProfileMigrator.key,
+ 1
+ );
+ });
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that we write to the FX_MIGRATION_ERRORS histogram when a
+ * resource fails to migrate properly.
+ */
+add_task(async function test_fx_migration_errors() {
+ let migration = waitForTestMigration(
+ [
+ MigrationUtils.resourceTypes.BOOKMARKS,
+ MigrationUtils.resourceTypes.PASSWORDS,
+ ],
+ [
+ MigrationUtils.resourceTypes.BOOKMARKS,
+ MigrationUtils.resourceTypes.PASSWORDS,
+ ],
+ InternalTestingProfileMigrator.testProfile,
+ [MigrationUtils.resourceTypes.PASSWORDS]
+ );
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+ let wizardDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:DoneMigration"
+ );
+
+ selectResourceTypesAndStartMigration(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS,
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS,
+ ]);
+ await migration;
+ await wizardDone;
+
+ assertQuantitiesShown(
+ wizard,
+ [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS,
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS,
+ ],
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS]
+ );
+ });
+});
diff --git a/browser/components/migration/tests/browser/browser_no_browsers_state.js b/browser/components/migration/tests/browser/browser_no_browsers_state.js
new file mode 100644
index 0000000000..cd4677f31d
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_no_browsers_state.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the wizard switches to the NO_BROWSERS_FOUND page
+ * when no migrators are detected.
+ */
+add_task(async function test_browser_no_programs() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ sandbox.stub(MigrationUtils, "availableMigratorKeys").get(() => {
+ return [];
+ });
+
+ // Let's enable the Passwords CSV import by default so that it appears
+ // as a file migrator.
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.management.page.fileImport.enabled", true]],
+ });
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialog = prefsWin.document.querySelector("#migrationWizardDialog");
+ let wizard = dialog.querySelector("migration-wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+ let deck = shadow.querySelector("#wizard-deck");
+
+ await BrowserTestUtils.waitForMutationCondition(
+ deck,
+ { attributeFilter: ["selected-view"] },
+ () => {
+ return (
+ deck.getAttribute("selected-view") ==
+ "page-" + MigrationWizardConstants.PAGES.NO_BROWSERS_FOUND
+ );
+ }
+ );
+
+ Assert.ok(
+ true,
+ "Went to no browser page after attempting to search for migrators."
+ );
+ let chooseImportFromFile = shadow.querySelector("#choose-import-from-file");
+ Assert.ok(
+ !chooseImportFromFile.hidden,
+ "Selecting a file migrator should still be possible."
+ );
+ });
+
+ // Now disable all file migrators to make sure that the "Import from file"
+ // button is hidden.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.management.page.fileImport.enabled", false],
+ ["browser.migrate.bookmarks-file.enabled", false],
+ ],
+ });
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialog = prefsWin.document.querySelector("#migrationWizardDialog");
+ let wizard = dialog.querySelector("migration-wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+ let deck = shadow.querySelector("#wizard-deck");
+
+ await BrowserTestUtils.waitForMutationCondition(
+ deck,
+ { attributeFilter: ["selected-view"] },
+ () => {
+ return (
+ deck.getAttribute("selected-view") ==
+ "page-" + MigrationWizardConstants.PAGES.NO_BROWSERS_FOUND
+ );
+ }
+ );
+
+ Assert.ok(
+ true,
+ "Went to no browser page after attempting to search for migrators."
+ );
+ let chooseImportFromFile = shadow.querySelector("#choose-import-from-file");
+ Assert.ok(
+ chooseImportFromFile.hidden,
+ "Selecting a file migrator should not be possible."
+ );
+ });
+
+ sandbox.restore();
+});
diff --git a/browser/components/migration/tests/browser/browser_only_file_migrators.js b/browser/components/migration/tests/browser/browser_only_file_migrators.js
new file mode 100644
index 0000000000..ca18a8c0d5
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_only_file_migrators.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the NO_BROWSERS_FOUND page has a button to redirect to the
+ * selection page when only file migrators are found.
+ */
+add_task(async function test_only_file_migrators() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.management.page.fileImport.enabled", true]],
+ });
+
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ sandbox.stub(MigrationUtils, "availableMigratorKeys").get(() => {
+ return [];
+ });
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialog = prefsWin.document.querySelector("#migrationWizardDialog");
+ let wizard = dialog.querySelector("migration-wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+ let deck = shadow.querySelector("#wizard-deck");
+
+ await BrowserTestUtils.waitForMutationCondition(
+ deck,
+ { attributeFilter: ["selected-view"] },
+ () => {
+ return (
+ deck.getAttribute("selected-view") ==
+ "page-" + MigrationWizardConstants.PAGES.NO_BROWSERS_FOUND
+ );
+ }
+ );
+
+ let chooseImportFileButton = shadow.querySelector(
+ "#choose-import-from-file"
+ );
+
+ let changedToSelectionPage = BrowserTestUtils.waitForMutationCondition(
+ deck,
+ { attributeFilter: ["selected-view"] },
+ () => {
+ return (
+ deck.getAttribute("selected-view") ==
+ "page-" + MigrationWizardConstants.PAGES.SELECTION
+ );
+ }
+ );
+ chooseImportFileButton.click();
+ await changedToSelectionPage;
+
+ // No browser migrators should be listed.
+ let browserMigratorItems = shadow.querySelectorAll(
+ `panel-item[type="${MigrationWizardConstants.MIGRATOR_TYPES.BROWSER}"]`
+ );
+ Assert.ok(!browserMigratorItems.length, "No browser migrators listed.");
+
+ // Check to make sure there's at least one file migrator listed.
+ let fileMigratorItems = shadow.querySelectorAll(
+ `panel-item[type="${MigrationWizardConstants.MIGRATOR_TYPES.FILE}"]`
+ );
+
+ Assert.ok(!!fileMigratorItems.length, "Listed at least one file migrator.");
+ });
+});
diff --git a/browser/components/migration/tests/browser/browser_permissions.js b/browser/components/migration/tests/browser/browser_permissions.js
new file mode 100644
index 0000000000..35d902bb37
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_permissions.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.migrate.chrome.get_permissions.enabled", true]],
+ });
+});
+
+/**
+ * Tests that the migration wizard can request permission from
+ * the user to read from other browser data directories when
+ * explicit permission needs to be granted.
+ *
+ * This can occur when, for example, Firefox is installed as a
+ * Snap on Ubuntu Linux. In this state, Firefox does not have
+ * direct read access to other browser's data directories (although)
+ * it can tell if they exist. For Chromium-based browsers, this
+ * means we cannot tell what profiles nor resources are available
+ * for Chromium-based browsers without read permissions.
+ *
+ * Note that the Safari migrator is not tested here, as it has
+ * its own special permission flow. This is because we can
+ * determine what resources Safari has before requiring permissions,
+ * and (as of this writing) Safari does not support multiple
+ * user profiles.
+ */
+add_task(async function test_permissions() {
+ Services.telemetry.clearEvents();
+
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ sandbox
+ .stub(InternalTestingProfileMigrator.prototype, "canGetPermissions")
+ .resolves("/some/path");
+
+ let hasPermissionsStub = sandbox
+ .stub(InternalTestingProfileMigrator.prototype, "hasPermissions")
+ .resolves(false);
+
+ let testingMigrator = await MigrationUtils.getMigrator(
+ InternalTestingProfileMigrator.key
+ );
+ Assert.ok(
+ testingMigrator,
+ "Got migrator, even though we don't yet have permission to read its resources."
+ );
+
+ sandbox.stub(testingMigrator, "getPermissions").callsFake(async () => {
+ testingMigrator.flushResourceCache();
+ hasPermissionsStub.resolves(true);
+ return Promise.resolve(true);
+ });
+
+ let getResourcesStub = sandbox
+ .stub(testingMigrator, "getResources")
+ .resolves([]);
+
+ let migration = waitForTestMigration(
+ [MigrationUtils.resourceTypes.BOOKMARKS],
+ [MigrationUtils.resourceTypes.BOOKMARKS],
+ InternalTestingProfileMigrator.testProfile
+ );
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+
+ // Clear out any pre-existing events events have been logged
+ Services.telemetry.clearEvents();
+ TelemetryTestUtils.assertNumberOfEvents(0);
+
+ let panelItem = shadow.querySelector(
+ `panel-item[key="${InternalTestingProfileMigrator.key}"]`
+ );
+ panelItem.click();
+
+ let resourceList = shadow.querySelector(".resource-selection-details");
+ Assert.ok(
+ BrowserTestUtils.isHidden(resourceList),
+ "Resources list is hidden."
+ );
+ let importButton = shadow.querySelector("#import");
+ Assert.ok(BrowserTestUtils.isHidden(importButton), "Import button hidden.");
+ let noPermissionsMessage = shadow.querySelector(".no-permissions-message");
+ Assert.ok(
+ BrowserTestUtils.isVisible(noPermissionsMessage),
+ "No permissions message shown."
+ );
+ let getPermissionButton = shadow.querySelector("#get-permissions");
+ Assert.ok(
+ BrowserTestUtils.isVisible(getPermissionButton),
+ "Get permissions button shown."
+ );
+
+ // Now put the permissions functions back into their default
+ // state - which is the "permission granted" state.
+ getResourcesStub.restore();
+ hasPermissionsStub.restore();
+
+ let refreshDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:Ready"
+ );
+
+ getPermissionButton.click();
+
+ await refreshDone;
+ Assert.ok(true, "Refreshed migrator list.");
+
+ let wizardDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:DoneMigration"
+ );
+ selectResourceTypesAndStartMigration(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS,
+ ]);
+ await migration;
+ await wizardDone;
+
+ assertQuantitiesShown(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS,
+ ]);
+
+ let dialog = prefsWin.document.querySelector("#migrationWizardDialog");
+ let doneButton = shadow.querySelector(
+ "div[name='page-progress'] .done-button"
+ );
+
+ await new Promise(resolve => prefsWin.requestAnimationFrame(resolve));
+ Assert.equal(
+ shadow.activeElement,
+ doneButton,
+ "Done button should be focused."
+ );
+
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close");
+ doneButton.click();
+ await dialogClosed;
+ });
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "browser.migration",
+ method: "linux_perms",
+ object: "wizard",
+ value: null,
+ extra: {
+ migrator_key: InternalTestingProfileMigrator.key,
+ },
+ },
+ ],
+ {
+ category: "browser.migration",
+ method: "linux_perms",
+ object: "wizard",
+ }
+ );
+});
diff --git a/browser/components/migration/tests/browser/browser_safari_passwords.js b/browser/components/migration/tests/browser/browser_safari_passwords.js
new file mode 100644
index 0000000000..c005342b46
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_safari_passwords.js
@@ -0,0 +1,468 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { SafariProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/SafariProfileMigrator.sys.mjs"
+);
+const { LoginCSVImport } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginCSVImport.sys.mjs"
+);
+
+const TEST_FILE_PATH = getTestFilePath("dummy_file.csv");
+
+// We use MockFilePicker to simulate a native file picker, and prepare it
+// to return a dummy file pointed at TEST_FILE_PATH. The file at
+// TEST_FILE_PATH is not required (nor expected) to exist.
+const { MockFilePicker } = SpecialPowers;
+
+add_setup(async function () {
+ MockFilePicker.init(window);
+ registerCleanupFunction(() => {
+ MockFilePicker.cleanup();
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.management.page.fileImport.enabled", true]],
+ });
+});
+
+/**
+ * A helper function that does most of the heavy lifting for the tests in
+ * this file. Specfically, it takes care of:
+ *
+ * 1. Stubbing out the various hunks of the SafariProfileMigrator in order
+ * to simulate a migration without actually performing one, since the
+ * migrator itself isn't being tested here.
+ * 2. Stubbing out parts of MigrationUtils and LoginCSVImport to have a
+ * consistent reporting on how many things are imported.
+ * 3. Setting up the MockFilePicker if expectsFilePicker is true to return
+ * the TEST_FILE_PATH.
+ * 4. Opens up the migration wizard, and chooses to import both BOOKMARKS
+ * and PASSWORDS, and then clicks "Import".
+ * 5. Waits for the migration wizard to show the Safari password import
+ * instructions.
+ * 6. Runs taskFn
+ * 7. Closes the migration dialog.
+ *
+ * @param {boolean} expectsFilePicker
+ * True if the MockFilePicker should be set up to return TEST_FILE_PATH.
+ * @param {boolean} migrateBookmarks
+ * True if bookmarks should be migrated alongside passwords. If not, only
+ * passwords will be migrated.
+ * @param {boolean} shouldPasswordImportFail
+ * True if importing from the CSV file should fail.
+ * @param {Function} taskFn
+ * An asynchronous function that takes the following parameters in this
+ * order:
+ *
+ * {Element} wizard
+ * The opened migration wizard
+ * {Promise} filePickerShownPromise
+ * A Promise that resolves once the MockFilePicker has closed. This is
+ * undefined if expectsFilePicker was false.
+ * {object} importFromCSVStub
+ * The Sinon stub object for LoginCSVImport.importFromCSV. This can be
+ * used to check to see whether it was called.
+ * {Promise} didMigration
+ * A Promise that resolves to true once the migration completes.
+ * {Promise} wizardDone
+ * A Promise that resolves once the migration wizard reports that a
+ * migration has completed.
+ * @returns {Promise<undefined>}
+ */
+async function testSafariPasswordHelper(
+ expectsFilePicker,
+ migrateBookmarks,
+ shouldPasswordImportFail,
+ taskFn
+) {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let safariMigrator = new SafariProfileMigrator();
+ sandbox.stub(MigrationUtils, "getMigrator").resolves(safariMigrator);
+
+ // We're not testing the permission flow here, so let's pretend that we
+ // always have permission to read resources from the disk.
+ sandbox
+ .stub(SafariProfileMigrator.prototype, "hasPermissions")
+ .resolves(true);
+
+ // Have the migrator claim that only BOOKMARKS are only available.
+ sandbox
+ .stub(SafariProfileMigrator.prototype, "getMigrateData")
+ .resolves(MigrationUtils.resourceTypes.BOOKMARKS);
+
+ let migrateStub;
+ let didMigration = new Promise(resolve => {
+ migrateStub = sandbox
+ .stub(SafariProfileMigrator.prototype, "migrate")
+ .callsFake((aResourceTypes, aStartup, aProfile, aProgressCallback) => {
+ if (!migrateBookmarks) {
+ Assert.ok(
+ false,
+ "Should not have called migrate when only migrating Safari passwords."
+ );
+ }
+
+ Assert.ok(
+ !aStartup,
+ "Migrator should not have been called as a startup migration."
+ );
+ Assert.ok(
+ aResourceTypes & MigrationUtils.resourceTypes.BOOKMARKS,
+ "Should have requested to migrate the BOOKMARKS resource."
+ );
+ Assert.ok(
+ !(aResourceTypes & MigrationUtils.resourceTypes.PASSWORDS),
+ "Should not have requested to migrate the PASSWORDS resource."
+ );
+
+ aProgressCallback(MigrationUtils.resourceTypes.BOOKMARKS, true);
+ Services.obs.notifyObservers(null, "Migration:Ended");
+ resolve();
+ });
+ });
+
+ // We'll pretend we added EXPECTED_QUANTITY passwords from the Safari
+ // password file.
+ let results = [];
+ for (let i = 0; i < EXPECTED_QUANTITY; ++i) {
+ results.push({ result: "added" });
+ }
+ let importFromCSVStub = sandbox.stub(LoginCSVImport, "importFromCSV");
+
+ if (shouldPasswordImportFail) {
+ importFromCSVStub.rejects(new Error("Some error message"));
+ } else {
+ importFromCSVStub.resolves(results);
+ }
+
+ sandbox.stub(MigrationUtils, "_importQuantities").value({
+ bookmarks: EXPECTED_QUANTITY,
+ });
+
+ let filePickerShownPromise;
+
+ if (expectsFilePicker) {
+ MockFilePicker.reset();
+
+ let dummyFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ dummyFile.initWithPath(TEST_FILE_PATH);
+ filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ Assert.ok(true, "Filepicker shown.");
+ MockFilePicker.setFiles([dummyFile]);
+ resolve();
+ };
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ }
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+ let wizardDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:DoneMigration"
+ );
+
+ let shadow = wizard.openOrClosedShadowRoot;
+
+ info("Choosing Safari");
+ let panelItem = shadow.querySelector(
+ `panel-item[key="${SafariProfileMigrator.key}"]`
+ );
+ panelItem.click();
+
+ let resourceTypeList = shadow.querySelector("#resource-type-list");
+
+ // Let's choose whether to import BOOKMARKS first.
+ let bookmarksNode = resourceTypeList.querySelector(
+ `label[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS}"]`
+ );
+ bookmarksNode.control.checked = migrateBookmarks;
+
+ // Let's make sure that PASSWORDS is displayed despite the migrator only
+ // (currently) returning BOOKMARKS as an available resource to migrate.
+ let passwordsNode = resourceTypeList.querySelector(
+ `label[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS}"]`
+ );
+ Assert.ok(
+ !passwordsNode.hidden,
+ "PASSWORDS should be available to import from."
+ );
+ passwordsNode.control.checked = true;
+
+ let deck = shadow.querySelector("#wizard-deck");
+ let switchedToSafariPermissionPage =
+ BrowserTestUtils.waitForMutationCondition(
+ deck,
+ { attributeFilter: ["selected-view"] },
+ () => {
+ return (
+ deck.getAttribute("selected-view") ==
+ "page-" + MigrationWizardConstants.PAGES.SAFARI_PASSWORD_PERMISSION
+ );
+ }
+ );
+
+ let importButton = shadow.querySelector("#import");
+ importButton.click();
+ await switchedToSafariPermissionPage;
+ Assert.ok(true, "Went to Safari permission page after attempting import.");
+
+ await taskFn(
+ wizard,
+ filePickerShownPromise,
+ importFromCSVStub,
+ didMigration,
+ migrateStub,
+ wizardDone
+ );
+
+ let dialog = prefsWin.document.querySelector("#migrationWizardDialog");
+ let doneButton = shadow.querySelector(
+ "div[name='page-progress'] .done-button"
+ );
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close");
+
+ doneButton.click();
+ await dialogClosed;
+ });
+
+ sandbox.restore();
+ MockFilePicker.reset();
+}
+
+/**
+ * Tests the flow of importing passwords from Safari via an
+ * exported CSV file.
+ */
+add_task(async function test_safari_password_do_import() {
+ await testSafariPasswordHelper(
+ true,
+ true,
+ false,
+ async (
+ wizard,
+ filePickerShownPromise,
+ importFromCSVStub,
+ didMigration,
+ migrateStub,
+ wizardDone
+ ) => {
+ let shadow = wizard.openOrClosedShadowRoot;
+ let safariPasswordImportSelect = shadow.querySelector(
+ "#safari-password-import-select"
+ );
+ safariPasswordImportSelect.click();
+ await filePickerShownPromise;
+ Assert.ok(true, "File picker was shown.");
+
+ await didMigration;
+ Assert.ok(importFromCSVStub.called, "Importing from CSV was called.");
+
+ await wizardDone;
+
+ assertQuantitiesShown(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS,
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS,
+ ]);
+ }
+ );
+});
+
+/**
+ * Tests that only passwords get imported if the user only opts
+ * to import passwords, and that nothing else gets imported.
+ */
+add_task(async function test_safari_password_only_do_import() {
+ await testSafariPasswordHelper(
+ true,
+ false,
+ false,
+ async (
+ wizard,
+ filePickerShownPromise,
+ importFromCSVStub,
+ didMigration,
+ migrateStub,
+ wizardDone
+ ) => {
+ let shadow = wizard.openOrClosedShadowRoot;
+ let safariPasswordImportSelect = shadow.querySelector(
+ "#safari-password-import-select"
+ );
+ safariPasswordImportSelect.click();
+ await filePickerShownPromise;
+ Assert.ok(true, "File picker was shown.");
+
+ await wizardDone;
+
+ assertQuantitiesShown(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS,
+ ]);
+
+ Assert.ok(importFromCSVStub.called, "Importing from CSV was called.");
+ Assert.ok(
+ !migrateStub.called,
+ "SafariProfileMigrator.migrate was not called."
+ );
+ }
+ );
+});
+
+/**
+ * Tests the flow of importing passwords from Safari when the file
+ * import fails.
+ */
+add_task(async function test_safari_password_empty_csv_file() {
+ await testSafariPasswordHelper(
+ true,
+ true,
+ true,
+ async (
+ wizard,
+ filePickerShownPromise,
+ importFromCSVStub,
+ didMigration,
+ migrateStub,
+ wizardDone
+ ) => {
+ let shadow = wizard.openOrClosedShadowRoot;
+ let safariPasswordImportSelect = shadow.querySelector(
+ "#safari-password-import-select"
+ );
+ safariPasswordImportSelect.click();
+ await filePickerShownPromise;
+ Assert.ok(true, "File picker was shown.");
+
+ await didMigration;
+ Assert.ok(importFromCSVStub.called, "Importing from CSV was called.");
+
+ await wizardDone;
+
+ let headerL10nID =
+ shadow.querySelector("#progress-header").dataset.l10nId;
+ Assert.equal(
+ headerL10nID,
+ "migration-wizard-progress-done-with-warnings-header"
+ );
+
+ let progressGroup = shadow.querySelector(
+ `.resource-progress-group[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS}"`
+ );
+ let progressIcon = progressGroup.querySelector(".progress-icon");
+ let messageText =
+ progressGroup.querySelector(".message-text").textContent;
+
+ Assert.equal(
+ progressIcon.getAttribute("state"),
+ "warning",
+ "Icon should be in the warning state."
+ );
+ Assert.stringMatches(
+ messageText,
+ /file doesn’t include any valid password data/
+ );
+ }
+ );
+});
+
+/**
+ * Tests that the user can skip importing passwords from Safari.
+ */
+add_task(async function test_safari_password_skip() {
+ await testSafariPasswordHelper(
+ false,
+ true,
+ false,
+ async (
+ wizard,
+ filePickerShownPromise,
+ importFromCSVStub,
+ didMigration,
+ migrateStub,
+ wizardDone
+ ) => {
+ let shadow = wizard.openOrClosedShadowRoot;
+ let safariPasswordImportSkip = shadow.querySelector(
+ "#safari-password-import-skip"
+ );
+ safariPasswordImportSkip.click();
+
+ await didMigration;
+ Assert.ok(!MockFilePicker.shown, "Never showed the file picker.");
+ Assert.ok(
+ !importFromCSVStub.called,
+ "Importing from CSV was never called."
+ );
+
+ await wizardDone;
+
+ assertQuantitiesShown(wizard, [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS,
+ ]);
+ }
+ );
+});
+
+/**
+ * Tests that importing from passwords for Safari doesn't exist if
+ * signon.management.page.fileImport.enabled is false.
+ */
+add_task(async function test_safari_password_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.management.page.fileImport.enabled", false]],
+ });
+
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let safariMigrator = new SafariProfileMigrator();
+ sandbox.stub(MigrationUtils, "getMigrator").resolves(safariMigrator);
+
+ // We're not testing the permission flow here, so let's pretend that we
+ // always have permission to read resources from the disk.
+ sandbox
+ .stub(SafariProfileMigrator.prototype, "hasPermissions")
+ .resolves(true);
+
+ // Have the migrator claim that only BOOKMARKS are only available.
+ sandbox
+ .stub(SafariProfileMigrator.prototype, "getMigrateData")
+ .resolves(MigrationUtils.resourceTypes.BOOKMARKS);
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+
+ let shadow = wizard.openOrClosedShadowRoot;
+
+ info("Choosing Safari");
+ let panelItem = shadow.querySelector(
+ `panel-item[key="${SafariProfileMigrator.key}"]`
+ );
+ panelItem.click();
+
+ let resourceTypeList = shadow.querySelector("#resource-type-list");
+
+ // Let's make sure that PASSWORDS is displayed despite the migrator only
+ // (currently) returning BOOKMARKS as an available resource to migrate.
+ let passwordsNode = resourceTypeList.querySelector(
+ `label[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS}"]`
+ );
+ Assert.ok(
+ passwordsNode.hidden,
+ "PASSWORDS should not be available to import from."
+ );
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/migration/tests/browser/browser_safari_permissions.js b/browser/components/migration/tests/browser/browser_safari_permissions.js
new file mode 100644
index 0000000000..bac56866f0
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_safari_permissions.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { SafariProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/SafariProfileMigrator.sys.mjs"
+);
+
+/**
+ * Tests that if we don't have permission to read the contents
+ * of ~/Library/Safari, that we can ask for permission to do that.
+ *
+ * This involves presenting the user with some instructions, and then
+ * showing a native folder picker for the user to select the
+ * ~/Library/Safari folder. This seems to give us read access to the
+ * folder contents.
+ *
+ * Revoking permissions for reading the ~/Library/Safari folder is
+ * not something that we know how to do just yet. It seems to be
+ * something involving macOS's System Integrity Protection. This test
+ * mocks out and simulates the actual permissions mechanism to make
+ * this test run reliably and repeatably.
+ */
+add_task(async function test_safari_permissions() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let hasPermissionsStub = sandbox
+ .stub(SafariProfileMigrator.prototype, "hasPermissions")
+ .resolves(false);
+
+ let safariMigrator = await MigrationUtils.getMigrator(
+ SafariProfileMigrator.key
+ );
+ Assert.ok(
+ safariMigrator,
+ "Got migrator, even though we don't yet have permission to read its resources."
+ );
+
+ sandbox.stub(MigrationUtils, "getMigrator").resolves(safariMigrator);
+
+ sandbox.stub(safariMigrator, "getPermissions").callsFake(async () => {
+ hasPermissionsStub.resolves(true);
+ return Promise.resolve(true);
+ });
+
+ sandbox.stub(safariMigrator, "getResources").callsFake(() => {
+ return Promise.resolve([
+ {
+ type: MigrationUtils.resourceTypes.BOOKMARKS,
+ migrate: () => {},
+ },
+ ]);
+ });
+
+ let didMigration = new Promise(resolve => {
+ sandbox
+ .stub(safariMigrator, "migrate")
+ .callsFake((aResourceTypes, aStartup, aProfile, aProgressCallback) => {
+ Assert.ok(
+ !aStartup,
+ "Migrator should not have been called as a startup migration."
+ );
+
+ aProgressCallback(MigrationUtils.resourceTypes.BOOKMARKS);
+ Services.obs.notifyObservers(null, "Migration:Ended");
+ resolve();
+ });
+ });
+
+ await withMigrationWizardDialog(async prefsWin => {
+ let dialogBody = prefsWin.document.body;
+ let wizard = dialogBody.querySelector("migration-wizard");
+ let wizardDone = BrowserTestUtils.waitForEvent(
+ wizard,
+ "MigrationWizard:DoneMigration"
+ );
+
+ let shadow = wizard.openOrClosedShadowRoot;
+
+ info("Choosing Safari");
+ let panelItem = shadow.querySelector(
+ `panel-item[key="${SafariProfileMigrator.key}"]`
+ );
+ panelItem.click();
+
+ // Let's just choose "Bookmarks" for now.
+ let resourceTypeList = shadow.querySelector("#resource-type-list");
+ let resourceNodes = resourceTypeList.querySelectorAll(
+ `label[data-resource-type]`
+ );
+ for (let resourceNode of resourceNodes) {
+ resourceNode.control.checked =
+ resourceNode.dataset.resourceType ==
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS;
+ }
+
+ let deck = shadow.querySelector("#wizard-deck");
+ let switchedToSafariPermissionPage =
+ BrowserTestUtils.waitForMutationCondition(
+ deck,
+ { attributeFilter: ["selected-view"] },
+ () => {
+ return (
+ deck.getAttribute("selected-view") ==
+ "page-" + MigrationWizardConstants.PAGES.SAFARI_PERMISSION
+ );
+ }
+ );
+
+ let importButton = shadow.querySelector("#import");
+ importButton.click();
+ await switchedToSafariPermissionPage;
+ Assert.ok(true, "Went to Safari permission page after attempting import.");
+
+ let requestPermissions = shadow.querySelector(
+ "#safari-request-permissions"
+ );
+ requestPermissions.click();
+ await didMigration;
+ Assert.ok(true, "Completed migration");
+
+ let dialog = prefsWin.document.querySelector("#migrationWizardDialog");
+ let doneButton = shadow.querySelector(
+ "div[name='page-progress'] .done-button"
+ );
+ let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close");
+
+ doneButton.click();
+ await dialogClosed;
+ await wizardDone;
+ });
+});
diff --git a/browser/components/migration/tests/browser/dummy_file.csv b/browser/components/migration/tests/browser/dummy_file.csv
new file mode 100644
index 0000000000..48a099ab76
--- /dev/null
+++ b/browser/components/migration/tests/browser/dummy_file.csv
@@ -0,0 +1 @@
+This file intentionally left blank. \ No newline at end of file
diff --git a/browser/components/migration/tests/browser/head.js b/browser/components/migration/tests/browser/head.js
new file mode 100644
index 0000000000..d3d188a7e1
--- /dev/null
+++ b/browser/components/migration/tests/browser/head.js
@@ -0,0 +1,534 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../head-common.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/migration/tests/browser/head-common.js",
+ this
+);
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const { InternalTestingProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/InternalTestingProfileMigrator.sys.mjs"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const DIALOG_URL =
+ "chrome://browser/content/migration/migration-dialog-window.html";
+
+/**
+ * We'll have this be our magic number of quantities of various imports.
+ * We will use Sinon to prepare MigrationUtils to presume that this was
+ * how many of each quantity-supported resource type was imported.
+ */
+const EXPECTED_QUANTITY = 123;
+
+/**
+ * These are the resource types that currently display their import success
+ * message with a quantity.
+ */
+const RESOURCE_TYPES_WITH_QUANTITIES = [
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS,
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY,
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS,
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA,
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PAYMENT_METHODS,
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS,
+];
+
+/**
+ * The withMigrationWizardDialog callback, called after the
+ * dialog has loaded and the wizard is ready.
+ *
+ * @callback withMigrationWizardDialogCallback
+ * @param {DOMWindow} window
+ * The content window of the migration wizard subdialog frame.
+ * @returns {Promise<undefined>}
+ */
+
+/**
+ * Opens the migration wizard HTML5 dialog in about:preferences in the
+ * current window's selected tab, runs an async taskFn, and then
+ * cleans up by loading about:blank in the tab before resolving.
+ *
+ * @param {withMigrationWizardDialogCallback} taskFn
+ * An async test function to be called while the migration wizard
+ * dialog is open.
+ * @returns {Promise<undefined>}
+ */
+async function withMigrationWizardDialog(taskFn) {
+ let migrationDialogPromise = waitForMigrationWizardDialogTab();
+ await MigrationUtils.showMigrationWizard(window, {});
+ let prefsBrowser = await migrationDialogPromise;
+
+ try {
+ await taskFn(prefsBrowser.contentWindow);
+ } finally {
+ if (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.getTabForBrowser(prefsBrowser));
+ } else {
+ BrowserTestUtils.startLoadingURIString(prefsBrowser, "about:blank");
+ await BrowserTestUtils.browserLoaded(prefsBrowser);
+ }
+ }
+}
+
+/**
+ * Returns a Promise that resolves when an about:preferences tab opens
+ * in the current window which loads the migration wizard dialog.
+ * The Promise will wait until the migration wizard reports that it
+ * is ready with the "MigrationWizard:Ready" event.
+ *
+ * @returns {Promise<browser>}
+ * Resolves with the about:preferences browser element.
+ */
+async function waitForMigrationWizardDialogTab() {
+ let wizardReady = BrowserTestUtils.waitForEvent(
+ window,
+ "MigrationWizard:Ready"
+ );
+
+ let tab;
+ if (gBrowser.selectedTab.isEmpty) {
+ tab = gBrowser.selectedTab;
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url => {
+ return url.startsWith("about:preferences");
+ });
+ } else {
+ tab = await BrowserTestUtils.waitForNewTab(gBrowser, url => {
+ return url.startsWith("about:preferences");
+ });
+ }
+
+ await wizardReady;
+ info("Done waiting - migration subdialog loaded and ready.");
+
+ return tab.linkedBrowser;
+}
+
+/**
+ * A helper function that prepares the InternalTestingProfileMigrator
+ * with some set of fake available resources, and resolves a Promise
+ * when the InternalTestingProfileMigrator is used for a migration.
+ *
+ * @param {number[]} availableResourceTypes
+ * An array of resource types from MigrationUtils.resourcesTypes.
+ * A single MigrationResource will be created per type, with a
+ * no-op migrate function.
+ * @param {number[]} expectedResourceTypes
+ * An array of resource types from MigrationUtils.resourceTypes.
+ * These are the resource types that are expected to be passed
+ * to the InternalTestingProfileMigrator.migrate function.
+ * @param {object|string} expectedProfile
+ * The profile object or string that is expected to be passed
+ * to the InternalTestingProfileMigrator.migrate function.
+ * @param {number[]} [errorResourceTypes=[]]
+ * Resource types that we should pretend have failed to complete
+ * their migration properly.
+ * @param {number} [totalExtensions=1]
+ * If migrating extensions, the total that should be reported to
+ * have been found from the source browser.
+ * @param {number} [matchedExtensions=1]
+ * If migrating extensions, the number of extensions that should
+ * be reported as having equivalent matches for this browser.
+ * @returns {Promise<undefined>}
+ */
+async function waitForTestMigration(
+ availableResourceTypes,
+ expectedResourceTypes,
+ expectedProfile,
+ errorResourceTypes = [],
+ totalExtensions = 1,
+ matchedExtensions = 1
+) {
+ let sandbox = sinon.createSandbox();
+ let sourceHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "FX_MIGRATION_SOURCE_BROWSER"
+ );
+ let usageHistogram =
+ TelemetryTestUtils.getAndClearKeyedHistogram("FX_MIGRATION_USAGE");
+ let errorHistogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "FX_MIGRATION_ERRORS"
+ );
+
+ // Fake out the getResources method of the migrator so that we return
+ // a single fake MigratorResource per availableResourceType.
+ sandbox
+ .stub(InternalTestingProfileMigrator.prototype, "getResources")
+ .callsFake(aProfile => {
+ Assert.deepEqual(
+ aProfile,
+ expectedProfile,
+ "Should have gotten the expected profile."
+ );
+ return Promise.resolve(
+ availableResourceTypes.map(resourceType => {
+ return {
+ type: resourceType,
+ migrate: () => {},
+ };
+ })
+ );
+ });
+
+ sandbox.stub(MigrationUtils, "_importQuantities").value({
+ bookmarks: EXPECTED_QUANTITY,
+ history: EXPECTED_QUANTITY,
+ logins: EXPECTED_QUANTITY,
+ cards: EXPECTED_QUANTITY,
+ });
+
+ sandbox
+ .stub(MigrationUtils, "getSourceIdForTelemetry")
+ .withArgs(InternalTestingProfileMigrator.key)
+ .returns(InternalTestingProfileMigrator.sourceID);
+
+ // Fake out the migrate method of the migrator and assert that the
+ // next time it's called, its arguments match our expectations.
+ return new Promise(resolve => {
+ sandbox
+ .stub(InternalTestingProfileMigrator.prototype, "migrate")
+ .callsFake((aResourceTypes, aStartup, aProfile, aProgressCallback) => {
+ Assert.ok(
+ !aStartup,
+ "Migrator should not have been called as a startup migration."
+ );
+
+ let bitMask = 0;
+ for (let resourceType of expectedResourceTypes) {
+ bitMask |= resourceType;
+ }
+
+ Assert.deepEqual(
+ aResourceTypes,
+ bitMask,
+ "Got the expected resource types"
+ );
+ Assert.deepEqual(
+ aProfile,
+ expectedProfile,
+ "Got the expected profile object"
+ );
+
+ for (let resourceType of expectedResourceTypes) {
+ let shouldError = errorResourceTypes.includes(resourceType);
+ if (
+ resourceType == MigrationUtils.resourceTypes.EXTENSIONS &&
+ !shouldError
+ ) {
+ let progressValue;
+ if (totalExtensions == matchedExtensions) {
+ progressValue = MigrationWizardConstants.PROGRESS_VALUE.SUCCESS;
+ } else if (
+ totalExtensions > matchedExtensions &&
+ matchedExtensions
+ ) {
+ progressValue = MigrationWizardConstants.PROGRESS_VALUE.INFO;
+ } else {
+ Assert.ok(
+ false,
+ "Total and matched extensions should be greater than 0 on success." +
+ `Total: ${totalExtensions}, Matched: ${matchedExtensions}`
+ );
+ }
+ aProgressCallback(resourceType, !shouldError, {
+ totalExtensions: Array(totalExtensions),
+ importedExtensions: Array(matchedExtensions),
+ progressValue,
+ });
+ } else {
+ aProgressCallback(resourceType, !shouldError);
+ }
+ }
+
+ let usageHistogramSnapshot =
+ usageHistogram.snapshot()[InternalTestingProfileMigrator.key];
+
+ let errorHistogramSnapshot =
+ errorHistogram.snapshot()[InternalTestingProfileMigrator.key];
+
+ for (let resourceTypeName in MigrationUtils.resourceTypes) {
+ let resourceType = MigrationUtils.resourceTypes[resourceTypeName];
+ if (resourceType == MigrationUtils.resourceTypes.ALL) {
+ continue;
+ }
+
+ if (expectedResourceTypes.includes(resourceType)) {
+ Assert.equal(
+ usageHistogramSnapshot.values[Math.log2(resourceType)],
+ 1,
+ `Should have set resource type ${resourceTypeName} on the FX_MIGRATION_USAGE keyed histogram.`
+ );
+
+ if (errorResourceTypes.includes(resourceType)) {
+ Assert.equal(
+ errorHistogramSnapshot.values[Math.log2(resourceType)],
+ 1,
+ `Should have set resource type ${resourceTypeName} on the FX_MIGRATION_ERRORS keyed histogram.`
+ );
+ }
+ } else {
+ let value = usageHistogramSnapshot.values[Math.log2(resourceType)];
+ Assert.ok(
+ value === 0 || value === undefined,
+ `Should not have set resource type ${resourceTypeName} on the FX_MIGRATION_USAGE keyed histogram.`
+ );
+ }
+ }
+
+ Services.obs.notifyObservers(null, "Migration:Ended");
+
+ TelemetryTestUtils.assertHistogram(
+ sourceHistogram,
+ InternalTestingProfileMigrator.sourceID,
+ 1
+ );
+
+ resolve();
+ });
+ }).finally(async () => {
+ sandbox.restore();
+
+ // MigratorBase caches resources fetched by the getResources method
+ // as a performance optimization. In order to allow different tests
+ // to have different available resources, we call into a special
+ // method of InternalTestingProfileMigrator that clears that
+ // cache.
+ let migrator = await MigrationUtils.getMigrator(
+ InternalTestingProfileMigrator.key
+ );
+ migrator.flushResourceCache();
+ });
+}
+
+/**
+ * Takes a MigrationWizard element and chooses the
+ * InternalTestingProfileMigrator as the browser to migrate from. Then, it
+ * checks the checkboxes associated with the selectedResourceTypes and
+ * unchecks the rest before clicking the "Import" button.
+ *
+ * @param {Element} wizard
+ * The MigrationWizard element.
+ * @param {string[]} selectedResourceTypes
+ * An array of resource type strings from
+ * MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.
+ * @param {string} [migratorKey=InternalTestingProfileMigrator.key]
+ * The key for the migrator to use. Defaults to the
+ * InternalTestingProfileMigrator.
+ */
+async function selectResourceTypesAndStartMigration(
+ wizard,
+ selectedResourceTypes,
+ migratorKey = InternalTestingProfileMigrator.key
+) {
+ let shadow = wizard.openOrClosedShadowRoot;
+
+ // First, select the InternalTestingProfileMigrator browser.
+ let selector = shadow.querySelector("#browser-profile-selector");
+ selector.click();
+
+ await new Promise(resolve => {
+ shadow
+ .querySelector("panel-list")
+ .addEventListener("shown", resolve, { once: true });
+ });
+
+ let panelItem = shadow.querySelector(`panel-item[key="${migratorKey}"]`);
+ panelItem.click();
+
+ // And then check the right checkboxes for the resource types.
+ let resourceTypeList = shadow.querySelector("#resource-type-list");
+ for (let resourceType of getChoosableResourceTypes()) {
+ let node = resourceTypeList.querySelector(
+ `label[data-resource-type="${resourceType}"]`
+ );
+ node.control.checked = selectedResourceTypes.includes(resourceType);
+ }
+
+ let importButton = shadow.querySelector("#import");
+ importButton.click();
+}
+
+/**
+ * Assert that the resource types passed in expectedResourceTypes are
+ * showing a success state after a migration, and if they are part of
+ * the RESOURCE_TYPES_WITH_QUANTITIES group, that they're showing the
+ * EXPECTED_QUANTITY magic number in their success message. Otherwise,
+ * we (currently) check that they show the empty string.
+ *
+ * @param {Element} wizard
+ * The MigrationWizard element.
+ * @param {string[]} expectedResourceTypes
+ * An array of resource type strings from
+ * MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.
+ * @param {string[]} [warningResourceTypes=[]]
+ * An array of resource type strings from
+ * MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES. These
+ * are the resources that should be showing a warning message.
+ */
+function assertQuantitiesShown(
+ wizard,
+ expectedResourceTypes,
+ warningResourceTypes = []
+) {
+ let shadow = wizard.openOrClosedShadowRoot;
+
+ // Make sure that we're showing the progress page first.
+ let deck = shadow.querySelector("#wizard-deck");
+ Assert.equal(
+ deck.selectedViewName,
+ `page-${MigrationWizardConstants.PAGES.PROGRESS}`
+ );
+
+ let headerL10nID = shadow.querySelector("#progress-header").dataset.l10nId;
+ if (warningResourceTypes.length) {
+ Assert.equal(
+ headerL10nID,
+ "migration-wizard-progress-done-with-warnings-header"
+ );
+ } else {
+ Assert.equal(headerL10nID, "migration-wizard-progress-done-header");
+ }
+
+ // Go through each displayed resource and make sure that only the
+ // ones that are expected are shown, and are showing the right
+ // success message.
+
+ let progressGroups = shadow.querySelectorAll(".resource-progress-group");
+ for (let progressGroup of progressGroups) {
+ if (expectedResourceTypes.includes(progressGroup.dataset.resourceType)) {
+ let progressIcon = progressGroup.querySelector(".progress-icon");
+ let messageText =
+ progressGroup.querySelector(".message-text").textContent;
+
+ if (warningResourceTypes.includes(progressGroup.dataset.resourceType)) {
+ Assert.equal(
+ progressIcon.getAttribute("state"),
+ "warning",
+ "Should be showing the warning icon state."
+ );
+ } else {
+ Assert.equal(
+ progressIcon.getAttribute("state"),
+ "success",
+ "Should be showing the success icon state."
+ );
+ }
+
+ if (
+ RESOURCE_TYPES_WITH_QUANTITIES.includes(
+ progressGroup.dataset.resourceType
+ )
+ ) {
+ if (
+ progressGroup.dataset.resourceType ==
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY
+ ) {
+ // HISTORY is a special case that doesn't show the number of imported
+ // history entries, but instead shows the maximum number of days of history
+ // that might have been imported.
+ Assert.notEqual(
+ messageText.indexOf(MigrationUtils.HISTORY_MAX_AGE_IN_DAYS),
+ -1,
+ `Found expected maximum number of days of history: ${messageText}`
+ );
+ } else if (
+ progressGroup.dataset.resourceType ==
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA
+ ) {
+ // FORMDATA is another special case, because we simply show "Form history" as
+ // the message string, rather than a particular quantity.
+ Assert.equal(
+ messageText,
+ "Form history",
+ `Found expected form data string: ${messageText}`
+ );
+ } else if (
+ progressGroup.dataset.resourceType ==
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS
+ ) {
+ // waitForTestMigration by default sets up a "successful" migration of 1
+ // extension.
+ Assert.stringMatches(messageText, "1 extension");
+ } else {
+ Assert.notEqual(
+ messageText.indexOf(EXPECTED_QUANTITY),
+ -1,
+ `Found expected quantity in message string: ${messageText}`
+ );
+ }
+ } else {
+ // If you've found yourself here, and this is failing, it's probably because you've
+ // updated MigrationWizardParent.#getStringForImportQuantity to return a string for
+ // a resource type that's not in RESOURCE_TYPES_WITH_QUANTITIES, and you'll need
+ // to modify this function to check for that string.
+ Assert.equal(
+ messageText,
+ "",
+ "Expected the empty string if the resource type " +
+ "isn't in RESOURCE_TYPES_WITH_QUANTITIES"
+ );
+ }
+ } else {
+ Assert.ok(
+ BrowserTestUtils.isHidden(progressGroup),
+ `Resource progress group for ${progressGroup.dataset.resourceType}` +
+ ` should be hidden.`
+ );
+ }
+ }
+}
+
+/**
+ * Translates an entrypoint string into the proper numeric value for the
+ * FX_MIGRATION_ENTRY_POINT_CATEGORICAL histogram.
+ *
+ * @param {string} entrypoint
+ * The entrypoint to translate from MIGRATION_ENTRYPOINTS.
+ * @returns {number}
+ * The numeric index value for the FX_MIGRATION_ENTRY_POINT_CATEGORICAL
+ * histogram.
+ */
+function getEntrypointHistogramIndex(entrypoint) {
+ switch (entrypoint) {
+ case MigrationUtils.MIGRATION_ENTRYPOINTS.FIRSTRUN: {
+ return 1;
+ }
+ case MigrationUtils.MIGRATION_ENTRYPOINTS.FXREFRESH: {
+ return 2;
+ }
+ case MigrationUtils.MIGRATION_ENTRYPOINTS.PLACES: {
+ return 3;
+ }
+ case MigrationUtils.MIGRATION_ENTRYPOINTS.PASSWORDS: {
+ return 4;
+ }
+ case MigrationUtils.MIGRATION_ENTRYPOINTS.NEWTAB: {
+ return 5;
+ }
+ case MigrationUtils.MIGRATION_ENTRYPOINTS.FILE_MENU: {
+ return 6;
+ }
+ case MigrationUtils.MIGRATION_ENTRYPOINTS.HELP_MENU: {
+ return 7;
+ }
+ case MigrationUtils.MIGRATION_ENTRYPOINTS.BOOKMARKS_TOOLBAR: {
+ return 8;
+ }
+ case MigrationUtils.MIGRATION_ENTRYPOINTS.PREFERENCES: {
+ return 9;
+ }
+ case MigrationUtils.MIGRATION_ENTRYPOINTS.UNKNOWN:
+ // Intentional fall-through
+ default: {
+ return 0; // Unknown
+ }
+ }
+}