From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../components/migration/tests/browser/browser.ini | 26 ++ .../tests/browser/browser_aboutwelcome_behavior.js | 100 +++++ .../tests/browser/browser_dialog_cancel_close.js | 55 +++ .../migration/tests/browser/browser_dialog_open.js | 55 +++ .../tests/browser/browser_dialog_resize.js | 29 ++ .../tests/browser/browser_disabled_migrator.js | 131 +++++++ .../tests/browser/browser_do_migration.js | 195 ++++++++++ .../tests/browser/browser_entrypoint_telemetry.js | 105 ++++++ .../tests/browser/browser_file_migration.js | 185 ++++++++++ .../browser_ie_edge_bookmarks_success_strings.js | 89 +++++ .../tests/browser/browser_no_browsers_state.js | 92 +++++ .../tests/browser/browser_only_file_migrators.js | 71 ++++ .../tests/browser/browser_safari_passwords.js | 401 +++++++++++++++++++++ .../tests/browser/browser_safari_permissions.js | 133 +++++++ .../migration/tests/browser/dummy_file.csv | 1 + browser/components/migration/tests/browser/head.js | 350 ++++++++++++++++++ 16 files changed, 2018 insertions(+) create mode 100644 browser/components/migration/tests/browser/browser.ini create mode 100644 browser/components/migration/tests/browser/browser_aboutwelcome_behavior.js create mode 100644 browser/components/migration/tests/browser/browser_dialog_cancel_close.js create mode 100644 browser/components/migration/tests/browser/browser_dialog_open.js create mode 100644 browser/components/migration/tests/browser/browser_dialog_resize.js create mode 100644 browser/components/migration/tests/browser/browser_disabled_migrator.js create mode 100644 browser/components/migration/tests/browser/browser_do_migration.js create mode 100644 browser/components/migration/tests/browser/browser_entrypoint_telemetry.js create mode 100644 browser/components/migration/tests/browser/browser_file_migration.js create mode 100644 browser/components/migration/tests/browser/browser_ie_edge_bookmarks_success_strings.js create mode 100644 browser/components/migration/tests/browser/browser_no_browsers_state.js create mode 100644 browser/components/migration/tests/browser/browser_only_file_migrators.js create mode 100644 browser/components/migration/tests/browser/browser_safari_passwords.js create mode 100644 browser/components/migration/tests/browser/browser_safari_permissions.js create mode 100644 browser/components/migration/tests/browser/dummy_file.csv create mode 100644 browser/components/migration/tests/browser/head.js (limited to 'browser/components/migration/tests/browser') diff --git a/browser/components/migration/tests/browser/browser.ini b/browser/components/migration/tests/browser/browser.ini new file mode 100644 index 0000000000..e8ac0d5995 --- /dev/null +++ b/browser/components/migration/tests/browser/browser.ini @@ -0,0 +1,26 @@ +[DEFAULT] +head = head.js +prefs = + browser.migrate.content-modal.enabled=true + browser.migrate.internal-testing.enabled=true + +[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_file_migration.js] +skip-if = os == "win" && debug # Bug 1827995 +support-files = + dummy_file.csv +[browser_ie_edge_bookmarks_success_strings.js] +[browser_no_browsers_state.js] +[browser_only_file_migrators.js] +[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..87f27cdb8d --- /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", + { onResize: () => {} } + ); + 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..e332e1ed4d --- /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.loadURIString(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..a1c8540c35 --- /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 => { + wizard + .querySelector("panel-list") + .addEventListener("shown", resolve, { once: true }); + }); + + let panelItem = wizard.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 => { + wizard + .querySelector("panel-list") + .addEventListener("shown", resolve, { once: true }); + }); + + let panelItem = wizard.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..b1a5e9ad60 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_do_migration.js @@ -0,0 +1,195 @@ +/* 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, + ]); + }); + + // 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 + ); + 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..bdeca0fdb5 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_entrypoint_telemetry.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", +}); +const CONTENT_MODAL_ENABLED_PREF = "browser.migrate.content-modal.enabled"; +const HISTOGRAM_ID = "FX_MIGRATION_ENTRY_POINT_CATEGORICAL"; +const LEGACY_HISTOGRAM_ID = "FX_MIGRATION_ENTRY_POINT"; + +async function showThenCloseMigrationWizardViaEntrypoint(entrypoint) { + let openedPromise = BrowserTestUtils.waitForMigrationWizard(window); + + // On some platforms, this call blocks, so in order to let the test proceed, we + // run it on the next tick of the event loop. + executeSoon(() => { + MigrationUtils.showMigrationWizard(window, { + entrypoint, + }); + }); + + let wizard = await openedPromise; + Assert.ok(wizard, "Migration wizard opened."); + await BrowserTestUtils.closeMigrationWizard(wizard); +} + +add_setup(async () => { + // Load the initial tab at example.com. This makes it so that if + // we're using the new migration wizard, we'll load the about:preferences + // page in a new tab rather than overtaking the initial one. This + // makes it easier to be consistent with closing and opening + // behaviours between the two kinds of migration wizards. + let browser = gBrowser.selectedBrowser; + BrowserTestUtils.loadURIString(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, as well + * as the legacy FX_MIGRATION_ENTRY_POINT histogram (but only if using the old + * wizard window). + */ +add_task(async function test_legacy_wizard() { + for (let contentModalEnabled of [true, false]) { + info("Testing with content modal enabled: " + contentModalEnabled); + await SpecialPowers.pushPrefEnv({ + set: [[CONTENT_MODAL_ENABLED_PREF, contentModalEnabled]], + }); + + let histogram = TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_ID); + let legacyHistogram = + TelemetryTestUtils.getAndClearHistogram(LEGACY_HISTOGRAM_ID); + + // Let's arbitrarily pick the "Bookmarks" entrypoint, and make sure this + // is recorded. + await showThenCloseMigrationWizardViaEntrypoint( + MigrationUtils.MIGRATION_ENTRYPOINTS.BOOKMARKS + ); + let entrypointId = MigrationUtils.getLegacyMigrationEntrypoint( + MigrationUtils.MIGRATION_ENTRYPOINTS.BOOKMARKS + ); + + TelemetryTestUtils.assertHistogram(histogram, entrypointId, 1); + + if (!contentModalEnabled) { + TelemetryTestUtils.assertHistogram(legacyHistogram, entrypointId, 1); + } + + histogram = TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_ID); + legacyHistogram = + TelemetryTestUtils.getAndClearHistogram(LEGACY_HISTOGRAM_ID); + + // Now let's pick the "Preferences" entrypoint, and make sure this + // is recorded. + await showThenCloseMigrationWizardViaEntrypoint( + MigrationUtils.MIGRATION_ENTRYPOINTS.PREFERENCES + ); + entrypointId = MigrationUtils.getLegacyMigrationEntrypoint( + MigrationUtils.MIGRATION_ENTRYPOINTS.PREFERENCES + ); + + TelemetryTestUtils.assertHistogram(histogram, entrypointId, 1); + if (!contentModalEnabled) { + TelemetryTestUtils.assertHistogram(legacyHistogram, entrypointId, 1); + } + + histogram = TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_ID); + legacyHistogram = + TelemetryTestUtils.getAndClearHistogram(LEGACY_HISTOGRAM_ID); + + // Finally, check the fallback by passing in something invalid as an entrypoint. + await showThenCloseMigrationWizardViaEntrypoint(undefined); + entrypointId = MigrationUtils.getLegacyMigrationEntrypoint( + MigrationUtils.MIGRATION_ENTRYPOINTS.UNKNOWN + ); + + TelemetryTestUtils.assertHistogram(histogram, entrypointId, 1); + if (!contentModalEnabled) { + TelemetryTestUtils.assertHistogram(legacyHistogram, entrypointId, 1); + } + } +}); 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..774ff25425 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_file_migration.js @@ -0,0 +1,185 @@ +/* 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]; + } +} + +/** + * 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; + }); + + // 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; + MockFilePicker.init(window); + registerCleanupFunction(() => { + MockFilePicker.cleanup(); + }); + + 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 => { + wizard + .querySelector("panel-list") + .addEventListener("shown", resolve, { once: true }); + }); + + info("Panel list shown. Clicking on panel-item"); + let panelItem = wizard.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-page-file-import-progress'] .resource-progress-group" + ); + for (let progressGroup of progressGroups) { + let expectedSuccessText = + SUCCESS_STATE[progressGroup.dataset.resourceType]; + if (expectedSuccessText) { + let successText = + progressGroup.querySelector(".success-text").textContent; + Assert.equal(successText, expectedSuccessText); + } else { + Assert.ok( + BrowserTestUtils.is_hidden(progressGroup), + `Resource progress group for ${progressGroup.dataset.resourceType}` + + ` should be hidden.` + ); + } + } + }); +}); 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..ab0b705678 --- /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 successTextElement = + bookmarksProgressGroup.querySelector(".success-text"); + + await BrowserTestUtils.waitForCondition(() => { + return successTextElement.textContent.trim(); + }); + + let successText = successTextElement.textContent.toLowerCase(); + + if (key == "internal-testing") { + Assert.ok( + successText.includes("bookmarks"), + `Success test should refer to bookmarks: ${successText}.` + ); + } else { + Assert.ok( + successText.includes("favorites"), + `Success test should refer to favorites: ${successText}` + ); + } + + 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_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..80c09e9a09 --- /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 = wizard.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 = wizard.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_safari_passwords.js b/browser/components/migration/tests/browser/browser_safari_passwords.js new file mode 100644 index 0000000000..299695f9e3 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_safari_passwords.js @@ -0,0 +1,401 @@ +/* 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 {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} + */ +async function testSafariPasswordHelper( + expectsFilePicker, + migrateBookmarks, + 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); + 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") + .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 = wizard.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, + 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, + 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 that the user can skip importing passwords from Safari. + */ +add_task(async function test_safari_password_skip() { + await testSafariPasswordHelper( + false, + true, + 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 = wizard.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..924d6cc4d4 --- /dev/null +++ b/browser/components/migration/tests/browser/browser_safari_permissions.js @@ -0,0 +1,133 @@ +/* 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 safariMigrator = new SafariProfileMigrator(); + sandbox.stub(MigrationUtils, "getMigrator").resolves(safariMigrator); + + sandbox + .stub(SafariProfileMigrator.prototype, "hasPermissions") + .onFirstCall() + .resolves(false) + .onSecondCall() + .resolves(true); + + sandbox + .stub(SafariProfileMigrator.prototype, "getPermissions") + .resolves(true); + + sandbox + .stub(SafariProfileMigrator.prototype, "getResources") + .callsFake(() => { + return Promise.resolve([ + { + type: MigrationUtils.resourceTypes.BOOKMARKS, + migrate: () => {}, + }, + ]); + }); + + let didMigration = new Promise(resolve => { + sandbox + .stub(SafariProfileMigrator.prototype, "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 = wizard.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..772dd8bd29 --- /dev/null +++ b/browser/components/migration/tests/browser/head.js @@ -0,0 +1,350 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); +const { MigrationWizardConstants } = ChromeUtils.importESModule( + "chrome://browser/content/migration/migration-wizard-constants.mjs" +); +const { InternalTestingProfileMigrator } = ChromeUtils.importESModule( + "resource:///modules/InternalTestingProfileMigrator.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, +]; + +/** + * 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} + */ + +/** + * 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} + */ +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.loadURIString(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} + * 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. + * @returns {Promise} + */ +async function waitForTestMigration( + availableResourceTypes, + expectedResourceTypes, + expectedProfile +) { + let sandbox = sinon.createSandbox(); + + // 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, + }); + + // 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) { + aProgressCallback(resourceType); + } + Services.obs.notifyObservers(null, "Migration:Ended"); + 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 => { + wizard + .querySelector("panel-list") + .addEventListener("shown", resolve, { once: true }); + }); + + let panelItem = wizard.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 in MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES) { + 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. + */ +function assertQuantitiesShown(wizard, expectedResourceTypes) { + 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}` + ); + + // 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 successText = + progressGroup.querySelector(".success-text").textContent; + + Assert.ok( + progressIcon.classList.contains("completed"), + "Should be showing completed 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( + successText.indexOf(MigrationUtils.HISTORY_MAX_AGE_IN_DAYS), + -1, + `Found expected maximum number of days of history: ${successText}` + ); + } else if ( + progressGroup.dataset.resourceType == + MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA + ) { + // FORMDATA is another special case, because we simply show "Form history" as + // the success string, rather than a particular quantity. + Assert.equal( + successText, + "Form history", + `Found expected form data string: ${successText}` + ); + } else { + Assert.notEqual( + successText.indexOf(EXPECTED_QUANTITY), + -1, + `Found expected quantity in success string: ${successText}` + ); + } + } 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( + successText, + "", + "Expected the empty string if the resource type " + + "isn't in RESOURCE_TYPES_WITH_QUANTITIES" + ); + } + } else { + Assert.ok( + BrowserTestUtils.is_hidden(progressGroup), + `Resource progress group for ${progressGroup.dataset.resourceType}` + + ` should be hidden.` + ); + } + } +} -- cgit v1.2.3