summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/tests
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/migration/tests')
-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
-rw-r--r--browser/components/migration/tests/chrome/chrome.toml5
-rw-r--r--browser/components/migration/tests/chrome/test_migration_wizard.html1533
-rw-r--r--browser/components/migration/tests/head-common.js24
-rw-r--r--browser/components/migration/tests/marionette/manifest.toml4
-rw-r--r--browser/components/migration/tests/marionette/test_refresh_firefox.py703
-rw-r--r--browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Faviconsbin0 -> 49152 bytes
-rw-r--r--browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Databin0 -> 24576 bytes
-rw-r--r--browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Web Databin0 -> 108544 bytes
-rw-r--r--browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Local State5
-rw-r--r--browser/components/migration/tests/unit/AppData/LocalWithNoData/Google/Chrome/User Data/Default/Login Databin0 -> 24576 bytes
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/0f3ab103a522f4463ecacc36d34eb996/360sefav.datbin0 -> 6144 bytes
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/360Bookmarks1
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2020_08_28.favdb3
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2021_12_02.favdb3
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2020_08_28.favdb1
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2021_12_02.favdb1
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat1
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/DailyBackup/360sefav_2020_08_28.favdb3
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/Bookmarks3
-rw-r--r--browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Local State12
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookiesbin0 -> 10240 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/_locales/en_US/messages.json9
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/manifest.json10
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/_locales/en_US/messages.json9
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/manifest.json5
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-2/1.0_0/manifest.json4
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryCorruptbin0 -> 91558 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMasterbin0 -> 118784 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Login Databin0 -> 24576 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State22
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Bookmarks.plistbin0 -> 2252 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.dbbin0 -> 40960 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-lock0
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-shmbin0 -> 32768 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-walbin0 -> 280192 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/04860FA3D07D8936B87D2B965317C6E9bin0 -> 24838 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/19A777E4F7BDA0C0E350D6C681B6E271bin0 -> 15086 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/2558A57A1AE576AA31F0DCD1364B3F42bin0 -> 5558 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/57D1907A1EBDA1889AA85B8AB7A90804bin0 -> 5558 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/6EEDD53B65A19CB364EB6FB07DEACF80bin0 -> 22382 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/7F65370AD319C7B294EDF2E2BEBA880Fbin0 -> 2734 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/999E2BD5CD612AA550F222A1088DB3D8bin0 -> 15406 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/9D8A6E2153D42043A7AE0430B41D374Abin0 -> 5558 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/A21F634481CF5188329FD2052F07ADBCbin0 -> 5558 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/BC2288B5BA9B7BE352BA586257442E08bin0 -> 22382 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/CFFC3831D8E7201BF8B77728FC79B52Bbin0 -> 15406 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/F3FA61DDA95B78A8B5F2C392C0382137bin0 -> 5430 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/HistoryStrangeEntries.dbbin0 -> 98304 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/HistoryTemplate.dbbin0 -> 98304 bytes
-rw-r--r--browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Default/Login Databin0 -> 24576 bytes
-rw-r--r--browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Local State22
-rw-r--r--browser/components/migration/tests/unit/bookmarks.exported.html32
-rw-r--r--browser/components/migration/tests/unit/bookmarks.exported.json194
-rw-r--r--browser/components/migration/tests/unit/bookmarks.invalid.html1
-rw-r--r--browser/components/migration/tests/unit/head_migration.js260
-rw-r--r--browser/components/migration/tests/unit/insertIEHistory/InsertIEHistory.cpp38
-rw-r--r--browser/components/migration/tests/unit/insertIEHistory/moz.build19
-rw-r--r--browser/components/migration/tests/unit/test_360seMigrationUtils.js164
-rw-r--r--browser/components/migration/tests/unit/test_360se_bookmarks.js62
-rw-r--r--browser/components/migration/tests/unit/test_BookmarksFileMigrator.js134
-rw-r--r--browser/components/migration/tests/unit/test_ChromeMigrationUtils.js87
-rw-r--r--browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js141
-rw-r--r--browser/components/migration/tests/unit/test_ChromeMigrationUtils_path_chromium_snap.js55
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_bookmarks.js205
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_corrupt_history.js83
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_credit_cards.js239
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_extensions.js165
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_formdata.js118
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_history.js206
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_passwords.js373
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_passwords_emptySource.js43
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_permissions.js205
-rw-r--r--browser/components/migration/tests/unit/test_Edge_db_migration.js849
-rw-r--r--browser/components/migration/tests/unit/test_Edge_registry_migration.js81
-rw-r--r--browser/components/migration/tests/unit/test_IE_bookmarks.js30
-rw-r--r--browser/components/migration/tests/unit/test_IE_history.js187
-rw-r--r--browser/components/migration/tests/unit/test_MigrationUtils_timedRetry.js29
-rw-r--r--browser/components/migration/tests/unit/test_PasswordFileMigrator.js116
-rw-r--r--browser/components/migration/tests/unit/test_Safari_bookmarks.js85
-rw-r--r--browser/components/migration/tests/unit/test_Safari_history.js101
-rw-r--r--browser/components/migration/tests/unit/test_Safari_history_strange_entries.js115
-rw-r--r--browser/components/migration/tests/unit/test_Safari_permissions.js145
-rw-r--r--browser/components/migration/tests/unit/test_fx_telemetry.js436
-rw-r--r--browser/components/migration/tests/unit/xpcshell.toml95
103 files changed, 10456 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
+ }
+ }
+}
diff --git a/browser/components/migration/tests/chrome/chrome.toml b/browser/components/migration/tests/chrome/chrome.toml
new file mode 100644
index 0000000000..8f1c943f31
--- /dev/null
+++ b/browser/components/migration/tests/chrome/chrome.toml
@@ -0,0 +1,5 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+support-files = ["../head-common.js"]
+
+["test_migration_wizard.html"]
diff --git a/browser/components/migration/tests/chrome/test_migration_wizard.html b/browser/components/migration/tests/chrome/test_migration_wizard.html
new file mode 100644
index 0000000000..d991cce114
--- /dev/null
+++ b/browser/components/migration/tests/chrome/test_migration_wizard.html
@@ -0,0 +1,1533 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Basic tests for the Migration Wizard component</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script src="head-common.js"></script>
+ <script
+ src="chrome://browser/content/migration/migration-wizard.mjs"
+ type="module"
+ ></script>
+ <link
+ rel="stylesheet"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ />
+ <script>
+ /* import-globals-from ../head-common.js */
+
+ "use strict";
+
+ const { BrowserTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/BrowserTestUtils.sys.mjs"
+ );
+
+ const MIGRATOR_PROFILE_INSTANCES = [
+ {
+ key: "some-browser-0",
+ type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER,
+ displayName: "Some Browser 0",
+ resourceTypes: ["HISTORY", "FORMDATA", "PASSWORDS", "BOOKMARKS", "EXTENSIONS"],
+ profile: { id: "person-2", name: "Person 2" },
+ hasPermissions: true,
+ },
+ {
+ key: "some-browser-1",
+ type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER,
+ displayName: "Some Browser 1",
+ resourceTypes: ["HISTORY", "BOOKMARKS"],
+ profile: null,
+ hasPermissions: true,
+ },
+ ];
+
+ let gWiz = null;
+ let gShadowRoot = null;
+ let gDeck = null;
+
+ /**
+ * Returns the .resource-progress-group div for a particular resource
+ * type.
+ *
+ * @param {string} displayedResourceType
+ * One of the constants belonging to
+ * MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.
+ * @returns {Element}
+ */
+ function getResourceGroup(displayedResourceType) {
+ return gShadowRoot.querySelector(
+ `.resource-progress-group[data-resource-type="${displayedResourceType}"]`
+ );
+ }
+
+ add_setup(async function() {
+ gWiz = document.getElementById("test-wizard");
+ gShadowRoot = gWiz.openOrClosedShadowRoot;
+ gDeck = gShadowRoot.querySelector("#wizard-deck");
+ });
+
+ /**
+ * Tests that the MigrationWizard:RequestState event is fired when the
+ * <migration-wizard> is added to the DOM if the auto-request-state attribute
+ * is set, and then ensures that the starting page is correct.
+ *
+ * This also tests that the MigrationWizard:RequestState is not fired automatically
+ * if the auto-request-state attribute is not set, but is then fired upon calling
+ * requestState().
+ *
+ * This uses a dynamically created <migration-wizard> instead of the one already
+ * in the content div to make sure that the init event is captured.
+ */
+ add_task(async function test_init_event() {
+ const REQUEST_STATE_EVENT = "MigrationWizard:RequestState";
+
+ let wiz = document.createElement("migration-wizard");
+ wiz.toggleAttribute("auto-request-state", true);
+ let content = document.getElementById("content");
+ let promise = new Promise(resolve => {
+ content.addEventListener(REQUEST_STATE_EVENT, resolve, {
+ once: true,
+ });
+ });
+ content.appendChild(wiz);
+ await promise;
+ ok(true, `Saw ${REQUEST_STATE_EVENT} event.`);
+ let shadowRoot = wiz.openOrClosedShadowRoot;
+ let deck = shadowRoot.querySelector("#wizard-deck");
+ is(
+ deck.selectedViewName,
+ "page-loading",
+ "Should have the loading page selected"
+ );
+ wiz.remove();
+
+ wiz.toggleAttribute("auto-request-state", false);
+ let sawEvent = false;
+ let handler = () => {
+ sawEvent = true;
+ };
+ content.addEventListener(REQUEST_STATE_EVENT, handler);
+ content.appendChild(wiz);
+ ok(!sawEvent, `Should not have seen ${REQUEST_STATE_EVENT} event.`);
+ content.removeEventListener(REQUEST_STATE_EVENT, handler);
+
+ promise = new Promise(resolve => {
+ content.addEventListener(REQUEST_STATE_EVENT, resolve, {
+ once: true,
+ });
+ });
+ wiz.requestState();
+ await promise;
+ ok(true, `Saw ${REQUEST_STATE_EVENT} event.`);
+ wiz.remove();
+ });
+
+ /**
+ * Tests that the wizard can show a list of browser and profiles.
+ */
+ add_task(async function test_selection() {
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: MIGRATOR_PROFILE_INSTANCES,
+ showImportAll: false,
+ });
+
+ let selector = gShadowRoot.querySelector("#browser-profile-selector");
+ let preamble = gShadowRoot.querySelector(".resource-selection-preamble");
+ ok(!isHidden(preamble), "preamble should shown.");
+
+ let panelList = gShadowRoot.querySelector("panel-list");
+ is(panelList.childElementCount, 2, "Should have two child elements");
+
+ let resourceTypeList = gShadowRoot.querySelector("#resource-type-list");
+ let details = gShadowRoot.querySelector("details");
+ ok(details.open, "Details should be open");
+
+ // Test that the resource type checkboxes are shown or hidden depending on
+ // which resourceTypes are included with the MigratorProfileInstance.
+ for (let migratorInstance of MIGRATOR_PROFILE_INSTANCES) {
+ selector.click();
+ await new Promise(resolve => {
+ gShadowRoot
+ .querySelector("panel-list")
+ .addEventListener("shown", resolve, { once: true });
+ });
+ let panelItem = gShadowRoot.querySelector(
+ `panel-item[key="${migratorInstance.key}"]`
+ );
+ ok(panelItem, "Should find panel-item.");
+ panelItem.click();
+
+ is(
+ selector.querySelector("#migrator-name").textContent,
+ migratorInstance.displayName,
+ "Selector should show display name"
+ );
+ let profileName = selector.querySelector("#profile-name");
+
+ if (migratorInstance.profile) {
+ ok(!isHidden(profileName), "Profile name element should be displayed.");
+ is(
+ profileName.textContent,
+ migratorInstance.profile.name,
+ "Selector should show profile name"
+ );
+ } else {
+ ok(isHidden(profileName), "Profile name element should be hidden.");
+ is(profileName.textContent, "");
+ }
+
+ for (let resourceType of getChoosableResourceTypes()) {
+ let node = resourceTypeList.querySelector(
+ `label[data-resource-type="${resourceType}"]`
+ );
+
+ if (migratorInstance.resourceTypes.includes(resourceType)) {
+ ok(!isHidden(node), `Selection for ${resourceType} should be shown.`);
+ ok(
+ node.control.checked,
+ `Checkbox for ${resourceType} should be checked.`
+ );
+ } else {
+ ok(isHidden(node), `Selection for ${resourceType} should be hidden.`);
+ ok(
+ !node.control.checked,
+ `Checkbox for ${resourceType} should be unchecked.`
+ );
+ }
+ }
+ }
+
+ let selectAll = gShadowRoot.querySelector("#select-all");
+ let summary = gShadowRoot.querySelector("summary");
+ ok(isHidden(selectAll), "Selection for select-all should be hidden.");
+ ok(isHidden(summary), "Summary should be hidden.");
+ ok(!isHidden(details), "Details should be shown.");
+ });
+
+ /**
+ * Tests the migration wizard with no resources
+ */
+ add_task(async function test_no_resources() {
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: [{
+ key: "some-browser-0",
+ type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER,
+ displayName: "Some Browser 0 with no resources",
+ resourceTypes: [],
+ profile: { id: "person-1", name: "Person 1" },
+ hasPermissions: true,
+ }],
+ showImportAll: false,
+ });
+
+ let noResourcesFound = gShadowRoot.querySelector(".no-resources-found");
+ let hideOnErrorEls = gShadowRoot.querySelectorAll(".hide-on-error");
+ ok(
+ !isHidden(noResourcesFound),
+ "Error message of no reasources should be shown."
+ );
+ for (let hideOnErrorEl of hideOnErrorEls) {
+ ok(isHidden(hideOnErrorEl), "Item should be hidden.");
+ }
+ });
+
+ /**
+ * Tests variant 2 of the migration wizard
+ */
+ add_task(async function test_selection_variant_2() {
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: MIGRATOR_PROFILE_INSTANCES,
+ showImportAll: true,
+ });
+
+ let preamble = gShadowRoot.querySelector(".resource-selection-preamble");
+ ok(isHidden(preamble), "preamble should be hidden.");
+
+ let selector = gShadowRoot.querySelector("#browser-profile-selector");
+ selector.click();
+ await new Promise(resolve => {
+ let panelList = gShadowRoot.querySelector("panel-list");
+ if (panelList) {
+ panelList.addEventListener("shown", resolve, { once: true });
+ }
+ });
+
+ let panelItems = gShadowRoot.querySelectorAll("panel-list > panel-item");
+ is(panelItems.length, 2, "Should have two panel items");
+
+ let details = gShadowRoot.querySelector("details");
+ ok(!details.open, "Details should be closed");
+ details.open = true;
+
+ for (let i = 0; i < panelItems.length; i++) {
+ let migratorInstance = MIGRATOR_PROFILE_INSTANCES[i];
+ let panelItem = panelItems[i];
+ panelItem.click();
+ for (let resourceType of getChoosableResourceTypes()) {
+ let node = gShadowRoot.querySelector(
+ `#resource-type-list label[data-resource-type="${resourceType}"]`
+ );
+ if (migratorInstance.resourceTypes.includes(resourceType)) {
+ ok(!isHidden(node), `Selection for ${resourceType} should be shown.`);
+ ok(
+ node.control.checked,
+ `Checkbox for ${resourceType} should be checked.`
+ );
+ } else {
+ ok(isHidden(node), `Selection for ${resourceType} should be hidden.`);
+ ok(
+ !node.control.checked,
+ `Checkbox for ${resourceType} should be unchecked.`
+ );
+ }
+ }
+ }
+
+ let selectAll = gShadowRoot.querySelector("#select-all");
+ let summary = gShadowRoot.querySelector("summary");
+ ok(!isHidden(selectAll), "Selection for select-all should be shown.");
+ ok(selectAll.control.checked, "Checkbox for select-all should be checked.");
+ ok(!isHidden(summary), "Summary should be shown.");
+ ok(!isHidden(details), "Details should be shown.");
+
+ let selectAllCheckbox = gShadowRoot.querySelector(".select-all-checkbox");
+ selectAllCheckbox.checked = true;
+ selectAllCheckbox.dispatchEvent(new CustomEvent("change"));
+ let resourceLabels = gShadowRoot.querySelectorAll("label[data-resource-type]");
+ for (let resourceLabel of resourceLabels) {
+ if (resourceLabel.hidden) {
+ ok(
+ !resourceLabel.control.checked,
+ `Hidden checkbox for ${resourceLabel.dataset.resourceType} should be unchecked.`
+
+ );
+ } else {
+ ok(
+ resourceLabel.control.checked,
+ `Visible checkbox for ${resourceLabel.dataset.resourceType} should be checked.`
+ );
+ }
+ }
+
+ let selectedDataHeader = gShadowRoot.querySelector(".selected-data-header");
+ let selectedData = gShadowRoot.querySelector(".selected-data");
+
+ let bookmarks = gShadowRoot.querySelector("#bookmarks");
+ let history = gShadowRoot.querySelector("#history");
+
+ let selectedDataUpdated = BrowserTestUtils.waitForEvent(
+ gWiz,
+ "MigrationWizard:ResourcesUpdated"
+ );
+ bookmarks.control.checked = true;
+ history.control.checked = true;
+ bookmarks.dispatchEvent(new CustomEvent("change"));
+
+ ok(bookmarks.control.checked, "Bookmarks should be checked");
+ ok(history.control.checked, "History should be checked");
+
+ await selectedDataUpdated;
+
+ is(
+ selectedData.textContent,
+ "Bookmarks and history",
+ "Testing if selected-data reflects the selected resources."
+ );
+
+ is(
+ selectedDataHeader.dataset.l10nId,
+ "migration-all-available-data-label",
+ "Testing if selected-data-header reflects the selected resources"
+ );
+
+ let importButton = gShadowRoot.querySelector("#import");
+
+ ok(
+ !importButton.disabled,
+ "Testing if import button is enabled when at least one resource is selected."
+ );
+
+ let importButtonUpdated = BrowserTestUtils.waitForEvent(
+ gWiz,
+ "MigrationWizard:ResourcesUpdated"
+ );
+
+ selectAllCheckbox.checked = false;
+ selectAllCheckbox.dispatchEvent(new CustomEvent("change", { bubbles: true }));
+ await importButtonUpdated;
+
+ ok(
+ importButton.disabled,
+ "Testing if import button is disabled when no resources are selected."
+ );
+ });
+
+ /**
+ * Tests variant 2 of the migration wizard when there's a single resource
+ * item.
+ */
+ add_task(async function test_selection_variant_2_single_item() {
+ let resourcesUpdated = BrowserTestUtils.waitForEvent(
+ gWiz,
+ "MigrationWizard:ResourcesUpdated"
+ );
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: [{
+ key: "some-browser-0",
+ type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER,
+ displayName: "Some Browser 0 with a single resource",
+ resourceTypes: ["HISTORY"],
+ profile: { id: "person-1", name: "Person 1" },
+ hasPermissions: true,
+ }, {
+ key: "some-browser-1",
+ type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER,
+ displayName: "Some Browser 1 with a two resources",
+ resourceTypes: ["HISTORY", "BOOKMARKS"],
+ profile: { id: "person-2", name: "Person 2" },
+ hasPermissions: true,
+ }],
+ showImportAll: true,
+ });
+ await resourcesUpdated;
+
+ let selectAll = gShadowRoot.querySelector("#select-all");
+ let summary = gShadowRoot.querySelector("summary");
+ let details = gShadowRoot.querySelector("details");
+ ok(!details.open, "Details should be closed");
+ details.open = true;
+
+ ok(isHidden(selectAll), "Selection for select-all should be hidden.");
+ ok(!isHidden(summary), "Summary should be shown.");
+ ok(!isHidden(details), "Details should be shown.");
+
+ resourcesUpdated = BrowserTestUtils.waitForEvent(
+ gWiz,
+ "MigrationWizard:ResourcesUpdated"
+ );
+ let browser1Item = gShadowRoot.querySelector("panel-item[key='some-browser-1']");
+ browser1Item.click();
+ await resourcesUpdated;
+
+ ok(!isHidden(selectAll), "Selection for select-all should be shown.");
+ ok(!isHidden(summary), "Summary should be shown.");
+ ok(!isHidden(details), "Details should be shown.");
+ });
+
+ /**
+ * Tests that the Select All checkbox is checked if all non-hidden resource
+ * types are checked, and unchecked otherwise.
+ */
+ add_task(async function test_selection_variant_2_select_all() {
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: MIGRATOR_PROFILE_INSTANCES,
+ showImportAll: true,
+ });
+
+ let details = gShadowRoot.querySelector("details");
+ ok(!details.open, "Details should be closed");
+ details.open = true;
+
+ let selectAll = gShadowRoot.querySelector("#select-all");
+ ok(selectAll.control.checked, "Select all should be checked by default");
+
+ let bookmarksResourceLabel = gShadowRoot.querySelector(
+ "label[data-resource-type='BOOKMARKS']"
+ );
+ ok(bookmarksResourceLabel.control.checked, "Bookmarks should be checked");
+
+ bookmarksResourceLabel.control.click();
+ ok(!bookmarksResourceLabel.control.checked, "Bookmarks should no longer be checked");
+ ok(!selectAll.control.checked, "Select all should not longer be checked");
+
+ bookmarksResourceLabel.control.click();
+ ok(bookmarksResourceLabel.control.checked, "Bookmarks should be checked again");
+ ok(selectAll.control.checked, "Select all should be checked");
+ });
+
+ /**
+ * Tests that the wizard can show partial progress during migration.
+ */
+ add_task(async function test_partial_progress() {
+ const BOOKMARKS_SUCCESS_STRING = "Some bookmarks success string";
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.PROGRESS,
+ key: "chrome",
+ progress: {
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
+ message: BOOKMARKS_SUCCESS_STRING,
+ },
+ // Don't include PASSWORDS to check that it's hidden.
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.LOADING,
+ },
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.LOADING,
+ },
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.LOADING,
+ },
+ },
+ });
+ is(
+ gDeck.selectedViewName,
+ "page-progress",
+ "Should have the progress page selected"
+ );
+
+ // Bookmarks
+ let bookmarksGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS
+ );
+ ok(!isHidden(bookmarksGroup), "Bookmarks group should be visible");
+ let progressIcon = bookmarksGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "success",
+ "Progress should be completed"
+ );
+ is(
+ bookmarksGroup.querySelector(".message-text").textContent,
+ BOOKMARKS_SUCCESS_STRING
+ );
+
+ // Passwords
+ let passwordsGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS
+ );
+ ok(isHidden(passwordsGroup), "Passwords group should be hidden");
+
+ // History
+ let historyGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY
+ );
+ ok(!isHidden(historyGroup), "History group should be visible");
+ progressIcon = historyGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "loading",
+ "Progress should be still be underway"
+ );
+ is(historyGroup.querySelector(".message-text").textContent.trim(), "");
+
+ // Extensions
+ let extensionsGroup = getResourceGroup(MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS);
+ ok(!isHidden(extensionsGroup), "Extensions group should be visible");
+ progressIcon = extensionsGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "loading",
+ "Progress should be still be underway"
+ );
+ is(extensionsGroup.querySelector("a.message-text").textContent.trim(), "");
+ is(extensionsGroup.querySelector("span.message-text").textContent.trim(), "");
+
+ // Form Data
+ let formDataGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA
+ );
+ ok(!isHidden(formDataGroup), "Form data group should be visible");
+ progressIcon = formDataGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "loading",
+ "Progress should be still be underway"
+ );
+ is(formDataGroup.querySelector(".message-text").textContent.trim(), "");
+
+ // With progress still being underway, the header should be using the
+ // in progress string.
+ let header = gShadowRoot.querySelector("#progress-header");
+ is(
+ header.getAttribute("data-l10n-id"),
+ "migration-wizard-progress-header",
+ "Should be showing in-progress header string"
+ );
+
+ let progressPage = gShadowRoot.querySelector("div[name='page-progress']");
+ let doneButton = progressPage.querySelector(".done-button");
+ ok(isHidden(doneButton), "Done button should be hidden");
+ let cancelButton = progressPage.querySelector(".cancel-close");
+ ok(!isHidden(cancelButton), "Cancel button should be visible");
+ ok(cancelButton.disabled, "Cancel button should be disabled");
+ });
+
+ /**
+ * Tests that the wizard can show completed migration progress.
+ */
+ add_task(async function test_completed_progress() {
+ const BOOKMARKS_SUCCESS_STRING = "Some bookmarks success string";
+ const PASSWORDS_SUCCESS_STRING = "Some passwords success string";
+ const FORMDATA_SUCCESS_STRING = "Some formdata string";
+ const EXTENSIONS_SUCCESS_STRING = "Some extensions string";
+ const EXTENSIONS_SUCCESS_HREF = "about:addons";
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.PROGRESS,
+ key: "chrome",
+ progress: {
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
+ message: BOOKMARKS_SUCCESS_STRING,
+ },
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
+ message: PASSWORDS_SUCCESS_STRING,
+ },
+ // Don't include HISTORY to check that it's hidden.
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
+ message: EXTENSIONS_SUCCESS_STRING,
+ },
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
+ message: FORMDATA_SUCCESS_STRING,
+ },
+ },
+ });
+ is(
+ gDeck.selectedViewName,
+ "page-progress",
+ "Should have the progress page selected"
+ );
+
+ // Bookmarks
+ let bookmarksGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS
+ );
+ ok(!isHidden(bookmarksGroup), "Bookmarks group should be visible");
+ let progressIcon = bookmarksGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "success",
+ "Progress should be completed"
+ );
+ is(
+ bookmarksGroup.querySelector(".message-text").textContent,
+ BOOKMARKS_SUCCESS_STRING
+ );
+
+ // Passwords
+ let passwordsGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS
+ );
+ ok(!isHidden(passwordsGroup), "Passwords group should be visible");
+ progressIcon = passwordsGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "success",
+ "Progress should be completed"
+ );
+ is(
+ passwordsGroup.querySelector(".message-text").textContent,
+ PASSWORDS_SUCCESS_STRING
+ );
+
+ // History
+ let historyGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY
+ );
+ ok(isHidden(historyGroup), "History group should be hidden");
+
+ // Extensions
+ let extensionsDataGroup = getResourceGroup(MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS);
+ ok(!isHidden(extensionsDataGroup), "Extensions data group should be visible");
+ progressIcon = extensionsDataGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "success",
+ "Progress should be completed"
+ );
+ is(
+ extensionsDataGroup.querySelector("a.message-text").textContent,
+ EXTENSIONS_SUCCESS_STRING
+ );
+ is(
+ extensionsDataGroup.querySelector("a.message-text").href,
+ EXTENSIONS_SUCCESS_HREF
+ )
+ // Form Data
+ let formDataGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA
+ );
+ ok(!isHidden(formDataGroup), "Form data group should be visible");
+ progressIcon = formDataGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "success",
+ "Progress should be completed"
+ );
+ is(
+ formDataGroup.querySelector(".message-text").textContent,
+ FORMDATA_SUCCESS_STRING
+ );
+
+ // With progress being complete, the header should be using the completed
+ // migration string.
+ let header = gShadowRoot.querySelector("#progress-header");
+ is(
+ header.getAttribute("data-l10n-id"),
+ "migration-wizard-progress-done-header",
+ "Should be showing completed migration header string"
+ );
+
+ let progressPage = gShadowRoot.querySelector("div[name='page-progress']");
+ let doneButton = progressPage.querySelector(".done-button");
+ ok(!isHidden(doneButton), "Done button should be visible and enabled");
+ let cancelButton = progressPage.querySelector(".cancel-close");
+ ok(isHidden(cancelButton), "Cancel button should be hidden");
+ });
+
+ add_task(async function test_extension_partial_success() {
+ const EXTENSIONS_INFO_STRING = "Extensions info string";
+ const EXTENSIONS_INFO_HREF = "about:addons";
+ const EXTENSIONS_SUPPORT_STRING = "extensions support string";
+ const EXTENSIONS_SUPPORT_HREF = "about:blank";
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.PROGRESS,
+ key: "chrome",
+ progress: {
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.INFO,
+ message: EXTENSIONS_INFO_STRING,
+ linkText: EXTENSIONS_SUPPORT_STRING,
+ linkURL: EXTENSIONS_SUPPORT_HREF,
+ }
+ }
+ });
+ is(
+ gDeck.selectedViewName,
+ "page-progress",
+ "Should have the progress page selected"
+ );
+
+ let extensionsGroup = getResourceGroup(MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS);
+ ok(!isHidden(extensionsGroup), "Extensions group should be visible");
+ let progressIcon = extensionsGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "info",
+ "Progress should be completed, in info state"
+ );
+ is(
+ extensionsGroup.querySelector("a.message-text").textContent,
+ EXTENSIONS_INFO_STRING
+ );
+ is(
+ extensionsGroup.querySelector("a.message-text").href,
+ EXTENSIONS_INFO_HREF
+ );
+ is(
+ extensionsGroup.querySelector(".support-text").textContent,
+ EXTENSIONS_SUPPORT_STRING
+ );
+ is(
+ extensionsGroup.querySelector(".support-text").href,
+ EXTENSIONS_SUPPORT_HREF
+ );
+
+ // With progress being complete, the header should be using the completed
+ // migration string.
+ let header = gShadowRoot.querySelector("#progress-header");
+ is(
+ header.getAttribute("data-l10n-id"),
+ "migration-wizard-progress-done-header",
+ "Should be showing completed migration header string"
+ );
+
+ let progressPage = gShadowRoot.querySelector("div[name='page-progress']");
+ let doneButton = progressPage.querySelector(".done-button");
+ ok(!isHidden(doneButton), "Done button should be visible and enabled");
+ let cancelButton = progressPage.querySelector(".cancel-close");
+ ok(isHidden(cancelButton), "Cancel button should be hidden");
+ });
+
+ add_task(async function test_extension_error() {
+ const EXTENSIONS_ERROR_STRING = "Extensions error string";
+ const EXTENSIONS_SUPPORT_STRING = "extensions support string";
+ const EXTENSIONS_SUPPORT_HREF = "about:blank";
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.PROGRESS,
+ key: "chrome",
+ progress: {
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.WARNING,
+ message: EXTENSIONS_ERROR_STRING,
+ linkText: EXTENSIONS_SUPPORT_STRING,
+ linkURL: EXTENSIONS_SUPPORT_HREF,
+ }
+ }
+ });
+ is(
+ gDeck.selectedViewName,
+ "page-progress",
+ "Should have the progress page selected"
+ );
+
+ let extensionsGroup = getResourceGroup(MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS);
+ ok(!isHidden(extensionsGroup), "Extensions group should be visible");
+ let progressIcon = extensionsGroup.querySelector(".progress-icon");
+ is(progressIcon.getAttribute("state"), "warning");
+ is(
+ extensionsGroup.querySelector("a.message-text").textContent,
+ ""
+ );
+ is(
+ extensionsGroup.querySelector("span.message-text").textContent,
+ EXTENSIONS_ERROR_STRING
+ );
+ is(
+ extensionsGroup.querySelector(".support-text").textContent,
+ EXTENSIONS_SUPPORT_STRING
+ );
+ is(
+ extensionsGroup.querySelector(".support-text").href,
+ EXTENSIONS_SUPPORT_HREF
+ );
+
+ // With progress being complete, the header should be using the completed
+ // migration string.
+ let header = gShadowRoot.querySelector("#progress-header");
+ is(
+ header.getAttribute("data-l10n-id"),
+ "migration-wizard-progress-done-with-warnings-header",
+ "Should be showing completed migration header string"
+ );
+
+ let progressPage = gShadowRoot.querySelector("div[name='page-progress']");
+ let doneButton = progressPage.querySelector(".done-button");
+ ok(!isHidden(doneButton), "Done button should be visible and enabled");
+ let cancelButton = progressPage.querySelector(".cancel-close");
+ ok(isHidden(cancelButton), "Cancel button should be hidden");
+ });
+
+ /**
+ * Tests that the wizard can show partial progress during file migration.
+ */
+ add_task(async function test_partial_file_progress() {
+ const PASSWORDS_SUCCESS_STRING = "Some passwords success string";
+ const TITLE = "Partial progress";
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS,
+ title: "Partial progress",
+ progress: {
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_FROM_FILE]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
+ message: PASSWORDS_SUCCESS_STRING,
+ },
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.LOADING,
+ },
+ },
+ });
+
+ is(
+ gDeck.selectedViewName,
+ "page-file-import-progress",
+ "Should have the file progress page selected"
+ );
+
+ let header = gShadowRoot.querySelector("#file-import-progress-header");
+ is(header.textContent, TITLE, "Title is set correctly.");
+
+ let passwordsFromFileGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_FROM_FILE
+ );
+ ok(!isHidden(passwordsFromFileGroup), "Passwords from file group should be visible");
+ let progressIcon = passwordsFromFileGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "success",
+ "Progress should be completed"
+ );
+ is(
+ passwordsFromFileGroup.querySelector(".message-text").textContent,
+ PASSWORDS_SUCCESS_STRING
+ );
+
+ let passwordsUpdatedGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_UPDATED
+ );
+ ok(isHidden(passwordsUpdatedGroup), "Passwords updated group should be hidden");
+
+ let passwordsNewGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW
+ );
+ ok(!isHidden(passwordsNewGroup), "Passwords new group should be visible");
+ progressIcon = passwordsNewGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "loading",
+ "Progress should be still be underway"
+ );
+ is(passwordsNewGroup.querySelector(".message-text").textContent.trim(), "");
+
+ let progressPage = gShadowRoot.querySelector("div[name='page-file-import-progress']");
+ let doneButton = progressPage.querySelector(".done-button");
+ ok(isHidden(doneButton), "Done button should be hidden");
+ let cancelButton = progressPage.querySelector(".cancel-close");
+ ok(!isHidden(cancelButton), "Cancel button should be visible");
+ ok(cancelButton.disabled, "Cancel button should be disabled");
+ });
+
+ /**
+ * Tests that the wizard can show completed migration progress.
+ */
+ add_task(async function test_completed_file_progress() {
+ const PASSWORDS_NEW_SUCCESS_STRING = "2 added";
+ const PASSWORDS_UPDATED_SUCCESS_STRING = "5 updated";
+ const TITLE = "Done doing file import";
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS,
+ title: TITLE,
+ progress: {
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
+ message: PASSWORDS_NEW_SUCCESS_STRING,
+ },
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_UPDATED]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
+ message: PASSWORDS_UPDATED_SUCCESS_STRING,
+ },
+ },
+ });
+ is(
+ gDeck.selectedViewName,
+ "page-file-import-progress",
+ "Should have the file progress page selected"
+ );
+
+ let header = gShadowRoot.querySelector("#file-import-progress-header");
+ is(header.textContent, TITLE, "Title is set correctly.");
+
+ let passwordsNewGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW
+ );
+ ok(!isHidden(passwordsNewGroup), "Passwords new group should be visible");
+ let progressIcon = passwordsNewGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "success",
+ "Progress should be completed"
+ );
+ is(
+ passwordsNewGroup.querySelector(".message-text").textContent,
+ PASSWORDS_NEW_SUCCESS_STRING
+ );
+
+ let passwordsUpdatedGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_UPDATED
+ );
+ ok(!isHidden(passwordsUpdatedGroup), "Passwords updated group should be visible");
+ progressIcon = passwordsUpdatedGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "success",
+ "Progress should be completed"
+ );
+ is(
+ passwordsUpdatedGroup.querySelector(".message-text").textContent,
+ PASSWORDS_UPDATED_SUCCESS_STRING
+ );
+
+ let passwordsFromFileGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_FROM_FILE
+ );
+ ok(isHidden(passwordsFromFileGroup), "Passwords from file group should be hidden");
+
+ let progressPage = gShadowRoot.querySelector("div[name='page-file-import-progress']");
+ let doneButton = progressPage.querySelector(".done-button");
+ ok(!isHidden(doneButton), "Done button should be visible and enabled");
+ let cancelButton = progressPage.querySelector(".cancel-close");
+ ok(isHidden(cancelButton), "Cancel button should be hidden");
+ });
+
+ /**
+ * Tests that the buttons that dismiss the wizard when embedded in
+ * a dialog are only visible when in dialog mode, and dispatch a
+ * MigrationWizard:Close event when clicked.
+ */
+ add_task(async function test_dialog_mode_close() {
+ gWiz.toggleAttribute("dialog-mode", true);
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: MIGRATOR_PROFILE_INSTANCES,
+ });
+
+ // For now, there's only a single .cancel-close button, so let's just test
+ // that one. Let's make this test fail if there are multiple so that we can
+ // then update this test to switch to the right pages to test those buttons
+ // too.
+ let buttons = gShadowRoot.querySelectorAll(".cancel-close:not([disabled])");
+ ok(
+ buttons.length,
+ "This test expects at least one enabled .cancel-close button"
+ );
+ let button = buttons[0];
+ ok(
+ !isHidden(button),
+ ".cancel-close button should be visible in dialog mode."
+ );
+ let closeEvent = BrowserTestUtils.waitForEvent(gWiz, "MigrationWizard:Close");
+ button.click();
+ await closeEvent;
+
+ gWiz.toggleAttribute("dialog-mode", false);
+ ok(
+ isHidden(button),
+ ".cancel-close button should be hidden when not in dialog mode."
+ );
+ });
+
+ /**
+ * Internet Explorer and Edge refer to bookmarks as "favorites",
+ * and we change our labels to suit when either of those browsers are
+ * selected as the migration source. This test tests that behavior in the
+ * selection page.
+ */
+ add_task(async function test_ie_edge_favorites_selection() {
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: MIGRATOR_PROFILE_INSTANCES,
+ showImportAll: false,
+ });
+
+ let bookmarksCheckboxLabel = gShadowRoot.querySelector("#bookmarks");
+ let span = bookmarksCheckboxLabel.querySelector("span[default-data-l10n-id]");
+ ok(span, "The bookmarks selection span has a default-data-l10n-id attribute");
+ is(
+ span.getAttribute("data-l10n-id"),
+ span.getAttribute("default-data-l10n-id"),
+ "Should be showing the default string for bookmarks"
+ );
+
+ // Now test when in Variant 2, for the string in the <summary>.
+ let selectedDataUpdated = BrowserTestUtils.waitForEvent(
+ gWiz,
+ "MigrationWizard:ResourcesUpdated"
+ );
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: MIGRATOR_PROFILE_INSTANCES,
+ showImportAll: true,
+ });
+
+ await selectedDataUpdated;
+
+ let summary = gShadowRoot.querySelector("summary");
+ ok(
+ summary.textContent.toLowerCase().includes("bookmarks"),
+ "Summary should include the string 'bookmarks'"
+ );
+
+ for (let key of MigrationWizardConstants.USES_FAVORITES) {
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: [{
+ key,
+ displayName: "Legacy Microsoft Browser",
+ resourceTypes: ["BOOKMARKS"],
+ profile: null,
+ hasPermissions: true,
+ }],
+ showImportAll: false,
+ });
+
+ is(
+ span.getAttribute("data-l10n-id"),
+ span.getAttribute("ie-edge-data-l10n-id"),
+ "Should be showing the IE/Edge string for bookmarks"
+ );
+
+ // Now test when in Variant 2, for the string in the <summary>.
+ selectedDataUpdated = BrowserTestUtils.waitForEvent(
+ gWiz,
+ "MigrationWizard:ResourcesUpdated"
+ );
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: [{
+ key,
+ displayName: "Legacy Microsoft Browser",
+ resourceTypes: ["BOOKMARKS"],
+ profile: null,
+ hasPermissions: true,
+ }],
+ showImportAll: true,
+ });
+
+ await selectedDataUpdated;
+
+ ok(
+ summary.textContent.toLowerCase().includes("favorites"),
+ "Summary should include the string 'favorites'"
+ );
+ }
+ });
+
+ /**
+ * Internet Explorer and Edge refer to bookmarks as "favorites",
+ * and we change our labels to suit when either of those browsers are
+ * selected as the migration source. This test tests that behavior in the
+ * progress page
+ */
+ add_task(async function test_ie_edge_favorites_progress() {
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.PROGRESS,
+ key: "chrome",
+ progress: {
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: {
+ inProgress: false,
+ message: "A string from the parent",
+ },
+ },
+ });
+
+ let bookmarksGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS
+ );
+ let span = bookmarksGroup.querySelector("span[default-data-l10n-id]");
+ ok(span, "Should have found a span with default-data-l10n-id");
+ is(
+ span.getAttribute("data-l10n-id"),
+ span.getAttribute("default-data-l10n-id"),
+ "Should be using the default string."
+ );
+
+
+ for (let key of MigrationWizardConstants.USES_FAVORITES) {
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.PROGRESS,
+ key,
+ progress: {
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: {
+ inProgress: false,
+ message: "A string from the parent",
+ },
+ },
+ });
+
+ is(
+ span.getAttribute("data-l10n-id"),
+ span.getAttribute("ie-edge-data-l10n-id"),
+ "Should be showing the IE/Edge string for bookmarks"
+ );
+ }
+ });
+
+ /**
+ * Tests that the button shown in either the browser migration success or
+ * file migration success pages is a "continue" button rather than a
+ * "done" button.
+ */
+ add_task(async function test_embedded_continue_button() {
+ gWiz.toggleAttribute("dialog-mode", false);
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.PROGRESS,
+ key: "chrome",
+ progress: {
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
+ message: "A string from the parent",
+ },
+ },
+ });
+
+ let progressPage = gShadowRoot.querySelector("div[name='page-progress']");
+ let cancelButton = progressPage.querySelector(".cancel-close");
+ ok(isHidden(cancelButton), "Cancel button should be hidden");
+ let doneButton = progressPage.querySelector(".done-button");
+ ok(isHidden(doneButton), "Done button should be hidden when embedding");
+ let continueButton = progressPage.querySelector(".continue-button");
+ ok(!isHidden(continueButton), "Continue button should be displayed");
+
+ let content = document.getElementById("content");
+
+ let promise = new Promise(resolve => {
+ content.addEventListener("MigrationWizard:Close", resolve, {
+ once: true,
+ });
+ });
+ continueButton.click();
+ await promise;
+ ok(
+ true,
+ "Clicking on the Continue button sent the MigrationWizard:Close event."
+ );
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS,
+ title: "Done importing file data",
+ progress: {
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]: {
+ inProgress: false,
+ message: "Some message",
+ },
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_UPDATED]: {
+ inProgress: false,
+ message: "Some other",
+ },
+ },
+ });
+
+ progressPage = gShadowRoot.querySelector("div[name='page-file-import-progress']");
+ cancelButton = progressPage.querySelector(".cancel-close");
+ ok(isHidden(cancelButton), "Cancel button should be hidden");
+ doneButton = progressPage.querySelector(".done-button");
+ ok(isHidden(doneButton), "Done button should be hidden when embedding");
+ continueButton = progressPage.querySelector(".continue-button");
+ ok(!isHidden(continueButton), "Continue button should be displayed");
+
+ promise = new Promise(resolve => {
+ content.addEventListener("MigrationWizard:Close", resolve, {
+ once: true,
+ });
+ });
+ continueButton.click();
+ await promise;
+ ok(
+ true,
+ "Clicking on the Continue button sent the MigrationWizard:Close event."
+ );
+
+ gWiz.toggleAttribute("dialog-mode", true);
+ });
+
+ /**
+ * Tests that if a progress update comes down which puts a resource from
+ * being done to loading, that the status message is cleared.
+ */
+ add_task(async function test_clear_status_message_when_in_progress() {
+ const STRING_TO_CLEAR = "This string should get cleared";
+ gWiz.toggleAttribute("dialog-mode", false);
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.PROGRESS,
+ key: "chrome",
+ progress: {
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
+ message: STRING_TO_CLEAR,
+ },
+ },
+ });
+
+ let bookmarksGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS
+ );
+ ok(!isHidden(bookmarksGroup), "Bookmarks group should be visible");
+ let progressIcon = bookmarksGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "success",
+ "Progress should be completed"
+ );
+ is(
+ bookmarksGroup.querySelector(".message-text").textContent,
+ STRING_TO_CLEAR
+ );
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.PROGRESS,
+ key: "chrome",
+ progress: {
+ [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.LOADING,
+ message: "",
+ },
+ },
+ });
+
+ ok(!isHidden(bookmarksGroup), "Bookmarks group should be visible");
+ is(
+ progressIcon.getAttribute("state"),
+ "loading",
+ "Progress should be still be underway"
+ );
+ is(
+ bookmarksGroup.querySelector(".message-text").textContent.trim(),
+ ""
+ );
+ });
+
+ /**
+ * Tests that if a file progress update comes down which puts a resource
+ * from being done to loading, that the status message is cleared.
+ *
+ * This is extremely similar to the above test, except that it's for progress
+ * updates for file resources.
+ */
+ add_task(async function test_clear_status_message_when_in_file_progress() {
+ const STRING_TO_CLEAR = "This string should get cleared";
+ gWiz.toggleAttribute("dialog-mode", false);
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS,
+ key: "chrome",
+ progress: {
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
+ message: STRING_TO_CLEAR,
+ },
+ },
+ });
+
+ let passwordsGroup = getResourceGroup(
+ MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW
+ );
+ ok(!isHidden(passwordsGroup), "Passwords group should be visible");
+ let progressIcon = passwordsGroup.querySelector(".progress-icon");
+ is(
+ progressIcon.getAttribute("state"),
+ "success",
+ "Progress should be completed"
+ );
+ is(
+ passwordsGroup.querySelector(".message-text").textContent,
+ STRING_TO_CLEAR
+ );
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS,
+ key: "chrome",
+ progress: {
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]: {
+ value: MigrationWizardConstants.PROGRESS_VALUE.LOADING,
+ message: "",
+ },
+ },
+ });
+
+ ok(!isHidden(passwordsGroup), "Passwords group should be visible");
+ is(
+ progressIcon.getAttribute("state"),
+ "loading",
+ "Progress should be still be underway"
+ );
+ is(
+ passwordsGroup.querySelector(".message-text").textContent.trim(),
+ ""
+ );
+ });
+
+ /**
+ * Tests that the migration wizard can be put into the selection page after
+ * a file migrator error and show an error message.
+ */
+ add_task(async function test_file_migrator_error() {
+ const FILE_MIGRATOR_KEY = "some-file-migrator";
+ const FILE_IMPORT_ERROR_MESSAGE = "This is an error message";
+ const MIGRATORS = [
+ {
+ key: "some-browser-0",
+ type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER,
+ displayName: "Some Browser 0",
+ resourceTypes: ["HISTORY", "FORMDATA", "PASSWORDS", "BOOKMARKS"],
+ profile: { id: "person-2", name: "Person 2" },
+ hasPermissions: true,
+ },
+ {
+ key: "some-browser-1",
+ type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER,
+ displayName: "Some Browser 1",
+ resourceTypes: ["HISTORY", "BOOKMARKS"],
+ profile: null,
+ hasPermissions: true,
+ },
+ {
+ key: FILE_MIGRATOR_KEY,
+ type: MigrationWizardConstants.MIGRATOR_TYPES.FILE,
+ displayName: "Some File Migrator",
+ resourceTypes: [],
+ },
+ ];
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: MIGRATORS,
+ showImportAll: true,
+ migratorKey: "some-file-migrator",
+ fileImportErrorMessage: FILE_IMPORT_ERROR_MESSAGE,
+ });
+
+ let errorMessageContainer = gShadowRoot.querySelector(".file-import-error");
+ ok(!isHidden(errorMessageContainer), "Error message should be shown.");
+ let errorText = gShadowRoot.querySelector("#file-import-error-message").textContent;
+ is(errorText, FILE_IMPORT_ERROR_MESSAGE);
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: MIGRATORS,
+ showImportAll: true,
+ });
+
+ ok(isHidden(errorMessageContainer), "Error message should be hidden.");
+ errorText = gShadowRoot.querySelector("#file-import-error-message").textContent;
+ is(errorText, "");
+ });
+
+ /**
+ * Tests that the variant of the wizard can be forced via
+ * attributes on the migration-wizard element.
+ */
+ add_task(async function force_show_import_all() {
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: MIGRATOR_PROFILE_INSTANCES,
+ showImportAll: true,
+ });
+
+ let selectionPage = gShadowRoot.querySelector("div[name='page-selection']");
+ let details = gShadowRoot.querySelector("details");
+ ok(
+ selectionPage.hasAttribute("show-import-all"),
+ "Should be paying attention to showImportAll=true on state object"
+ );
+ ok(!details.open, "Details collapsed by default");
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: MIGRATOR_PROFILE_INSTANCES,
+ showImportAll: false,
+ });
+
+ ok(
+ !selectionPage.hasAttribute("show-import-all"),
+ "Should be paying attention to showImportAll=false on state object"
+ );
+ ok(details.open, "Details expanded by default");
+
+ gWiz.setAttribute("force-show-import-all", "false");
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: MIGRATOR_PROFILE_INSTANCES,
+ showImportAll: true,
+ });
+ ok(
+ !selectionPage.hasAttribute("show-import-all"),
+ "Should be paying attention to force-show-import-all=false on DOM node"
+ );
+ ok(details.open, "Details expanded by default");
+
+ gWiz.setAttribute("force-show-import-all", "true");
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: MIGRATOR_PROFILE_INSTANCES,
+ showImportAll: false,
+ });
+ ok(
+ selectionPage.hasAttribute("show-import-all"),
+ "Should be paying attention to force-show-import-all=true on DOM node"
+ );
+ ok(!details.open, "Details collapsed by default");
+
+ gWiz.removeAttribute("force-show-import-all");
+ });
+
+ /**
+ * Tests that for non-Safari migrators without permissions, we show
+ * the appropriate message and the button for getting permissions.
+ */
+ add_task(async function no_permissions() {
+ const SOME_FAKE_PERMISSION_PATH = "/some/fake/path";
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: [{
+ key: "some-browser-0",
+ type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER,
+ displayName: "Some Browser 0 with no permissions",
+ resourceTypes: [],
+ profile: null,
+ hasPermissions: false,
+ permissionsPath: SOME_FAKE_PERMISSION_PATH,
+ }],
+ showImportAll: false,
+ });
+
+ let selectionPage = gShadowRoot.querySelector("div[name='page-selection']");
+ ok(selectionPage.hasAttribute("no-permissions"), "no-permissions attribute set.");
+
+ let resourceList = gShadowRoot.querySelector(".resource-selection-details");
+ ok(isHidden(resourceList), "Resources list is hidden.");
+
+ let importButton = gShadowRoot.querySelector("#import");
+ ok(isHidden(importButton), "Import button hidden.");
+ let noPermissionsMessage = gShadowRoot.querySelector(".no-permissions-message");
+ ok(!isHidden(noPermissionsMessage), "No permissions message shown.");
+ let getPermissionButton = gShadowRoot.querySelector("#get-permissions");
+ ok(!isHidden(getPermissionButton), "Get permissions button shown.");
+
+ let step2 = gShadowRoot.querySelector(".migration-no-permissions-instructions-step2");
+ ok(step2.hasAttribute("data-l10n-args"));
+ is(JSON.parse(step2.getAttribute("data-l10n-args")).permissionsPath, SOME_FAKE_PERMISSION_PATH);
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: MIGRATOR_PROFILE_INSTANCES,
+ showImportAll: false,
+ });
+
+ ok(!selectionPage.hasAttribute("no-permissions"), "no-permissions attribute set to false.");
+ ok(!isHidden(resourceList), "Resources list is shown.");
+ ok(!isHidden(importButton), "Import button is shown.");
+ ok(isHidden(noPermissionsMessage), "No permissions message hidden.");
+ ok(isHidden(getPermissionButton), "Get permissions button hidden.");
+ });
+
+ /**
+ * Tests that for the Safari migrator without permissions, we show the
+ * normal resources list and impor tbutton instead of the no permissions
+ * message. Safari has a special flow where permissions are requested
+ * only after resource selection has occurred.
+ */
+ add_task(async function no_permissions_safari() {
+ const SOME_FAKE_PERMISSION_PATH = "/some/fake/safari/path";
+
+ gWiz.setState({
+ page: MigrationWizardConstants.PAGES.SELECTION,
+ migrators: [{
+ key: "safari",
+ type: MigrationWizardConstants.MIGRATOR_TYPES.BROWSER,
+ displayName: "Safari with no permissions",
+ resourceTypes: ["HISTORY", "BOOKMARKS"],
+ profile: null,
+ hasPermissions: false,
+ permissionsPath: SOME_FAKE_PERMISSION_PATH,
+ }],
+ showImportAll: false,
+ });
+
+ let selectionPage = gShadowRoot.querySelector("div[name='page-selection']");
+ ok(!selectionPage.hasAttribute("no-permissions"), "no-permissions attribute not set.");
+
+ let resourceList = gShadowRoot.querySelector(".resource-selection-details");
+ ok(!isHidden(resourceList), "Resources list is shown.");
+
+ let importButton = gShadowRoot.querySelector("#import");
+ ok(!isHidden(importButton), "Import button shown.");
+ let noPermissionsMessage = gShadowRoot.querySelector(".no-permissions-message");
+ ok(isHidden(noPermissionsMessage), "No permissions message hidden.");
+ let getPermissionButton = gShadowRoot.querySelector("#get-permissions");
+ ok(isHidden(getPermissionButton), "Get permissions button hiddne.");
+ });
+ </script>
+ </head>
+ <body>
+ <p id="display"></p>
+ <div id="content">
+ <migration-wizard id="test-wizard" dialog-mode=""></migration-wizard>
+ </div>
+ <pre id="test"></pre>
+ </body>
+</html>
diff --git a/browser/components/migration/tests/head-common.js b/browser/components/migration/tests/head-common.js
new file mode 100644
index 0000000000..025d3e5a16
--- /dev/null
+++ b/browser/components/migration/tests/head-common.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { MigrationWizardConstants } = ChromeUtils.importESModule(
+ "chrome://browser/content/migration/migration-wizard-constants.mjs"
+);
+
+/**
+ * Returns the constant strings from MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES
+ * that aren't also part of MigrationWizardConstants.PROFILE_RESET_ONLY_RESOURCE_TYPES.
+ *
+ * This is the set of resources that the user can actually choose to migrate via
+ * checkboxes.
+ *
+ * @returns {string[]}
+ */
+function getChoosableResourceTypes() {
+ return Object.keys(MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES).filter(
+ resourceType =>
+ !MigrationWizardConstants.PROFILE_RESET_ONLY_RESOURCE_TYPES[resourceType]
+ );
+}
diff --git a/browser/components/migration/tests/marionette/manifest.toml b/browser/components/migration/tests/marionette/manifest.toml
new file mode 100644
index 0000000000..1e3b536ee2
--- /dev/null
+++ b/browser/components/migration/tests/marionette/manifest.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+run-if = ["buildapp == 'browser'"]
+
+["test_refresh_firefox.py"]
diff --git a/browser/components/migration/tests/marionette/test_refresh_firefox.py b/browser/components/migration/tests/marionette/test_refresh_firefox.py
new file mode 100644
index 0000000000..ea5d6bce99
--- /dev/null
+++ b/browser/components/migration/tests/marionette/test_refresh_firefox.py
@@ -0,0 +1,703 @@
+import os
+import time
+
+from marionette_driver.errors import NoAlertPresentException
+from marionette_harness import MarionetteTestCase
+
+
+# Holds info about things we need to cleanup after the tests are done.
+class PendingCleanup:
+ desktop_backup_path = None
+ reset_profile_path = None
+ reset_profile_local_path = None
+
+ def __init__(self, profile_name_to_remove):
+ self.profile_name_to_remove = profile_name_to_remove
+
+
+class TestFirefoxRefresh(MarionetteTestCase):
+ _sandbox = "firefox-refresh"
+
+ _username = "marionette-test-login"
+ _password = "marionette-test-password"
+ _bookmarkURL = "about:mozilla"
+ _bookmarkText = "Some bookmark from Marionette"
+
+ _cookieHost = "firefox-refresh.marionette-test.mozilla.org"
+ _cookiePath = "some/cookie/path"
+ _cookieName = "somecookie"
+ _cookieValue = "some cookie value"
+
+ _historyURL = "http://firefox-refresh.marionette-test.mozilla.org/"
+ _historyTitle = "Test visit for Firefox Reset"
+
+ _formHistoryFieldName = "some-very-unique-marionette-only-firefox-reset-field"
+ _formHistoryValue = "special-pumpkin-value"
+
+ _formAutofillAvailable = False
+ _formAutofillAddressGuid = None
+
+ _expectedURLs = ["about:robots", "about:mozilla"]
+
+ def savePassword(self):
+ self.runAsyncCode(
+ """
+ let [username, password, resolve] = arguments;
+ let myLogin = new global.LoginInfo(
+ "test.marionette.mozilla.com",
+ "http://test.marionette.mozilla.com/some/form/",
+ null,
+ username,
+ password,
+ "username",
+ "password"
+ );
+ Services.logins.addLoginAsync(myLogin)
+ .then(() => resolve(false), resolve);
+ """,
+ script_args=(self._username, self._password),
+ )
+
+ def createBookmarkInMenu(self):
+ error = self.runAsyncCode(
+ """
+ // let url = arguments[0];
+ // let title = arguments[1];
+ // let resolve = arguments[arguments.length - 1];
+ let [url, title, resolve] = arguments;
+ PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid, url, title
+ }).then(() => resolve(false), resolve);
+ """,
+ script_args=(self._bookmarkURL, self._bookmarkText),
+ )
+ if error:
+ print(error)
+
+ def createBookmarksOnToolbar(self):
+ error = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ let children = [];
+ for (let i = 1; i <= 5; i++) {
+ children.push({url: `about:rights?p=${i}`, title: `Bookmark ${i}`});
+ }
+ PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children
+ }).then(() => resolve(false), resolve);
+ """
+ )
+ if error:
+ print(error)
+
+ def createHistory(self):
+ error = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ PlacesUtils.history.insert({
+ url: arguments[0],
+ title: arguments[1],
+ visits: [{
+ date: new Date(Date.now() - 5000),
+ referrer: "about:mozilla"
+ }]
+ }).then(() => resolve(false),
+ ex => resolve("Unexpected error in adding visit: " + ex));
+ """,
+ script_args=(self._historyURL, self._historyTitle),
+ )
+ if error:
+ print(error)
+
+ def createFormHistory(self):
+ error = self.runAsyncCode(
+ """
+ let updateDefinition = {
+ op: "add",
+ fieldname: arguments[0],
+ value: arguments[1],
+ firstUsed: (Date.now() - 5000) * 1000,
+ };
+ let resolve = arguments[arguments.length - 1];
+ global.FormHistory.update(updateDefinition).then(() => {
+ resolve(false);
+ }, error => {
+ resolve("Unexpected error in adding formhistory: " + error);
+ });
+ """,
+ script_args=(self._formHistoryFieldName, self._formHistoryValue),
+ )
+ if error:
+ print(error)
+
+ def createFormAutofill(self):
+ if not self._formAutofillAvailable:
+ return
+ self._formAutofillAddressGuid = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ const TEST_ADDRESS_1 = {
+ "given-name": "John",
+ "additional-name": "R.",
+ "family-name": "Smith",
+ organization: "World Wide Web Consortium",
+ "street-address": "32 Vassar Street\\\nMIT Room 32-G524",
+ "address-level2": "Cambridge",
+ "address-level1": "MA",
+ "postal-code": "02139",
+ country: "US",
+ tel: "+15195555555",
+ email: "user@example.com",
+ };
+ return global.formAutofillStorage.initialize().then(() => {
+ return global.formAutofillStorage.addresses.add(TEST_ADDRESS_1);
+ }).then(resolve);
+ """
+ )
+
+ def createCookie(self):
+ self.runCode(
+ """
+ // Expire in 15 minutes:
+ let expireTime = Math.floor(Date.now() / 1000) + 15 * 60;
+ Services.cookies.add(arguments[0], arguments[1], arguments[2], arguments[3],
+ true, false, false, expireTime, {},
+ Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_UNSET);
+ """,
+ script_args=(
+ self._cookieHost,
+ self._cookiePath,
+ self._cookieName,
+ self._cookieValue,
+ ),
+ )
+
+ def createSession(self):
+ self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ const COMPLETE_STATE = Ci.nsIWebProgressListener.STATE_STOP +
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+ let { TabStateFlusher } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
+ );
+ let expectedURLs = Array.from(arguments[0])
+ gBrowser.addTabsProgressListener({
+ onStateChange(browser, webprogress, request, flags, status) {
+ try {
+ request && request.QueryInterface(Ci.nsIChannel);
+ } catch (ex) {}
+ let uriLoaded = request.originalURI && request.originalURI.spec;
+ if ((flags & COMPLETE_STATE == COMPLETE_STATE) && uriLoaded &&
+ expectedURLs.includes(uriLoaded)) {
+ TabStateFlusher.flush(browser).then(function() {
+ expectedURLs.splice(expectedURLs.indexOf(uriLoaded), 1);
+ if (!expectedURLs.length) {
+ gBrowser.removeTabsProgressListener(this);
+ resolve();
+ }
+ });
+ }
+ }
+ });
+ let expectedTabs = new Set();
+ for (let url of expectedURLs) {
+ expectedTabs.add(gBrowser.addTab(url, {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ }));
+ }
+ // Close any other tabs that might be open:
+ let allTabs = Array.from(gBrowser.tabs);
+ for (let tab of allTabs) {
+ if (!expectedTabs.has(tab)) {
+ gBrowser.removeTab(tab);
+ }
+ }
+ """, # NOQA: E501
+ script_args=(self._expectedURLs,),
+ )
+
+ def createFxa(self):
+ # This script will write an entry to the login manager and create
+ # a signedInUser.json in the profile dir.
+ self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ let { FxAccountsStorageManager } = ChromeUtils.import(
+ "resource://gre/modules/FxAccountsStorage.jsm"
+ );
+ let storage = new FxAccountsStorageManager();
+ let data = {email: "test@test.com", uid: "uid", keyFetchToken: "top-secret"};
+ storage.initialize(data);
+ storage.finalize().then(resolve);
+ """
+ )
+
+ def createSync(self):
+ # This script will write the canonical preference which indicates a user
+ # is signed into sync.
+ self.marionette.execute_script(
+ """
+ Services.prefs.setStringPref("services.sync.username", "test@test.com");
+ """
+ )
+
+ def checkPassword(self):
+ loginInfo = self.runAsyncCode(
+ """
+ let [resolve] = arguments;
+ Services.logins.searchLoginsAsync({
+ origin: "test.marionette.mozilla.com",
+ formActionOrigin: "http://test.marionette.mozilla.com/some/form/",
+ }).then(ary => resolve(ary.length ? ary : {username: "null", password: "null"}));
+ """
+ )
+ self.assertEqual(len(loginInfo), 1)
+ self.assertEqual(loginInfo[0]["username"], self._username)
+ self.assertEqual(loginInfo[0]["password"], self._password)
+
+ loginCount = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ Services.logins.getAllLogins().then(logins => resolve(logins.length));
+ """
+ )
+ # Note that we expect 2 logins - one from us, one from sync.
+ self.assertEqual(loginCount, 2, "No other logins are present")
+
+ def checkBookmarkInMenu(self):
+ titleInBookmarks = self.runAsyncCode(
+ """
+ let [url, resolve] = arguments;
+ PlacesUtils.bookmarks.fetch({url}).then(
+ bookmark => resolve(bookmark ? bookmark.title : ""),
+ ex => resolve(ex)
+ );
+ """,
+ script_args=(self._bookmarkURL,),
+ )
+ self.assertEqual(titleInBookmarks, self._bookmarkText)
+
+ def checkBookmarkToolbarVisibility(self):
+ toolbarVisible = self.marionette.execute_script(
+ """
+ const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL;
+ return Services.xulStore.getValue(BROWSER_DOCURL, "PersonalToolbar", "collapsed");
+ """
+ )
+ if toolbarVisible == "":
+ toolbarVisible = "false"
+ self.assertEqual(toolbarVisible, "false")
+
+ def checkHistory(self):
+ historyResult = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ PlacesUtils.history.fetch(arguments[0]).then(pageInfo => {
+ if (!pageInfo) {
+ resolve("No visits found");
+ } else {
+ resolve(pageInfo);
+ }
+ }).catch(e => {
+ resolve("Unexpected error in fetching page: " + e);
+ });
+ """,
+ script_args=(self._historyURL,),
+ )
+ if type(historyResult) == str:
+ self.fail(historyResult)
+ return
+
+ self.assertEqual(historyResult["title"], self._historyTitle)
+
+ def checkFormHistory(self):
+ formFieldResults = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ let results = [];
+ global.FormHistory.search(["value"], {fieldname: arguments[0]})
+ .then(resolve);
+ """,
+ script_args=(self._formHistoryFieldName,),
+ )
+ if type(formFieldResults) == str:
+ self.fail(formFieldResults)
+ return
+
+ formFieldResultCount = len(formFieldResults)
+ self.assertEqual(
+ formFieldResultCount,
+ 1,
+ "Should have exactly 1 entry for this field, got %d" % formFieldResultCount,
+ )
+ if formFieldResultCount == 1:
+ self.assertEqual(formFieldResults[0]["value"], self._formHistoryValue)
+
+ formHistoryCount = self.runAsyncCode(
+ """
+ let [resolve] = arguments;
+ global.FormHistory.count({}).then(resolve);
+ """
+ )
+ self.assertEqual(
+ formHistoryCount, 1, "There should be only 1 entry in the form history"
+ )
+
+ def checkFormAutofill(self):
+ if not self._formAutofillAvailable:
+ return
+
+ formAutofillResults = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1];
+ return global.formAutofillStorage.initialize().then(() => {
+ return global.formAutofillStorage.addresses.getAll()
+ }).then(resolve);
+ """,
+ )
+ if type(formAutofillResults) == str:
+ self.fail(formAutofillResults)
+ return
+
+ formAutofillAddressCount = len(formAutofillResults)
+ self.assertEqual(
+ formAutofillAddressCount,
+ 1,
+ "Should have exactly 1 saved address, got %d" % formAutofillAddressCount,
+ )
+ if formAutofillAddressCount == 1:
+ self.assertEqual(
+ formAutofillResults[0]["guid"], self._formAutofillAddressGuid
+ )
+
+ def checkCookie(self):
+ cookieInfo = self.runCode(
+ """
+ try {
+ let cookies = Services.cookies.getCookiesFromHost(arguments[0], {});
+ let cookie = null;
+ for (let hostCookie of cookies) {
+ // getCookiesFromHost returns any cookie from the BASE host.
+ if (hostCookie.rawHost != arguments[0])
+ continue;
+ if (cookie != null) {
+ return "more than 1 cookie! That shouldn't happen!";
+ }
+ cookie = hostCookie;
+ }
+ return {path: cookie.path, name: cookie.name, value: cookie.value};
+ } catch (ex) {
+ return "got exception trying to fetch cookie: " + ex;
+ }
+ """,
+ script_args=(self._cookieHost,),
+ )
+ if not isinstance(cookieInfo, dict):
+ self.fail(cookieInfo)
+ return
+ self.assertEqual(cookieInfo["path"], self._cookiePath)
+ self.assertEqual(cookieInfo["value"], self._cookieValue)
+ self.assertEqual(cookieInfo["name"], self._cookieName)
+
+ def checkSession(self):
+ tabURIs = self.runCode(
+ """
+ return [... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec)
+ """
+ )
+ self.assertSequenceEqual(tabURIs, ["about:welcomeback"])
+
+ # Dismiss modal dialog if any. This is mainly to dismiss the check for
+ # default browser dialog if it shows up.
+ try:
+ alert = self.marionette.switch_to_alert()
+ alert.dismiss()
+ except NoAlertPresentException:
+ pass
+
+ tabURIs = self.runAsyncCode(
+ """
+ let resolve = arguments[arguments.length - 1]
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ window.addEventListener("SSWindowStateReady", function() {
+ window.addEventListener("SSTabRestored", function() {
+ resolve(Array.from(gBrowser.browsers, b => b.currentURI?.spec));
+ }, { capture: false, once: true });
+ }, { capture: false, once: true });
+
+ let fs = function() {
+ if (content.document.readyState === "complete") {
+ content.document.getElementById("errorTryAgain").click();
+ } else {
+ content.window.addEventListener("load", function(event) {
+ content.document.getElementById("errorTryAgain").click();
+ }, { once: true });
+ }
+ };
+
+ Services.prefs.setBoolPref("security.allow_parent_unrestricted_js_loads", true);
+ mm.loadFrameScript("data:application/javascript,(" + fs.toString() + ")()", true);
+ Services.prefs.setBoolPref("security.allow_parent_unrestricted_js_loads", false);
+ """ # NOQA: E501
+ )
+ self.assertSequenceEqual(tabURIs, self._expectedURLs)
+
+ def checkFxA(self):
+ result = self.runAsyncCode(
+ """
+ let { FxAccountsStorageManager } = ChromeUtils.import(
+ "resource://gre/modules/FxAccountsStorage.jsm"
+ );
+ let resolve = arguments[arguments.length - 1];
+ let storage = new FxAccountsStorageManager();
+ let result = {};
+ storage.initialize();
+ storage.getAccountData().then(data => {
+ result.accountData = data;
+ return storage.finalize();
+ }).then(() => {
+ resolve(result);
+ }).catch(err => {
+ resolve(err.toString());
+ });
+ """
+ )
+ if type(result) != dict:
+ self.fail(result)
+ return
+ self.assertEqual(result["accountData"]["email"], "test@test.com")
+ self.assertEqual(result["accountData"]["uid"], "uid")
+ self.assertEqual(result["accountData"]["keyFetchToken"], "top-secret")
+
+ def checkSync(self, expect_sync_user):
+ pref_value = self.marionette.execute_script(
+ """
+ return Services.prefs.getStringPref("services.sync.username", null);
+ """
+ )
+ expected_value = "test@test.com" if expect_sync_user else None
+ self.assertEqual(pref_value, expected_value)
+
+ def checkStartupMigrationStateCleared(self):
+ result = self.runCode(
+ """
+ let { MigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/MigrationUtils.sys.mjs"
+ );
+ return MigrationUtils.isStartupMigration;
+ """
+ )
+ self.assertFalse(result)
+
+ def checkProfile(self, has_migrated=False, expect_sync_user=True):
+ self.checkPassword()
+ self.checkBookmarkInMenu()
+ self.checkHistory()
+ self.checkFormHistory()
+ self.checkFormAutofill()
+ self.checkCookie()
+ self.checkFxA()
+ self.checkSync(expect_sync_user)
+ if has_migrated:
+ self.checkBookmarkToolbarVisibility()
+ self.checkSession()
+ self.checkStartupMigrationStateCleared()
+
+ def createProfileData(self):
+ self.savePassword()
+ self.createBookmarkInMenu()
+ self.createBookmarksOnToolbar()
+ self.createHistory()
+ self.createFormHistory()
+ self.createFormAutofill()
+ self.createCookie()
+ self.createSession()
+ self.createFxa()
+ self.createSync()
+
+ def setUpScriptData(self):
+ self.marionette.set_context(self.marionette.CONTEXT_CHROME)
+ self.runCode(
+ """
+ window.global = {};
+ global.LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init");
+ global.profSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(Ci.nsIToolkitProfileService);
+ global.Preferences = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+ ).Preferences;
+ global.FormHistory = ChromeUtils.import(
+ "resource://gre/modules/FormHistory.jsm"
+ ).FormHistory;
+ """ # NOQA: E501
+ )
+ self._formAutofillAvailable = self.runCode(
+ """
+ try {
+ global.formAutofillStorage = ChromeUtils.import(
+ "resource://formautofill/FormAutofillStorage.jsm"
+ ).formAutofillStorage;
+ } catch(e) {
+ return false;
+ }
+ return true;
+ """ # NOQA: E501
+ )
+
+ def runCode(self, script, *args, **kwargs):
+ return self.marionette.execute_script(
+ script, new_sandbox=False, sandbox=self._sandbox, *args, **kwargs
+ )
+
+ def runAsyncCode(self, script, *args, **kwargs):
+ return self.marionette.execute_async_script(
+ script, new_sandbox=False, sandbox=self._sandbox, *args, **kwargs
+ )
+
+ def setUp(self):
+ MarionetteTestCase.setUp(self)
+ self.setUpScriptData()
+
+ self.cleanups = []
+
+ def tearDown(self):
+ # Force yet another restart with a clean profile to disconnect from the
+ # profile and environment changes we've made, to leave a more or less
+ # blank slate for the next person.
+ self.marionette.restart(in_app=False, clean=True)
+ self.setUpScriptData()
+
+ # Super
+ MarionetteTestCase.tearDown(self)
+
+ # A helper to deal with removing a load of files
+ import mozfile
+
+ for cleanup in self.cleanups:
+ if cleanup.desktop_backup_path:
+ mozfile.remove(cleanup.desktop_backup_path)
+
+ if cleanup.reset_profile_path:
+ # Remove ourselves from profiles.ini
+ self.runCode(
+ """
+ let name = arguments[0];
+ let profile = global.profSvc.getProfileByName(name);
+ profile.remove(false)
+ global.profSvc.flush();
+ """,
+ script_args=(cleanup.profile_name_to_remove,),
+ )
+ # Remove the local profile dir if it's not the same as the profile dir:
+ different_path = (
+ cleanup.reset_profile_local_path != cleanup.reset_profile_path
+ )
+ if cleanup.reset_profile_local_path and different_path:
+ mozfile.remove(cleanup.reset_profile_local_path)
+
+ # And delete all the files.
+ mozfile.remove(cleanup.reset_profile_path)
+
+ def doReset(self):
+ profileName = "marionette-test-profile-" + str(int(time.time() * 1000))
+ cleanup = PendingCleanup(profileName)
+ self.runCode(
+ """
+ // Ensure the current (temporary) profile is in profiles.ini:
+ let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let profileName = arguments[1];
+ let myProfile = global.profSvc.createProfile(profD, profileName);
+ global.profSvc.flush()
+
+ // Now add the reset parameters:
+ let prefsToKeep = Array.from(Services.prefs.getChildList("marionette."));
+ // Add all the modified preferences set from geckoinstance.py to avoid
+ // non-local connections.
+ prefsToKeep = prefsToKeep.concat(JSON.parse(
+ Services.env.get("MOZ_MARIONETTE_REQUIRED_PREFS")));
+ let prefObj = {};
+ for (let pref of prefsToKeep) {
+ prefObj[pref] = global.Preferences.get(pref);
+ }
+ Services.env.set("MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS", JSON.stringify(prefObj));
+ Services.env.set("MOZ_RESET_PROFILE_RESTART", "1");
+ Services.env.set("XRE_PROFILE_PATH", arguments[0]);
+ """,
+ script_args=(
+ self.marionette.instance.profile.profile,
+ profileName,
+ ),
+ )
+
+ profileLeafName = os.path.basename(
+ os.path.normpath(self.marionette.instance.profile.profile)
+ )
+
+ # Now restart the browser to get it reset:
+ self.marionette.restart(clean=False, in_app=True)
+ self.setUpScriptData()
+
+ # Determine the new profile path (we'll need to remove it when we're done)
+ [cleanup.reset_profile_path, cleanup.reset_profile_local_path] = self.runCode(
+ """
+ let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let localD = Services.dirsvc.get("ProfLD", Ci.nsIFile);
+ return [profD.path, localD.path];
+ """
+ )
+
+ # Determine the backup path
+ cleanup.desktop_backup_path = self.runCode(
+ """
+ let container;
+ try {
+ container = Services.dirsvc.get("Desk", Ci.nsIFile);
+ } catch (ex) {
+ container = Services.dirsvc.get("Home", Ci.nsIFile);
+ }
+ let bundle = Services.strings.createBundle("chrome://mozapps/locale/profile/profileSelection.properties");
+ let dirName = bundle.formatStringFromName("resetBackupDirectory", [Services.appinfo.name]);
+ container.append(dirName);
+ container.append(arguments[0]);
+ return container.path;
+ """, # NOQA: E501
+ script_args=(profileLeafName,),
+ )
+
+ self.assertTrue(
+ os.path.isdir(cleanup.reset_profile_path),
+ "Reset profile path should be present",
+ )
+ self.assertTrue(
+ os.path.isdir(cleanup.desktop_backup_path),
+ "Backup profile path should be present",
+ )
+ self.assertIn(cleanup.profile_name_to_remove, cleanup.reset_profile_path)
+ return cleanup
+
+ def testResetEverything(self):
+ self.createProfileData()
+
+ self.checkProfile(expect_sync_user=True)
+
+ this_cleanup = self.doReset()
+ self.cleanups.append(this_cleanup)
+
+ # Now check that we're doing OK...
+ self.checkProfile(has_migrated=True, expect_sync_user=True)
+
+ def testFxANoSync(self):
+ # This test doesn't need to repeat all the non-sync tests...
+ # Setup FxA but *not* sync
+ self.createFxa()
+
+ self.checkFxA()
+ self.checkSync(False)
+
+ this_cleanup = self.doReset()
+ self.cleanups.append(this_cleanup)
+
+ self.checkFxA()
+ self.checkSync(False)
diff --git a/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Favicons b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Favicons
new file mode 100644
index 0000000000..fddee798b3
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Favicons
Binary files differ
diff --git a/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data
new file mode 100644
index 0000000000..7e6e843a03
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data
Binary files differ
diff --git a/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Web Data b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Web Data
new file mode 100644
index 0000000000..c557c9b851
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Web Data
Binary files differ
diff --git a/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Local State b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Local State
new file mode 100644
index 0000000000..3f3fecb651
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Local State
@@ -0,0 +1,5 @@
+{
+ "os_crypt" : {
+ "encrypted_key" : "RFBBUEk/ThisNPAPIKeyCanOnlyBeDecryptedByTheOriginalDeviceSoThisWillThrowFromDecryptData"
+ }
+}
diff --git a/browser/components/migration/tests/unit/AppData/LocalWithNoData/Google/Chrome/User Data/Default/Login Data b/browser/components/migration/tests/unit/AppData/LocalWithNoData/Google/Chrome/User Data/Default/Login Data
new file mode 100644
index 0000000000..fd135624c4
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/LocalWithNoData/Google/Chrome/User Data/Default/Login Data
Binary files differ
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat
new file mode 100644
index 0000000000..1835c33583
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat
Binary files differ
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/360Bookmarks b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/360Bookmarks
new file mode 100644
index 0000000000..f51195f54c
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/360Bookmarks
@@ -0,0 +1 @@
+Encrypted canonical bookmarks storage, since 360 SE 10
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2020_08_28.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2020_08_28.favdb
new file mode 100644
index 0000000000..ea466a25bf
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2020_08_28.favdb
@@ -0,0 +1,3 @@
+{
+ "note": "Plain text bookmarks backup, since 360 SE 10."
+}
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2021_12_02.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2021_12_02.favdb
new file mode 100644
index 0000000000..ea466a25bf
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360default_ori_2021_12_02.favdb
@@ -0,0 +1,3 @@
+{
+ "note": "Plain text bookmarks backup, since 360 SE 10."
+}
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2020_08_28.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2020_08_28.favdb
new file mode 100644
index 0000000000..32b4002a32
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2020_08_28.favdb
@@ -0,0 +1 @@
+Encrypted bookmarks backup, since 360 SE 10.
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2021_12_02.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2021_12_02.favdb
new file mode 100644
index 0000000000..32b4002a32
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default/DailyBackup/360sefav_new_2021_12_02.favdb
@@ -0,0 +1 @@
+Encrypted bookmarks backup, since 360 SE 10.
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat
new file mode 100644
index 0000000000..440e7145bd
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/360sefav.dat
@@ -0,0 +1 @@
+Bookmarks storage in legacy SQLite format.
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/DailyBackup/360sefav_2020_08_28.favdb b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/DailyBackup/360sefav_2020_08_28.favdb
new file mode 100644
index 0000000000..d5d939629c
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/0f3ab103a522f4463ecacc36d34eb996/DailyBackup/360sefav_2020_08_28.favdb
@@ -0,0 +1,3 @@
+{
+ "note": "Plain text bookmarks backup, 360 SE 9."
+}
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/Bookmarks b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/Bookmarks
new file mode 100644
index 0000000000..6f47e5a55c
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Default4SE9Test/Bookmarks
@@ -0,0 +1,3 @@
+{
+ "note": "Plain text canonical bookmarks storage, 360 SE 9."
+}
diff --git a/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Local State b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Local State
new file mode 100644
index 0000000000..dd3fecce45
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Roaming/360se6/User Data/Local State
@@ -0,0 +1,12 @@
+{
+ "profile": {
+ "info_cache": {
+ "Default": {
+ "name": "用户1"
+ }
+ }
+ },
+ "sync_login_info": {
+ "filepath": "0f3ab103a522f4463ecacc36d34eb996"
+ }
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies
new file mode 100644
index 0000000000..83d855cb33
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/_locales/en_US/messages.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/_locales/en_US/messages.json
new file mode 100644
index 0000000000..44e855edbd
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/_locales/en_US/messages.json
@@ -0,0 +1,9 @@
+{
+ "description": {
+ "description": "Extension description in manifest. Should not exceed 132 characters.",
+ "message": "It is the description of fake app 1."
+ },
+ "name": {
+ "message": "Fake App 1"
+ }
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/manifest.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/manifest.json
new file mode 100644
index 0000000000..1550bf1c0e
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/manifest.json
@@ -0,0 +1,10 @@
+{
+ "app": {
+ "launch": {
+ "local_path": "main.html"
+ }
+ },
+ "default_locale": "en_US",
+ "description": "__MSG_description__",
+ "name": "__MSG_name__"
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/_locales/en_US/messages.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/_locales/en_US/messages.json
new file mode 100644
index 0000000000..11657460d8
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/_locales/en_US/messages.json
@@ -0,0 +1,9 @@
+{
+ "description": {
+ "description": "Extension description in manifest. Should not exceed 132 characters.",
+ "message": "It is the description of fake extension 1."
+ },
+ "name": {
+ "message": "Fake Extension 1"
+ }
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/manifest.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/manifest.json
new file mode 100644
index 0000000000..5ceced8031
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/manifest.json
@@ -0,0 +1,5 @@
+{
+ "default_locale": "en_US",
+ "description": "__MSG_description__",
+ "name": "__MSG_name__"
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-2/1.0_0/manifest.json b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-2/1.0_0/manifest.json
new file mode 100644
index 0000000000..983c37560c
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-2/1.0_0/manifest.json
@@ -0,0 +1,4 @@
+{
+ "default_locale": "en_US",
+ "name": "Fake Extension 2"
+}
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryCorrupt b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryCorrupt
new file mode 100644
index 0000000000..8585f308c5
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryCorrupt
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMaster b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMaster
new file mode 100644
index 0000000000..7fb19903b0
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMaster
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Login Data b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Login Data
new file mode 100644
index 0000000000..19b8542b98
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Login Data
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State
new file mode 100644
index 0000000000..01b99455e4
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State
@@ -0,0 +1,22 @@
+{
+ "profile" : {
+ "info_cache" : {
+ "Default" : {
+ "active_time" : 1430950755.65137,
+ "is_using_default_name" : true,
+ "is_ephemeral" : false,
+ "is_omitted_from_profile_list" : false,
+ "user_name" : "",
+ "background_apps" : false,
+ "is_using_default_avatar" : true,
+ "avatar_icon" : "chrome://theme/IDR_PROFILE_AVATAR_0",
+ "name" : "Person 1"
+ }
+ },
+ "profiles_created" : 1,
+ "last_used" : "Default",
+ "last_active_profiles" : [
+ "Default"
+ ]
+ }
+}
diff --git a/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist b/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist
new file mode 100644
index 0000000000..a9c33e1b1a
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db
new file mode 100644
index 0000000000..dd5d0c7512
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-lock b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-lock
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-lock
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-shm b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-shm
new file mode 100644
index 0000000000..edd607898b
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-shm
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-wal b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-wal
new file mode 100644
index 0000000000..e145119298
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons.db-wal
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/04860FA3D07D8936B87D2B965317C6E9 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/04860FA3D07D8936B87D2B965317C6E9
new file mode 100644
index 0000000000..1c6741c165
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/04860FA3D07D8936B87D2B965317C6E9
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/19A777E4F7BDA0C0E350D6C681B6E271 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/19A777E4F7BDA0C0E350D6C681B6E271
new file mode 100644
index 0000000000..47b40f707f
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/19A777E4F7BDA0C0E350D6C681B6E271
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/2558A57A1AE576AA31F0DCD1364B3F42 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/2558A57A1AE576AA31F0DCD1364B3F42
new file mode 100644
index 0000000000..2a4c30b31e
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/2558A57A1AE576AA31F0DCD1364B3F42
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/57D1907A1EBDA1889AA85B8AB7A90804 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/57D1907A1EBDA1889AA85B8AB7A90804
new file mode 100644
index 0000000000..f4996ba082
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/57D1907A1EBDA1889AA85B8AB7A90804
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/6EEDD53B65A19CB364EB6FB07DEACF80 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/6EEDD53B65A19CB364EB6FB07DEACF80
new file mode 100644
index 0000000000..f519ce9ad2
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/6EEDD53B65A19CB364EB6FB07DEACF80
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/7F65370AD319C7B294EDF2E2BEBA880F b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/7F65370AD319C7B294EDF2E2BEBA880F
new file mode 100644
index 0000000000..e70021849b
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/7F65370AD319C7B294EDF2E2BEBA880F
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/999E2BD5CD612AA550F222A1088DB3D8 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/999E2BD5CD612AA550F222A1088DB3D8
new file mode 100644
index 0000000000..559502b02b
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/999E2BD5CD612AA550F222A1088DB3D8
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/9D8A6E2153D42043A7AE0430B41D374A b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/9D8A6E2153D42043A7AE0430B41D374A
new file mode 100644
index 0000000000..89ed9a1c39
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/9D8A6E2153D42043A7AE0430B41D374A
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/A21F634481CF5188329FD2052F07ADBC b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/A21F634481CF5188329FD2052F07ADBC
new file mode 100644
index 0000000000..7b86185e67
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/A21F634481CF5188329FD2052F07ADBC
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/BC2288B5BA9B7BE352BA586257442E08 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/BC2288B5BA9B7BE352BA586257442E08
new file mode 100644
index 0000000000..a1d03856b5
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/BC2288B5BA9B7BE352BA586257442E08
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/CFFC3831D8E7201BF8B77728FC79B52B b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/CFFC3831D8E7201BF8B77728FC79B52B
new file mode 100644
index 0000000000..ba1145ca83
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/CFFC3831D8E7201BF8B77728FC79B52B
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/F3FA61DDA95B78A8B5F2C392C0382137 b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/F3FA61DDA95B78A8B5F2C392C0382137
new file mode 100644
index 0000000000..82339b3b1d
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Favicon Cache/favicons/F3FA61DDA95B78A8B5F2C392C0382137
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/HistoryStrangeEntries.db b/browser/components/migration/tests/unit/Library/Safari/HistoryStrangeEntries.db
new file mode 100644
index 0000000000..533daba3df
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/HistoryStrangeEntries.db
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Safari/HistoryTemplate.db b/browser/components/migration/tests/unit/Library/Safari/HistoryTemplate.db
new file mode 100644
index 0000000000..5a317c70e8
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/HistoryTemplate.db
Binary files differ
diff --git a/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Default/Login Data b/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Default/Login Data
new file mode 100644
index 0000000000..b2d425eb4a
--- /dev/null
+++ b/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Default/Login Data
Binary files differ
diff --git a/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Local State b/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Local State
new file mode 100644
index 0000000000..bb03d6b9a1
--- /dev/null
+++ b/browser/components/migration/tests/unit/LibraryWithNoData/Application Support/Google/Chrome/Local State
@@ -0,0 +1,22 @@
+{
+ "profile" : {
+ "info_cache" : {
+ "Default" : {
+ "active_time" : 1430950755.65137,
+ "is_using_default_name" : true,
+ "is_ephemeral" : false,
+ "is_omitted_from_profile_list" : false,
+ "user_name" : "",
+ "background_apps" : false,
+ "is_using_default_avatar" : true,
+ "avatar_icon" : "chrome://theme/IDR_PROFILE_AVATAR_0",
+ "name" : "Person With No Data"
+ }
+ },
+ "profiles_created" : 1,
+ "last_used" : "Default",
+ "last_active_profiles" : [
+ "Default"
+ ]
+ }
+}
diff --git a/browser/components/migration/tests/unit/bookmarks.exported.html b/browser/components/migration/tests/unit/bookmarks.exported.html
new file mode 100644
index 0000000000..5a9ec43325
--- /dev/null
+++ b/browser/components/migration/tests/unit/bookmarks.exported.html
@@ -0,0 +1,32 @@
+<!DOCTYPE NETSCAPE-Bookmark-file-1>
+<!-- This is an automatically generated file.
+ It will be read and overwritten.
+ DO NOT EDIT! -->
+<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
+<meta http-equiv="Content-Security-Policy"
+ content="default-src 'self'; script-src 'none'; img-src data: *; object-src 'none'"></meta>
+<TITLE>Bookmarks</TITLE>
+<H1>Bookmarks Menu</H1>
+
+<DL><p>
+ <DT><H3 ADD_DATE="1685118888" LAST_MODIFIED="1685118888">Mozilla Firefox</H3>
+ <DL><p>
+ <DT><A HREF="http://en-us.www.mozilla.com/en-US/firefox/help/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">Help and Tutorials</A>
+ <DT><A HREF="http://en-us.www.mozilla.com/en-US/firefox/customize/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/firefox/customize/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">Customize Firefox</A>
+ <DT><A HREF="http://en-us.www.mozilla.com/en-US/firefox/community/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/firefox/community/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">Get Involved</A>
+ <DT><A HREF="http://en-us.www.mozilla.com/en-US/about/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/about/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">About Us</A>
+ </DL><p>
+ <HR> <DT><H3 ADD_DATE="1177541020" LAST_MODIFIED="1177541050">test</H3>
+ <DL><p>
+ <DT><A HREF="http://test/post" ADD_DATE="1177375336" LAST_MODIFIED="1177375423" SHORTCUTURL="test" POST_DATA="hidden1%3Dbar&amp;text1%3D%25s" LAST_CHARSET="ISO-8859-1">test post keyword</A>
+ </DL><p>
+ <DT><H3 ADD_DATE="1685118888" LAST_MODIFIED="1685118888" PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Toolbar</H3>
+ <DL><p>
+ <DT><A HREF="http://en-us.www.mozilla.com/en-US/firefox/central/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888" ICON_URI="fake-favicon-uri:http://en-us.www.mozilla.com/en-US/firefox/central/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==">Getting Started</A>
+ <DT><A HREF="http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" ADD_DATE="1177541035" LAST_MODIFIED="1177541035">Latest Headlines</A>
+ </DL><p>
+ <DT><H3 ADD_DATE="1685118888" LAST_MODIFIED="1685118888" UNFILED_BOOKMARKS_FOLDER="true">Other Bookmarks</H3>
+ <DL><p>
+ <DT><A HREF="http://example.tld/" ADD_DATE="1685118888" LAST_MODIFIED="1685118888">Example.tld</A>
+ </DL><p>
+</DL>
diff --git a/browser/components/migration/tests/unit/bookmarks.exported.json b/browser/components/migration/tests/unit/bookmarks.exported.json
new file mode 100644
index 0000000000..2a73f00b31
--- /dev/null
+++ b/browser/components/migration/tests/unit/bookmarks.exported.json
@@ -0,0 +1,194 @@
+{
+ "guid": "root________",
+ "title": "",
+ "index": 0,
+ "dateAdded": 1685116351936000,
+ "lastModified": 1685372151518000,
+ "id": 1,
+ "typeCode": 2,
+ "type": "text/x-moz-place-container",
+ "root": "placesRoot",
+ "children": [
+ {
+ "guid": "menu________",
+ "title": "menu",
+ "index": 0,
+ "dateAdded": 1685116351936000,
+ "lastModified": 1685116352325000,
+ "id": 2,
+ "typeCode": 2,
+ "type": "text/x-moz-place-container",
+ "root": "bookmarksMenuFolder",
+ "children": [
+ {
+ "guid": "jCs_9YrgXKq7",
+ "title": "Firefox Nightly Resources",
+ "index": 0,
+ "dateAdded": 1685116352325000,
+ "lastModified": 1685116352325000,
+ "id": 7,
+ "typeCode": 2,
+ "type": "text/x-moz-place-container",
+ "children": [
+ {
+ "guid": "xwdRLsUWYFwm",
+ "title": "Firefox Nightly blog",
+ "index": 0,
+ "dateAdded": 1685116352325000,
+ "lastModified": 1685116352325000,
+ "id": 8,
+ "typeCode": 1,
+ "iconUri": "fake-favicon-uri:https://blog.nightly.mozilla.org/",
+ "type": "text/x-moz-place",
+ "uri": "https://blog.nightly.mozilla.org/"
+ },
+ {
+ "guid": "uhdiDrWjH0-n",
+ "title": "Mozilla Bug Tracker",
+ "index": 1,
+ "dateAdded": 1685116352325000,
+ "lastModified": 1685116352325000,
+ "id": 9,
+ "typeCode": 1,
+ "iconUri": "fake-favicon-uri:https://bugzilla.mozilla.org/",
+ "type": "text/x-moz-place",
+ "uri": "https://bugzilla.mozilla.org/",
+ "keyword": "bz",
+ "postData": null
+ },
+ {
+ "guid": "zOK7d-gjJ5Vy",
+ "title": "Mozilla Developer Network",
+ "index": 2,
+ "dateAdded": 1685116352325000,
+ "lastModified": 1685116352325000,
+ "id": 10,
+ "typeCode": 1,
+ "iconUri": "fake-favicon-uri:https://developer.mozilla.org/",
+ "type": "text/x-moz-place",
+ "uri": "https://developer.mozilla.org/",
+ "keyword": "mdn",
+ "postData": null
+ },
+ {
+ "guid": "7gcb4320A_y6",
+ "title": "Nightly Tester Tools",
+ "index": 3,
+ "dateAdded": 1685116352325000,
+ "lastModified": 1685116352325000,
+ "id": 11,
+ "typeCode": 1,
+ "iconUri": "fake-favicon-uri:https://addons.mozilla.org/firefox/addon/nightly-tester-tools/",
+ "type": "text/x-moz-place",
+ "uri": "https://addons.mozilla.org/firefox/addon/nightly-tester-tools/"
+ },
+ {
+ "guid": "c4753lDvJwNE",
+ "title": "All your crashes",
+ "index": 4,
+ "dateAdded": 1685116352325000,
+ "lastModified": 1685116352325000,
+ "id": 12,
+ "typeCode": 1,
+ "iconUri": "fake-favicon-uri:about:crashes",
+ "type": "text/x-moz-place",
+ "uri": "about:crashes"
+ },
+ {
+ "guid": "IyYGIH9VCs2t",
+ "title": "Planet Mozilla",
+ "index": 5,
+ "dateAdded": 1685116352325000,
+ "lastModified": 1685116352325000,
+ "id": 13,
+ "typeCode": 1,
+ "iconUri": "fake-favicon-uri:https://planet.mozilla.org/",
+ "type": "text/x-moz-place",
+ "uri": "https://planet.mozilla.org/"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "guid": "toolbar_____",
+ "title": "toolbar",
+ "index": 1,
+ "dateAdded": 1685116351936000,
+ "lastModified": 1685372151518000,
+ "id": 3,
+ "typeCode": 2,
+ "type": "text/x-moz-place-container",
+ "root": "toolbarFolder",
+ "children": [
+ {
+ "guid": "5jN1vdzOEnHx",
+ "title": "Get Involved",
+ "index": 0,
+ "dateAdded": 1685116352413000,
+ "lastModified": 1685116352413000,
+ "id": 14,
+ "typeCode": 1,
+ "iconUri": "fake-favicon-uri:https://www.mozilla.org/contribute/?utm_medium=firefox-desktop&utm_source=bookmarks-toolbar&utm_campaign=new-users-nightly&utm_content=-global",
+ "type": "text/x-moz-place",
+ "uri": "https://www.mozilla.org/contribute/?utm_medium=firefox-desktop&utm_source=bookmarks-toolbar&utm_campaign=new-users-nightly&utm_content=-global"
+ },
+ {
+ "guid": "5RsMT9sWsmIe",
+ "title": "Why More Psychiatrists Think Mindfulness Can Help Treat ADHD",
+ "index": 1,
+ "dateAdded": 1685372143048000,
+ "lastModified": 1685372143048000,
+ "id": 15,
+ "typeCode": 1,
+ "type": "text/x-moz-place",
+ "uri": "https://getpocket.com/explore/item/why-more-psychiatrists-think-mindfulness-can-help-treat-adhd?utm_source=pocket-newtab"
+ },
+ {
+ "guid": "ejoNUqAfEMQL",
+ "title": "Your New Favorite Weeknight Recipe Is Meat-Free (and Easy, Too)",
+ "index": 2,
+ "dateAdded": 1685372148200000,
+ "lastModified": 1685372148200000,
+ "id": 16,
+ "typeCode": 1,
+ "type": "text/x-moz-place",
+ "uri": "https://getpocket.com/collections/your-new-favorite-weeknight-recipe-is-meat-free-and-easy-too?utm_source=pocket-newtab"
+ },
+ {
+ "guid": "O5QCiQ1zrqHY",
+ "title": "8 Natural Ways to Repel Insects Without Bug Spray",
+ "index": 3,
+ "dateAdded": 1685372151518000,
+ "lastModified": 1685372151518000,
+ "id": 17,
+ "typeCode": 1,
+ "type": "text/x-moz-place",
+ "uri": "https://getpocket.com/explore/item/8-natural-ways-to-repel-insects-without-bug-spray?utm_source=pocket-newtab"
+ }
+ ]
+ },
+ {
+ "guid": "unfiled_____",
+ "title": "unfiled",
+ "index": 3,
+ "dateAdded": 1685116351936000,
+ "lastModified": 1685116352272000,
+ "id": 5,
+ "typeCode": 2,
+ "type": "text/x-moz-place-container",
+ "root": "unfiledBookmarksFolder"
+ },
+ {
+ "guid": "mobile______",
+ "title": "mobile",
+ "index": 4,
+ "dateAdded": 1685116351968000,
+ "lastModified": 1685116352272000,
+ "id": 6,
+ "typeCode": 2,
+ "type": "text/x-moz-place-container",
+ "root": "mobileFolder"
+ }
+ ]
+}
diff --git a/browser/components/migration/tests/unit/bookmarks.invalid.html b/browser/components/migration/tests/unit/bookmarks.invalid.html
new file mode 100644
index 0000000000..900ec52e1d
--- /dev/null
+++ b/browser/components/migration/tests/unit/bookmarks.invalid.html
@@ -0,0 +1 @@
+This shouldn't cause anything to be imported.
diff --git a/browser/components/migration/tests/unit/head_migration.js b/browser/components/migration/tests/unit/head_migration.js
new file mode 100644
index 0000000000..9900f34232
--- /dev/null
+++ b/browser/components/migration/tests/unit/head_migration.js
@@ -0,0 +1,260 @@
+"use strict";
+
+var { MigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/MigrationUtils.sys.mjs"
+);
+var { LoginHelper } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginHelper.sys.mjs"
+);
+var { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+var { PlacesUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesUtils.sys.mjs"
+);
+var { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+var { PlacesTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PlacesTestUtils.sys.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
+});
+
+// Initialize profile.
+var gProfD = do_get_profile();
+
+var { getAppInfo, newAppInfo, updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo();
+
+/**
+ * Migrates the requested resource and waits for the migration to be complete.
+ *
+ * @param {MigratorBase} migrator
+ * The migrator being used to migrate the data.
+ * @param {number} resourceType
+ * This is a bitfield with bits from nsIBrowserProfileMigrator flipped to indicate what
+ * resources should be migrated.
+ * @param {object|string|null} [aProfile=null]
+ * The profile to be migrated. If set to null, the default profile will be
+ * migrated.
+ * @param {boolean} succeeds
+ * True if this migration is expected to succeed.
+ * @returns {Promise<Array<string[]>>}
+ * An array of the results from each nsIObserver topics being observed to
+ * verify if the migration succeeded or failed. Those results are 2-element
+ * arrays of [subject, data].
+ */
+async function promiseMigration(
+ migrator,
+ resourceType,
+ aProfile = null,
+ succeeds = null
+) {
+ // Ensure resource migration is available.
+ let availableSources = await migrator.getMigrateData(aProfile);
+ Assert.ok(
+ (availableSources & resourceType) > 0,
+ "Resource supported by migrator"
+ );
+ let promises = [TestUtils.topicObserved("Migration:Ended")];
+
+ if (succeeds !== null) {
+ // Check that the specific resource type succeeded
+ promises.push(
+ TestUtils.topicObserved(
+ succeeds ? "Migration:ItemAfterMigrate" : "Migration:ItemError",
+ (_, data) => data == resourceType
+ )
+ );
+ }
+
+ // Start the migration.
+ migrator.migrate(resourceType, null, aProfile);
+
+ return Promise.all(promises);
+}
+/**
+ * Function that returns a favicon url for a given page url
+ *
+ * @param {string} uri
+ * The Bookmark URI
+ * @returns {string} faviconURI
+ * The Favicon URI
+ */
+async function getFaviconForPageURI(uri) {
+ let faviconURI = await new Promise(resolve => {
+ PlacesUtils.favicons.getFaviconDataForPage(uri, favURI => {
+ resolve(favURI);
+ });
+ });
+ return faviconURI;
+}
+
+/**
+ * Takes an array of page URIs and checks that the favicon was imported for each page URI
+ *
+ * @param {Array} pageURIs An array of page URIs
+ */
+async function assertFavicons(pageURIs) {
+ for (let uri of pageURIs) {
+ let faviconURI = await getFaviconForPageURI(uri);
+ Assert.ok(faviconURI, `Got favicon for ${uri.spec}`);
+ }
+}
+
+/**
+ * Replaces a directory service entry with a given nsIFile.
+ *
+ * @param {string} key
+ * The nsIDirectoryService directory key to register a fake path for.
+ * For example: "AppData", "ULibDir".
+ * @param {nsIFile} file
+ * The nsIFile to map the key to. Note that this nsIFile should represent
+ * a directory and not an individual file.
+ * @see nsDirectoryServiceDefs.h for the list of directories that can be
+ * overridden.
+ */
+function registerFakePath(key, file) {
+ let dirsvc = Services.dirsvc.QueryInterface(Ci.nsIProperties);
+ let originalFile;
+ try {
+ // If a file is already provided save it and undefine, otherwise set will
+ // throw for persistent entries (ones that are cached).
+ originalFile = dirsvc.get(key, Ci.nsIFile);
+ dirsvc.undefine(key);
+ } catch (e) {
+ // dirsvc.get will throw if nothing provides for the key and dirsvc.undefine
+ // will throw if it's not a persistent entry, in either case we don't want
+ // to set the original file in cleanup.
+ originalFile = undefined;
+ }
+
+ dirsvc.set(key, file);
+ registerCleanupFunction(() => {
+ dirsvc.undefine(key);
+ if (originalFile) {
+ dirsvc.set(key, originalFile);
+ }
+ });
+}
+
+function getRootPath() {
+ let dirKey;
+ if (AppConstants.platform == "win") {
+ dirKey = "LocalAppData";
+ } else if (AppConstants.platform == "macosx") {
+ dirKey = "ULibDir";
+ } else {
+ dirKey = "Home";
+ }
+ return Services.dirsvc.get(dirKey, Ci.nsIFile).path;
+}
+
+/**
+ * Returns a PRTime value for the current date minus daysAgo number
+ * of days.
+ *
+ * @param {number} daysAgo
+ * How many days in the past from now the returned date should be.
+ * @returns {number}
+ */
+function PRTimeDaysAgo(daysAgo) {
+ return PlacesUtils.toPRTime(Date.now() - daysAgo * 24 * 60 * 60 * 1000);
+}
+
+/**
+ * Returns a Date value for the current date minus daysAgo number
+ * of days.
+ *
+ * @param {number} daysAgo
+ * How many days in the past from now the returned date should be.
+ * @returns {Date}
+ */
+function dateDaysAgo(daysAgo) {
+ return new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000);
+}
+
+/**
+ * Constructs and returns a data structure consistent with the Chrome
+ * browsers bookmarks storage. This data structure can then be serialized
+ * to JSON and written to disk to simulate a Chrome browser's bookmarks
+ * database.
+ *
+ * @param {number} [totalBookmarks=100]
+ * How many bookmarks to create.
+ * @returns {object}
+ */
+function createChromeBookmarkStructure(totalBookmarks = 100) {
+ let bookmarksData = {
+ roots: {
+ bookmark_bar: { children: [] },
+ other: { children: [] },
+ synced: { children: [] },
+ },
+ };
+ const MAX_BMS = totalBookmarks;
+ let barKids = bookmarksData.roots.bookmark_bar.children;
+ let menuKids = bookmarksData.roots.other.children;
+ let syncedKids = bookmarksData.roots.synced.children;
+ let currentMenuKids = menuKids;
+ let currentBarKids = barKids;
+ let currentSyncedKids = syncedKids;
+ for (let i = 0; i < MAX_BMS; i++) {
+ currentBarKids.push({
+ url: "https://www.chrome-bookmark-bar-bookmark" + i + ".com",
+ name: "bookmark " + i,
+ type: "url",
+ });
+ currentMenuKids.push({
+ url: "https://www.chrome-menu-bookmark" + i + ".com",
+ name: "bookmark for menu " + i,
+ type: "url",
+ });
+ currentSyncedKids.push({
+ url: "https://www.chrome-synced-bookmark" + i + ".com",
+ name: "bookmark for synced " + i,
+ type: "url",
+ });
+ if (i % 20 == 19) {
+ let nextFolder = {
+ name: "toolbar folder " + Math.ceil(i / 20),
+ type: "folder",
+ children: [],
+ };
+ currentBarKids.push(nextFolder);
+ currentBarKids = nextFolder.children;
+
+ nextFolder = {
+ name: "menu folder " + Math.ceil(i / 20),
+ type: "folder",
+ children: [],
+ };
+ currentMenuKids.push(nextFolder);
+ currentMenuKids = nextFolder.children;
+
+ nextFolder = {
+ name: "synced folder " + Math.ceil(i / 20),
+ type: "folder",
+ children: [],
+ };
+ currentSyncedKids.push(nextFolder);
+ currentSyncedKids = nextFolder.children;
+ }
+ }
+ return bookmarksData;
+}
diff --git a/browser/components/migration/tests/unit/insertIEHistory/InsertIEHistory.cpp b/browser/components/migration/tests/unit/insertIEHistory/InsertIEHistory.cpp
new file mode 100644
index 0000000000..dea79a9289
--- /dev/null
+++ b/browser/components/migration/tests/unit/insertIEHistory/InsertIEHistory.cpp
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Insert URLs into Internet Explorer (IE) history so we can test importing
+ * them.
+ *
+ * See API docs at
+ * https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/ms774949(v%3dvs.85)
+ */
+
+#include <urlhist.h> // IUrlHistoryStg
+#include <shlguid.h> // SID_SUrlHistory
+
+int main(int argc, char** argv) {
+ HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
+ if (FAILED(hr)) {
+ CoUninitialize();
+ return -1;
+ }
+ IUrlHistoryStg* ieHist;
+
+ hr =
+ ::CoCreateInstance(CLSID_CUrlHistory, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IUrlHistoryStg, reinterpret_cast<void**>(&ieHist));
+ if (FAILED(hr)) return -2;
+
+ hr = ieHist->AddUrl(L"http://www.mozilla.org/1", L"Mozilla HTTP Test", 0);
+ if (FAILED(hr)) return -3;
+
+ hr =
+ ieHist->AddUrl(L"https://www.mozilla.org/2", L"Mozilla HTTPS Test 🦊", 0);
+ if (FAILED(hr)) return -4;
+
+ CoUninitialize();
+
+ return 0;
+}
diff --git a/browser/components/migration/tests/unit/insertIEHistory/moz.build b/browser/components/migration/tests/unit/insertIEHistory/moz.build
new file mode 100644
index 0000000000..61ca96d48a
--- /dev/null
+++ b/browser/components/migration/tests/unit/insertIEHistory/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+FINAL_TARGET = "_tests/xpcshell/browser/components/migration/tests/unit"
+
+Program("InsertIEHistory")
+OS_LIBS += [
+ "ole32",
+ "uuid",
+]
+SOURCES += [
+ "InsertIEHistory.cpp",
+]
+
+NO_PGO = True
+DisableStlWrapping()
diff --git a/browser/components/migration/tests/unit/test_360seMigrationUtils.js b/browser/components/migration/tests/unit/test_360seMigrationUtils.js
new file mode 100644
index 0000000000..3a882b516d
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_360seMigrationUtils.js
@@ -0,0 +1,164 @@
+"use strict";
+
+const { Qihoo360seMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/360seMigrationUtils.sys.mjs"
+);
+
+const parentPath = do_get_file("AppData/Roaming/360se6/User Data").path;
+const loggedInPath = "0f3ab103a522f4463ecacc36d34eb996";
+const loggedInBackup = PathUtils.join(
+ parentPath,
+ "Default",
+ loggedInPath,
+ "DailyBackup",
+ "360default_ori_2021_12_02.favdb"
+);
+const loggedOutBackup = PathUtils.join(
+ parentPath,
+ "Default",
+ "DailyBackup",
+ "360default_ori_2021_12_02.favdb"
+);
+
+function getSqlitePath(profileId) {
+ return PathUtils.join(parentPath, profileId, loggedInPath, "360sefav.dat");
+}
+
+add_task(async function test_360se10_logged_in() {
+ const sqlitePath = getSqlitePath("Default");
+ await IOUtils.setModificationTime(sqlitePath);
+ await IOUtils.copy(
+ PathUtils.join(parentPath, "Default", "360Bookmarks"),
+ PathUtils.join(parentPath, "Default", loggedInPath)
+ );
+ await IOUtils.copy(loggedOutBackup, loggedInBackup);
+
+ const alternativeBookmarks =
+ await Qihoo360seMigrationUtils.getAlternativeBookmarks({
+ bookmarksPath: PathUtils.join(parentPath, "Default", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: loggedInPath,
+ },
+ },
+ });
+ Assert.ok(
+ alternativeBookmarks.resource && alternativeBookmarks.resource.exists,
+ "Should return the legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ undefined,
+ "Should not return any path to plain text bookmarks."
+ );
+});
+
+add_task(async function test_360se10_logged_in_outdated_sqlite() {
+ const sqlitePath = getSqlitePath("Default");
+ await IOUtils.setModificationTime(
+ sqlitePath,
+ new Date("2020-08-18").valueOf()
+ );
+
+ const alternativeBookmarks =
+ await Qihoo360seMigrationUtils.getAlternativeBookmarks({
+ bookmarksPath: PathUtils.join(parentPath, "Default", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: loggedInPath,
+ },
+ },
+ });
+ Assert.strictEqual(
+ alternativeBookmarks.resource,
+ undefined,
+ "Should not return the outdated legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ loggedInBackup,
+ "Should return path to the most recent plain text bookmarks backup."
+ );
+
+ await IOUtils.setModificationTime(sqlitePath);
+});
+
+add_task(async function test_360se10_logged_out() {
+ const alternativeBookmarks =
+ await Qihoo360seMigrationUtils.getAlternativeBookmarks({
+ bookmarksPath: PathUtils.join(parentPath, "Default", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: "",
+ },
+ },
+ });
+ Assert.strictEqual(
+ alternativeBookmarks.resource,
+ undefined,
+ "Should not return the legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ loggedOutBackup,
+ "Should return path to the most recent plain text bookmarks backup."
+ );
+});
+
+add_task(async function test_360se9_logged_in_outdated_sqlite() {
+ const sqlitePath = getSqlitePath("Default4SE9Test");
+ await IOUtils.setModificationTime(
+ sqlitePath,
+ new Date("2020-08-18").valueOf()
+ );
+
+ const alternativeBookmarks =
+ await Qihoo360seMigrationUtils.getAlternativeBookmarks({
+ bookmarksPath: PathUtils.join(parentPath, "Default4SE9Test", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: loggedInPath,
+ },
+ },
+ });
+ Assert.strictEqual(
+ alternativeBookmarks.resource,
+ undefined,
+ "Should not return the legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ PathUtils.join(
+ parentPath,
+ "Default4SE9Test",
+ loggedInPath,
+ "DailyBackup",
+ "360sefav_2020_08_28.favdb"
+ ),
+ "Should return path to the most recent plain text bookmarks backup."
+ );
+
+ await IOUtils.setModificationTime(sqlitePath);
+});
+
+add_task(async function test_360se9_logged_out() {
+ const alternativeBookmarks =
+ await Qihoo360seMigrationUtils.getAlternativeBookmarks({
+ bookmarksPath: PathUtils.join(parentPath, "Default4SE9Test", "Bookmarks"),
+ localState: {
+ sync_login_info: {
+ filepath: "",
+ },
+ },
+ });
+ Assert.strictEqual(
+ alternativeBookmarks.resource,
+ undefined,
+ "Should not return the legacy bookmark resource."
+ );
+ Assert.strictEqual(
+ alternativeBookmarks.path,
+ PathUtils.join(parentPath, "Default4SE9Test", "Bookmarks"),
+ "Should return path to the plain text canonical bookmarks."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_360se_bookmarks.js b/browser/components/migration/tests/unit/test_360se_bookmarks.js
new file mode 100644
index 0000000000..e4f42d1880
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_360se_bookmarks.js
@@ -0,0 +1,62 @@
+"use strict";
+
+const { CustomizableUI } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizableUI.sys.mjs"
+);
+
+add_task(async function () {
+ registerFakePath("AppData", do_get_file("AppData/Roaming/"));
+
+ let migrator = await MigrationUtils.getMigrator("chromium-360se");
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable());
+
+ let importedToBookmarksToolbar = false;
+ let itemsSeen = { bookmarks: 0, folders: 0 };
+
+ let listener = events => {
+ for (let event of events) {
+ itemsSeen[
+ event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER
+ ? "folders"
+ : "bookmarks"
+ ]++;
+ if (event.parentGuid == PlacesUtils.bookmarks.toolbarGuid) {
+ importedToBookmarksToolbar = true;
+ }
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+ let observerNotified = false;
+ Services.obs.addObserver((aSubject, aTopic, aData) => {
+ let [toolbar, visibility] = JSON.parse(aData);
+ Assert.equal(
+ toolbar,
+ CustomizableUI.AREA_BOOKMARKS,
+ "Notification should be received for bookmarks toolbar"
+ );
+ Assert.equal(
+ visibility,
+ "true",
+ "Notification should say to reveal the bookmarks toolbar"
+ );
+ observerNotified = true;
+ }, "browser-set-toolbar-visibility");
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS, {
+ id: "Default",
+ });
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+
+ // Check the bookmarks have been imported to all the expected parents.
+ Assert.ok(importedToBookmarksToolbar, "Bookmarks imported in the toolbar");
+ Assert.equal(itemsSeen.bookmarks, 8, "Should import all bookmarks.");
+ Assert.equal(itemsSeen.folders, 2, "Should import all folders.");
+ // Check that the telemetry matches:
+ Assert.equal(
+ MigrationUtils._importQuantities.bookmarks,
+ itemsSeen.bookmarks + itemsSeen.folders,
+ "Telemetry reporting correct."
+ );
+ Assert.ok(observerNotified, "The observer should be notified upon migration");
+});
diff --git a/browser/components/migration/tests/unit/test_BookmarksFileMigrator.js b/browser/components/migration/tests/unit/test_BookmarksFileMigrator.js
new file mode 100644
index 0000000000..ec10097e76
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_BookmarksFileMigrator.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { BookmarksFileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/FileMigrators.sys.mjs"
+);
+
+const { MigrationWizardConstants } = ChromeUtils.importESModule(
+ "chrome://browser/content/migration/migration-wizard-constants.mjs"
+);
+
+/**
+ * Tests that the BookmarksFileMigrator properly subclasses FileMigratorBase
+ * and delegates to BookmarkHTMLUtils or BookmarkJSONUtils.
+ *
+ * Normally, we'd override the BookmarkHTMLUtils and BookmarkJSONUtils methods
+ * in our test here so that we just ensure that they're called with the
+ * right arguments, rather than testing their behaviour. Unfortunately, both
+ * BookmarkHTMLUtils and BookmarkJSONUtils are frozen with Object.freeze, which
+ * prevents sinon from stubbing out any of their methods. Rather than unfreezing
+ * those objects just for testing, we test the whole flow end-to-end, including
+ * the import to Places.
+ */
+
+add_setup(() => {
+ Services.prefs.setBoolPref("browser.migrate.bookmarks-file.enabled", true);
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.prefs.clearUserPref("browser.migrate.bookmarks-file.enabled");
+ });
+});
+
+/**
+ * First check that the BookmarksFileMigrator implements the required parts
+ * of the parent class.
+ */
+add_task(async function test_BookmarksFileMigrator_members() {
+ let migrator = new BookmarksFileMigrator();
+ Assert.ok(
+ migrator.constructor.key,
+ `BookmarksFileMigrator implements static getter 'key'`
+ );
+
+ Assert.ok(
+ migrator.constructor.displayNameL10nID,
+ `BookmarksFileMigrator implements static getter 'displayNameL10nID'`
+ );
+
+ Assert.ok(
+ migrator.constructor.brandImage,
+ `BookmarksFileMigrator implements static getter 'brandImage'`
+ );
+
+ Assert.ok(
+ migrator.progressHeaderL10nID,
+ `BookmarksFileMigrator implements getter 'progressHeaderL10nID'`
+ );
+
+ Assert.ok(
+ migrator.successHeaderL10nID,
+ `BookmarksFileMigrator implements getter 'successHeaderL10nID'`
+ );
+
+ Assert.ok(
+ await migrator.getFilePickerConfig(),
+ `BookmarksFileMigrator implements method 'getFilePickerConfig'`
+ );
+
+ Assert.ok(
+ migrator.displayedResourceTypes,
+ `BookmarksFileMigrator implements getter 'displayedResourceTypes'`
+ );
+
+ Assert.ok(migrator.enabled, `BookmarksFileMigrator is enabled`);
+});
+
+add_task(async function test_BookmarksFileMigrator_HTML() {
+ let migrator = new BookmarksFileMigrator();
+ const EXPECTED_SUCCESS_STATE = {
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES
+ .BOOKMARKS_FROM_FILE]: "8 bookmarks",
+ };
+
+ const BOOKMARKS_PATH = PathUtils.join(
+ do_get_cwd().path,
+ "bookmarks.exported.html"
+ );
+
+ let result = await migrator.migrate(BOOKMARKS_PATH);
+
+ Assert.deepEqual(
+ result,
+ EXPECTED_SUCCESS_STATE,
+ "Returned the expected success state."
+ );
+});
+
+add_task(async function test_BookmarksFileMigrator_JSON() {
+ let migrator = new BookmarksFileMigrator();
+
+ const EXPECTED_SUCCESS_STATE = {
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES
+ .BOOKMARKS_FROM_FILE]: "10 bookmarks",
+ };
+
+ const BOOKMARKS_PATH = PathUtils.join(
+ do_get_cwd().path,
+ "bookmarks.exported.json"
+ );
+
+ let result = await migrator.migrate(BOOKMARKS_PATH);
+
+ Assert.deepEqual(
+ result,
+ EXPECTED_SUCCESS_STATE,
+ "Returned the expected success state."
+ );
+});
+
+add_task(async function test_BookmarksFileMigrator_invalid() {
+ let migrator = new BookmarksFileMigrator();
+
+ const INVALID_FILE_PATH = PathUtils.join(
+ do_get_cwd().path,
+ "bookmarks.invalid.html"
+ );
+
+ await Assert.rejects(
+ migrator.migrate(INVALID_FILE_PATH),
+ /Pick another file/
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js b/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js
new file mode 100644
index 0000000000..32a8d1b4bc
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js
@@ -0,0 +1,87 @@
+"use strict";
+
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+
+// Setup chrome user data path for all platforms.
+ChromeMigrationUtils.getDataPath = () => {
+ return Promise.resolve(
+ do_get_file("Library/Application Support/Google/Chrome/").path
+ );
+};
+
+add_task(async function test_getExtensionList_function() {
+ let extensionList = await ChromeMigrationUtils.getExtensionList("Default");
+ Assert.equal(
+ extensionList.length,
+ 2,
+ "There should be 2 extensions installed."
+ );
+ Assert.deepEqual(
+ extensionList.find(extension => extension.id == "fake-extension-1"),
+ {
+ id: "fake-extension-1",
+ name: "Fake Extension 1",
+ description: "It is the description of fake extension 1.",
+ },
+ "First extension should match expectations."
+ );
+ Assert.deepEqual(
+ extensionList.find(extension => extension.id == "fake-extension-2"),
+ {
+ id: "fake-extension-2",
+ name: "Fake Extension 2",
+ // There is no description in the `manifest.json` file of this extension.
+ description: null,
+ },
+ "Second extension should match expectations."
+ );
+});
+
+add_task(async function test_getExtensionInformation_function() {
+ let extension = await ChromeMigrationUtils.getExtensionInformation(
+ "fake-extension-1",
+ "Default"
+ );
+ Assert.deepEqual(
+ extension,
+ {
+ id: "fake-extension-1",
+ name: "Fake Extension 1",
+ description: "It is the description of fake extension 1.",
+ },
+ "Should get the extension information correctly."
+ );
+});
+
+add_task(async function test_getLocaleString_function() {
+ let name = await ChromeMigrationUtils._getLocaleString(
+ "__MSG_name__",
+ "en_US",
+ "fake-extension-1",
+ "Default"
+ );
+ Assert.deepEqual(
+ name,
+ "Fake Extension 1",
+ "The value of __MSG_name__ locale key is Fake Extension 1."
+ );
+});
+
+add_task(async function test_isExtensionInstalled_function() {
+ let isInstalled = await ChromeMigrationUtils.isExtensionInstalled(
+ "fake-extension-1",
+ "Default"
+ );
+ Assert.ok(isInstalled, "The fake-extension-1 extension should be installed.");
+});
+
+add_task(async function test_getLastUsedProfileId_function() {
+ let profileId = await ChromeMigrationUtils.getLastUsedProfileId();
+ Assert.equal(
+ profileId,
+ "Default",
+ "The last used profile ID should be Default."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js
new file mode 100644
index 0000000000..ca75595ea9
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js
@@ -0,0 +1,141 @@
+"use strict";
+
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+
+const SUB_DIRECTORIES = {
+ win: {
+ Chrome: ["Google", "Chrome", "User Data"],
+ Chromium: ["Chromium", "User Data"],
+ Canary: ["Google", "Chrome SxS", "User Data"],
+ },
+ macosx: {
+ Chrome: ["Application Support", "Google", "Chrome"],
+ Chromium: ["Application Support", "Chromium"],
+ Canary: ["Application Support", "Google", "Chrome Canary"],
+ },
+ linux: {
+ Chrome: [".config", "google-chrome"],
+ Chromium: [".config", "chromium"],
+ Canary: [],
+ },
+};
+
+add_task(async function setup_fakePaths() {
+ let pathId;
+ if (AppConstants.platform == "macosx") {
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ pathId = "LocalAppData";
+ } else {
+ pathId = "Home";
+ }
+
+ registerFakePath(pathId, do_get_file("chromefiles/", true));
+});
+
+add_task(async function test_getDataPath_function() {
+ let projects = ["Chrome", "Chromium", "Canary"];
+ let rootPath = getRootPath();
+
+ for (let project of projects) {
+ let subfolders = SUB_DIRECTORIES[AppConstants.platform][project];
+
+ await IOUtils.makeDirectory(PathUtils.join(rootPath, ...subfolders), {
+ createAncestor: true,
+ ignoreExisting: true,
+ });
+ }
+
+ let chromeUserDataPath = await ChromeMigrationUtils.getDataPath("Chrome");
+ let chromiumUserDataPath = await ChromeMigrationUtils.getDataPath("Chromium");
+ let canaryUserDataPath = await ChromeMigrationUtils.getDataPath("Canary");
+ if (AppConstants.platform == "win") {
+ Assert.equal(
+ chromeUserDataPath,
+ PathUtils.join(getRootPath(), "Google", "Chrome", "User Data"),
+ "Should get the path of Chrome data directory."
+ );
+ Assert.equal(
+ chromiumUserDataPath,
+ PathUtils.join(getRootPath(), "Chromium", "User Data"),
+ "Should get the path of Chromium data directory."
+ );
+ Assert.equal(
+ canaryUserDataPath,
+ PathUtils.join(getRootPath(), "Google", "Chrome SxS", "User Data"),
+ "Should get the path of Canary data directory."
+ );
+ } else if (AppConstants.platform == "macosx") {
+ Assert.equal(
+ chromeUserDataPath,
+ PathUtils.join(getRootPath(), "Application Support", "Google", "Chrome"),
+ "Should get the path of Chrome data directory."
+ );
+ Assert.equal(
+ chromiumUserDataPath,
+ PathUtils.join(getRootPath(), "Application Support", "Chromium"),
+ "Should get the path of Chromium data directory."
+ );
+ Assert.equal(
+ canaryUserDataPath,
+ PathUtils.join(
+ getRootPath(),
+ "Application Support",
+ "Google",
+ "Chrome Canary"
+ ),
+ "Should get the path of Canary data directory."
+ );
+ } else {
+ Assert.equal(
+ chromeUserDataPath,
+ PathUtils.join(getRootPath(), ".config", "google-chrome"),
+ "Should get the path of Chrome data directory."
+ );
+ Assert.equal(
+ chromiumUserDataPath,
+ PathUtils.join(getRootPath(), ".config", "chromium"),
+ "Should get the path of Chromium data directory."
+ );
+ Assert.equal(canaryUserDataPath, null, "Should get null for Canary.");
+ }
+});
+
+add_task(async function test_getExtensionPath_function() {
+ let extensionPath = await ChromeMigrationUtils.getExtensionPath("Default");
+ let expectedPath;
+ if (AppConstants.platform == "win") {
+ expectedPath = PathUtils.join(
+ getRootPath(),
+ "Google",
+ "Chrome",
+ "User Data",
+ "Default",
+ "Extensions"
+ );
+ } else if (AppConstants.platform == "macosx") {
+ expectedPath = PathUtils.join(
+ getRootPath(),
+ "Application Support",
+ "Google",
+ "Chrome",
+ "Default",
+ "Extensions"
+ );
+ } else {
+ expectedPath = PathUtils.join(
+ getRootPath(),
+ ".config",
+ "google-chrome",
+ "Default",
+ "Extensions"
+ );
+ }
+ Assert.equal(
+ extensionPath,
+ expectedPath,
+ "Should get the path of extensions directory."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path_chromium_snap.js b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path_chromium_snap.js
new file mode 100644
index 0000000000..5011991536
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path_chromium_snap.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+
+const SUB_DIRECTORIES = {
+ linux: {
+ Chromium: [".config", "chromium"],
+ SnapChromium: ["snap", "chromium", "common", "chromium"],
+ },
+};
+
+add_task(async function setup_fakePaths() {
+ let pathId;
+ if (AppConstants.platform == "macosx") {
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ pathId = "LocalAppData";
+ } else {
+ pathId = "Home";
+ }
+
+ registerFakePath(pathId, do_get_file("chromefiles/", true));
+});
+
+add_task(async function test_getDataPath_function() {
+ let rootPath = getRootPath();
+ let chromiumSubFolders = SUB_DIRECTORIES[AppConstants.platform].Chromium;
+ // must remove normal chromium path
+ await IOUtils.remove(PathUtils.join(rootPath, ...chromiumSubFolders), {
+ ignoreAbsent: true,
+ });
+
+ let snapChromiumSubFolders =
+ SUB_DIRECTORIES[AppConstants.platform].SnapChromium;
+ // must create snap chromium path
+ await IOUtils.makeDirectory(
+ PathUtils.join(rootPath, ...snapChromiumSubFolders),
+ {
+ createAncestor: true,
+ ignoreExisting: true,
+ }
+ );
+
+ let chromiumUserDataPath = await ChromeMigrationUtils.getDataPath("Chromium");
+ Assert.equal(
+ chromiumUserDataPath,
+ PathUtils.join(getRootPath(), ...snapChromiumSubFolders),
+ "Should get the path of Snap Chromium data directory."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_bookmarks.js b/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
new file mode 100644
index 0000000000..d115cda412
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
@@ -0,0 +1,205 @@
+"use strict";
+
+const { CustomizableUI } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizableUI.sys.mjs"
+);
+
+const { PlacesUIUtils } = ChromeUtils.importESModule(
+ "resource:///modules/PlacesUIUtils.sys.mjs"
+);
+
+let rootDir = do_get_file("chromefiles/", true);
+
+add_task(async function setup_fakePaths() {
+ let pathId;
+ if (AppConstants.platform == "macosx") {
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ pathId = "LocalAppData";
+ } else {
+ pathId = "Home";
+ }
+ registerFakePath(pathId, rootDir);
+});
+
+add_task(async function setup_initialBookmarks() {
+ let bookmarks = [];
+ for (let i = 0; i < PlacesUIUtils.NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE + 1; i++) {
+ bookmarks.push({ url: "https://example.com/" + i, title: "" + i });
+ }
+
+ // Ensure we have enough items in both the menu and toolbar to trip creating a "from" folder.
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: bookmarks,
+ });
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: bookmarks,
+ });
+});
+
+async function testBookmarks(migratorKey, subDirs) {
+ if (AppConstants.platform == "macosx") {
+ subDirs.unshift("Application Support");
+ } else if (AppConstants.platform == "win") {
+ subDirs.push("User Data");
+ } else {
+ subDirs.unshift(".config");
+ }
+
+ let target = rootDir.clone();
+
+ // Pretend this is the default profile
+ while (subDirs.length) {
+ target.append(subDirs.shift());
+ }
+
+ let localStatePath = PathUtils.join(target.path, "Local State");
+ await IOUtils.writeJSON(localStatePath, []);
+
+ target.append("Default");
+
+ await IOUtils.makeDirectory(target.path, {
+ createAncestor: true,
+ ignoreExisting: true,
+ });
+
+ // Copy Favicons database into Default profile
+ const sourcePath = do_get_file(
+ "AppData/Local/Google/Chrome/User Data/Default/Favicons"
+ ).path;
+ await IOUtils.copy(sourcePath, target.path);
+
+ // Get page url for each favicon
+ let faviconURIs = await MigrationUtils.getRowsFromDBWithoutLocks(
+ sourcePath,
+ "Chrome Bookmark Favicons",
+ `select page_url from icon_mapping`
+ );
+
+ target.append("Bookmarks");
+ await IOUtils.remove(target.path, { ignoreAbsent: true });
+
+ let bookmarksData = createChromeBookmarkStructure();
+ await IOUtils.writeJSON(target.path, bookmarksData);
+
+ let migrator = await MigrationUtils.getMigrator(migratorKey);
+ Assert.ok(await migrator.hasPermissions(), "Has permissions");
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable());
+
+ let itemsSeen = { bookmarks: 0, folders: 0 };
+ let listener = events => {
+ for (let event of events) {
+ itemsSeen[
+ event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER
+ ? "folders"
+ : "bookmarks"
+ ]++;
+ }
+ };
+
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+ const PROFILE = {
+ id: "Default",
+ name: "Default",
+ };
+ let observerNotified = false;
+ Services.obs.addObserver((aSubject, aTopic, aData) => {
+ let [toolbar, visibility] = JSON.parse(aData);
+ Assert.equal(
+ toolbar,
+ CustomizableUI.AREA_BOOKMARKS,
+ "Notification should be received for bookmarks toolbar"
+ );
+ Assert.equal(
+ visibility,
+ "true",
+ "Notification should say to reveal the bookmarks toolbar"
+ );
+ observerNotified = true;
+ }, "browser-set-toolbar-visibility");
+ const initialToolbarCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+ const initialUnfiledCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.unfiledGuid
+ );
+ const initialmenuCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.menuGuid
+ );
+
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.BOOKMARKS,
+ PROFILE
+ );
+ const postToolbarCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+ const postUnfiledCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.unfiledGuid
+ );
+ const postmenuCount = await getFolderItemCount(
+ PlacesUtils.bookmarks.menuGuid
+ );
+
+ Assert.equal(
+ postUnfiledCount - initialUnfiledCount,
+ 210,
+ "Should have seen 210 items in unsorted bookmarks"
+ );
+ Assert.equal(
+ postToolbarCount - initialToolbarCount,
+ 105,
+ "Should have seen 105 items in toolbar"
+ );
+ Assert.equal(
+ postmenuCount - initialmenuCount,
+ 0,
+ "Should have seen 0 items in menu toolbar"
+ );
+
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+
+ Assert.equal(itemsSeen.bookmarks, 300, "Should have seen 300 bookmarks.");
+ Assert.equal(itemsSeen.folders, 15, "Should have seen 15 folders.");
+ Assert.equal(
+ MigrationUtils._importQuantities.bookmarks,
+ itemsSeen.bookmarks + itemsSeen.folders,
+ "Telemetry reporting correct."
+ );
+ Assert.ok(observerNotified, "The observer should be notified upon migration");
+ let pageUrls = Array.from(faviconURIs, f =>
+ Services.io.newURI(f.getResultByName("page_url"))
+ );
+ await assertFavicons(pageUrls);
+}
+
+add_task(async function test_Chrome() {
+ // Expire all favicons before the test to make sure favicons are imported
+ PlacesUtils.favicons.expireAllFavicons();
+ let subDirs =
+ AppConstants.platform == "linux" ? ["google-chrome"] : ["Google", "Chrome"];
+ await testBookmarks("chrome", subDirs);
+});
+
+add_task(async function test_ChromiumEdge() {
+ PlacesUtils.favicons.expireAllFavicons();
+ if (AppConstants.platform == "linux") {
+ // Edge isn't available on Linux.
+ return;
+ }
+ let subDirs =
+ AppConstants.platform == "macosx"
+ ? ["Microsoft Edge"]
+ : ["Microsoft", "Edge"];
+ await testBookmarks("chromium-edge", subDirs);
+});
+
+async function getFolderItemCount(guid) {
+ let results = await PlacesUtils.promiseBookmarksTree(guid);
+
+ return results.itemsCount;
+}
diff --git a/browser/components/migration/tests/unit/test_Chrome_corrupt_history.js b/browser/components/migration/tests/unit/test_Chrome_corrupt_history.js
new file mode 100644
index 0000000000..31541f9fdb
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_corrupt_history.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let rootDir = do_get_file("chromefiles/", true);
+
+const SOURCE_PROFILE_DIR = "Library/Application Support/Google/Chrome/Default/";
+const PROFILE = {
+ id: "Default",
+ name: "Person 1",
+};
+
+add_setup(async function setup_fake_paths() {
+ let pathId;
+ if (AppConstants.platform == "macosx") {
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ pathId = "LocalAppData";
+ } else {
+ pathId = "Home";
+ }
+ registerFakePath(pathId, rootDir);
+
+ let file = do_get_file(`${SOURCE_PROFILE_DIR}HistoryCorrupt`);
+ file.copyTo(file.parent, "History");
+
+ registerCleanupFunction(() => {
+ let historyFile = do_get_file(`${SOURCE_PROFILE_DIR}History`, true);
+ try {
+ historyFile.remove(false);
+ } catch (ex) {
+ // It is ok if this doesn't exist.
+ if (ex.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
+ throw ex;
+ }
+ }
+ });
+
+ let subDirs =
+ AppConstants.platform == "linux" ? ["google-chrome"] : ["Google", "Chrome"];
+
+ if (AppConstants.platform == "macosx") {
+ subDirs.unshift("Application Support");
+ } else if (AppConstants.platform == "win") {
+ subDirs.push("User Data");
+ } else {
+ subDirs.unshift(".config");
+ }
+
+ let target = rootDir.clone();
+ // Pretend this is the default profile
+ subDirs.push("Default");
+ while (subDirs.length) {
+ target.append(subDirs.shift());
+ }
+
+ await IOUtils.makeDirectory(target.path, {
+ createAncestor: true,
+ ignoreExisting: true,
+ });
+
+ target.append("Bookmarks");
+ await IOUtils.remove(target.path, { ignoreAbsent: true });
+
+ let bookmarksData = createChromeBookmarkStructure();
+ await IOUtils.writeJSON(target.path, bookmarksData);
+});
+
+add_task(async function test_corrupt_history() {
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(await migrator.isSourceAvailable());
+
+ let data = await migrator.getMigrateData(PROFILE);
+ Assert.ok(
+ data & MigrationUtils.resourceTypes.BOOKMARKS,
+ "Bookmarks resource available."
+ );
+ Assert.ok(
+ !(data & MigrationUtils.resourceTypes.HISTORY),
+ "Corrupt history resource unavailable."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_credit_cards.js b/browser/components/migration/tests/unit/test_Chrome_credit_cards.js
new file mode 100644
index 0000000000..5c4d3517d2
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_credit_cards.js
@@ -0,0 +1,239 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* global structuredClone */
+
+const PROFILE = {
+ id: "Default",
+ name: "Default",
+};
+
+const PAYMENT_METHODS = [
+ {
+ name_on_card: "Name Name",
+ card_number: "4532962432748929", // Visa
+ expiration_month: 3,
+ expiration_year: 2027,
+ },
+ {
+ name_on_card: "Name Name Name",
+ card_number: "5359908373796416", // Mastercard
+ expiration_month: 5,
+ expiration_year: 2028,
+ },
+ {
+ name_on_card: "Name",
+ card_number: "346624461807588", // AMEX
+ expiration_month: 4,
+ expiration_year: 2026,
+ },
+];
+
+let OSKeyStoreTestUtils;
+add_setup(async function os_key_store_setup() {
+ ({ OSKeyStoreTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/OSKeyStoreTestUtils.sys.mjs"
+ ));
+ OSKeyStoreTestUtils.setup();
+ registerCleanupFunction(async function cleanup() {
+ await OSKeyStoreTestUtils.cleanup();
+ });
+});
+
+let rootDir = do_get_file("chromefiles/", true);
+
+function checkCardsAreEqual(importedCard, testCard, id) {
+ const CC_NUMBER_RE = /^(\*+)(.{4})$/;
+
+ Assert.equal(
+ importedCard["cc-name"],
+ testCard.name_on_card,
+ "The two logins ID " + id + " have the same name on card"
+ );
+
+ let matches = CC_NUMBER_RE.exec(importedCard["cc-number"]);
+ Assert.notEqual(matches, null);
+ Assert.equal(importedCard["cc-number"].length, testCard.card_number.length);
+ Assert.equal(testCard.card_number.endsWith(matches[2]), true);
+ Assert.notEqual(importedCard["cc-number-encrypted"], "");
+
+ Assert.equal(
+ importedCard["cc-exp-month"],
+ testCard.expiration_month,
+ "The two logins ID " + id + " have the same expiration month"
+ );
+ Assert.equal(
+ importedCard["cc-exp-year"],
+ testCard.expiration_year,
+ "The two logins ID " + id + " have the same expiration year"
+ );
+}
+
+add_task(async function setup_fakePaths() {
+ let pathId;
+ if (AppConstants.platform == "macosx") {
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ pathId = "LocalAppData";
+ } else {
+ pathId = "Home";
+ }
+ registerFakePath(pathId, rootDir);
+});
+
+add_task(async function test_credit_cards() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo_check_true(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ let loginCrypto;
+ let profilePathSegments;
+
+ let mockMacOSKeychain = {
+ passphrase: "bW96aWxsYWZpcmVmb3g=",
+ serviceName: "TESTING Chrome Safe Storage",
+ accountName: "TESTING Chrome",
+ };
+ if (AppConstants.platform == "macosx") {
+ let { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMacOSLoginCrypto.sys.mjs"
+ );
+ loginCrypto = new ChromeMacOSLoginCrypto(
+ mockMacOSKeychain.serviceName,
+ mockMacOSKeychain.accountName,
+ mockMacOSKeychain.passphrase
+ );
+ profilePathSegments = [
+ "Application Support",
+ "Google",
+ "Chrome",
+ "Default",
+ ];
+ } else if (AppConstants.platform == "win") {
+ let { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeWindowsLoginCrypto.sys.mjs"
+ );
+ loginCrypto = new ChromeWindowsLoginCrypto("Chrome");
+ profilePathSegments = ["Google", "Chrome", "User Data", "Default"];
+ } else {
+ throw new Error("Not implemented");
+ }
+
+ let target = rootDir.clone();
+ let defaultFolderPath = PathUtils.join(target.path, ...profilePathSegments);
+ let webDataPath = PathUtils.join(defaultFolderPath, "Web Data");
+ let localStatePath = defaultFolderPath.replace("Default", "");
+
+ await IOUtils.makeDirectory(defaultFolderPath, {
+ createAncestor: true,
+ ignoreExisting: true,
+ });
+
+ // Copy Web Data database into Default profile
+ const sourcePathWebData = do_get_file(
+ "AppData/Local/Google/Chrome/User Data/Default/Web Data"
+ ).path;
+ await IOUtils.copy(sourcePathWebData, webDataPath);
+
+ const sourcePathLocalState = do_get_file(
+ "AppData/Local/Google/Chrome/User Data/Local State"
+ ).path;
+ await IOUtils.copy(sourcePathLocalState, localStatePath);
+
+ let dbConn = await Sqlite.openConnection({ path: webDataPath });
+
+ for (let card of PAYMENT_METHODS) {
+ let encryptedCardNumber = await loginCrypto.encryptData(card.card_number);
+ let cardNumberEncryptedValue = new Uint8Array(
+ loginCrypto.stringToArray(encryptedCardNumber)
+ );
+
+ let cardCopy = structuredClone(card);
+
+ cardCopy.card_number_encrypted = cardNumberEncryptedValue;
+ delete cardCopy.card_number;
+
+ await dbConn.execute(
+ `INSERT INTO credit_cards
+ (name_on_card, card_number_encrypted, expiration_month, expiration_year)
+ VALUES (:name_on_card, :card_number_encrypted, :expiration_month, :expiration_year)
+ `,
+ cardCopy
+ );
+ }
+
+ await dbConn.close();
+
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ if (AppConstants.platform == "macosx") {
+ Object.assign(migrator, {
+ _keychainServiceName: mockMacOSKeychain.serviceName,
+ _keychainAccountName: mockMacOSKeychain.accountName,
+ _keychainMockPassphrase: mockMacOSKeychain.passphrase,
+ });
+ }
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ Services.prefs.setBoolPref(
+ "browser.migrate.chrome.payment_methods.enabled",
+ false
+ );
+ Assert.ok(
+ !(
+ (await migrator.getMigrateData(PROFILE)) &
+ MigrationUtils.resourceTypes.PAYMENT_METHODS
+ ),
+ "Should be able to disable migrating payment methods"
+ );
+ // Clear the cached resources now so that a re-check for payment methods
+ // will look again.
+ delete migrator._resourcesByProfile[PROFILE.id];
+
+ Services.prefs.setBoolPref(
+ "browser.migrate.chrome.payment_methods.enabled",
+ true
+ );
+
+ Assert.ok(
+ (await migrator.getMigrateData(PROFILE)) &
+ MigrationUtils.resourceTypes.PAYMENT_METHODS,
+ "Should be able to enable migrating payment methods"
+ );
+
+ let { formAutofillStorage } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillStorage.sys.mjs"
+ );
+ await formAutofillStorage.initialize();
+
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.PAYMENT_METHODS,
+ PROFILE
+ );
+
+ let cards = await formAutofillStorage.creditCards.getAll();
+
+ Assert.equal(
+ cards.length,
+ PAYMENT_METHODS.length,
+ "Check there are still the same number of credit cards after re-importing the data"
+ );
+ Assert.equal(
+ cards.length,
+ MigrationUtils._importQuantities.cards,
+ "Check telemetry matches the actual import."
+ );
+
+ for (let i = 0; i < PAYMENT_METHODS.length; i++) {
+ checkCardsAreEqual(cards[i], PAYMENT_METHODS[i], i + 1);
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_extensions.js b/browser/components/migration/tests/unit/test_Chrome_extensions.js
new file mode 100644
index 0000000000..7aa7a94194
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_extensions.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AMBrowserExtensionsImport, AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+const { ChromeProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeProfileMigrator.sys.mjs"
+);
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+const TEST_SERVER = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
+
+const PROFILE = {
+ id: "Default",
+ name: "Person 1",
+};
+
+const IMPORTED_ADDON_1 = {
+ name: "A Firefox extension",
+ version: "1.0",
+ browser_specific_settings: { gecko: { id: "some-ff@extension" } },
+};
+
+// Setup chrome user data path for all platforms.
+ChromeMigrationUtils.getDataPath = () => {
+ return Promise.resolve(
+ do_get_file("Library/Application Support/Google/Chrome/").path
+ );
+};
+
+const mockAddonRepository = ({ addons = [] } = {}) => {
+ return {
+ async getMappedAddons(browserID, extensionIDs) {
+ Assert.equal(browserID, "chrome", "expected browser ID");
+ // Sort extension IDs to have a predictable order.
+ extensionIDs.sort();
+ Assert.deepEqual(
+ extensionIDs,
+ ["fake-extension-1", "fake-extension-2"],
+ "expected an array of 2 extension IDs"
+ );
+ return Promise.resolve({
+ addons,
+ matchedIDs: [],
+ unmatchedIDs: [],
+ });
+ },
+ };
+};
+
+add_setup(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+
+ // Create a Firefox XPI that we can use during the import.
+ const xpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: IMPORTED_ADDON_1,
+ });
+ TEST_SERVER.registerFile(`/addons/addon-1.xpi`, xpi);
+
+ // Override the `AddonRepository` in `AMBrowserExtensionsImport` with our own
+ // mock so that we control the add-ons that are mapped to the list of Chrome
+ // extension IDs.
+ const addons = [
+ {
+ id: IMPORTED_ADDON_1.browser_specific_settings.gecko.id,
+ name: IMPORTED_ADDON_1.name,
+ version: IMPORTED_ADDON_1.version,
+ sourceURI: Services.io.newURI(`http://example.com/addons/addon-1.xpi`),
+ icons: {},
+ },
+ ];
+ AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ addons });
+
+ registerCleanupFunction(() => {
+ // Clear the add-on repository override.
+ AMBrowserExtensionsImport._addonRepository = null;
+ });
+});
+
+add_task(
+ { pref_set: [["browser.migrate.chrome.extensions.enabled", true]] },
+ async function test_import_extensions() {
+ const migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ let promiseTopic = TestUtils.topicObserved(
+ "webextension-imported-addons-pending"
+ );
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.EXTENSIONS,
+ PROFILE,
+ true
+ );
+ await promiseTopic;
+ // When this property is `true`, the UI should show a badge on the appmenu
+ // button, and the user can finalize the import later.
+ Assert.ok(
+ AMBrowserExtensionsImport.canCompleteOrCancelInstalls,
+ "expected some add-ons to have been imported"
+ );
+
+ // Let's actually complete the import programatically below.
+ promiseTopic = TestUtils.topicObserved(
+ "webextension-imported-addons-complete"
+ );
+ await AMBrowserExtensionsImport.completeInstalls();
+ await Promise.all([
+ AddonTestUtils.promiseInstallEvent("onInstallEnded"),
+ promiseTopic,
+ ]);
+
+ // The add-on should be installed and therefore it can be uninstalled.
+ const addon = await AddonManager.getAddonByID(
+ IMPORTED_ADDON_1.browser_specific_settings.gecko.id
+ );
+ await addon.uninstall();
+ }
+);
+
+/**
+ * Test that, for now at least, the extension resource type is only made
+ * available for Chrome and none of the derivitive browsers.
+ */
+add_task(
+ { pref_set: [["browser.migrate.chrome.extensions.enabled", true]] },
+ async function test_only_chrome_migrates_extensions() {
+ for (const key of MigrationUtils.availableMigratorKeys) {
+ let migrator = await MigrationUtils.getMigrator(key);
+
+ if (migrator instanceof ChromeProfileMigrator && key != "chrome") {
+ info("Testing migrator with key " + key);
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "First check the source exists"
+ );
+ let resourceTypes = await migrator.getMigrateData(PROFILE);
+ Assert.ok(
+ !(resourceTypes & MigrationUtils.resourceTypes.EXTENSIONS),
+ "Should not offer the extension resource type"
+ );
+ }
+ }
+ }
+);
diff --git a/browser/components/migration/tests/unit/test_Chrome_formdata.js b/browser/components/migration/tests/unit/test_Chrome_formdata.js
new file mode 100644
index 0000000000..1dc411cb14
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_formdata.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { FormHistory } = ChromeUtils.importESModule(
+ "resource://gre/modules/FormHistory.sys.mjs"
+);
+
+let rootDir = do_get_file("chromefiles/", true);
+
+add_setup(async function setup_fakePaths() {
+ let pathId;
+ if (AppConstants.platform == "macosx") {
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ pathId = "LocalAppData";
+ } else {
+ pathId = "Home";
+ }
+ registerFakePath(pathId, rootDir);
+});
+
+/**
+ * This function creates a testing database in the default profile,
+ * populates it with 10 example data entries,migrates the database,
+ * and then searches for each entry to ensure it exists in the FormHistory.
+ *
+ * @async
+ * @param {string} migratorKey
+ * A string that identifies the type of migrator object to be retrieved.
+ * @param {Array<string>} subDirs
+ * An array of strings that specifies the subdirectories for the target profile directory.
+ * @returns {Promise<undefined>}
+ * A Promise that resolves when the migration is completed.
+ */
+async function testFormdata(migratorKey, subDirs) {
+ if (AppConstants.platform == "macosx") {
+ subDirs.unshift("Application Support");
+ } else if (AppConstants.platform == "win") {
+ subDirs.push("User Data");
+ } else {
+ subDirs.unshift(".config");
+ }
+
+ let target = rootDir.clone();
+ // Pretend this is the default profile
+ subDirs.push("Default");
+ while (subDirs.length) {
+ target.append(subDirs.shift());
+ }
+
+ await IOUtils.makeDirectory(target.path, {
+ createAncestor: true,
+ ignoreExisting: true,
+ });
+
+ target.append("Web Data");
+ await IOUtils.remove(target.path, { ignoreAbsent: true });
+
+ // Clear any search history results
+ await FormHistory.update({ op: "remove" });
+
+ let dbConn = await Sqlite.openConnection({ path: target.path });
+
+ await dbConn.execute(
+ `CREATE TABLE "autofill" (name VARCHAR, value VARCHAR, value_lower VARCHAR, date_created INTEGER DEFAULT 0, date_last_used INTEGER DEFAULT 0, count INTEGER DEFAULT 1, PRIMARY KEY (name, value))`
+ );
+ for (let i = 0; i < 10; i++) {
+ await dbConn.execute(
+ `INSERT INTO autofill VALUES (:name, :value, :value_lower, :date_created, :date_last_used, :count)`,
+ {
+ name: `name${i}`,
+ value: `example${i}`,
+ value_lower: `example${i}`,
+ date_created: Math.round(Date.now() / 1000) - i * 10000,
+ date_last_used: Date.now(),
+ count: i,
+ }
+ );
+ }
+ await dbConn.close();
+
+ let migrator = await MigrationUtils.getMigrator(migratorKey);
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable());
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.FORMDATA, {
+ id: "Default",
+ name: "Person 1",
+ });
+
+ for (let i = 0; i < 10; i++) {
+ let results = await FormHistory.search(["fieldname", "value"], {
+ fieldname: `name${i}`,
+ value: `example${i}`,
+ });
+ Assert.ok(results.length, `Should have item${i} in FormHistory`);
+ }
+}
+
+add_task(async function test_Chrome() {
+ let subDirs =
+ AppConstants.platform == "linux" ? ["google-chrome"] : ["Google", "Chrome"];
+ await testFormdata("chrome", subDirs);
+});
+
+add_task(async function test_ChromiumEdge() {
+ if (AppConstants.platform == "linux") {
+ // Edge isn't available on Linux.
+ return;
+ }
+ let subDirs =
+ AppConstants.platform == "macosx"
+ ? ["Microsoft Edge"]
+ : ["Microsoft", "Edge"];
+ await testFormdata("chromium-edge", subDirs);
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_history.js b/browser/components/migration/tests/unit/test_Chrome_history.js
new file mode 100644
index 0000000000..c88a6380c2
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_history.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+
+const SOURCE_PROFILE_DIR = "Library/Application Support/Google/Chrome/Default/";
+
+const PROFILE = {
+ id: "Default",
+ name: "Person 1",
+};
+
+/**
+ * TEST_URLS reflects the data stored in '${SOURCE_PROFILE_DIR}HistoryMaster'.
+ * The main object reflects the data in the 'urls' table. The visits property
+ * reflects the associated data in the 'visits' table.
+ */
+const TEST_URLS = [
+ {
+ id: 1,
+ url: "http://example.com/",
+ title: "test",
+ visit_count: 1,
+ typed_count: 0,
+ last_visit_time: 13193151310368000,
+ hidden: 0,
+ visits: [
+ {
+ id: 1,
+ url: 1,
+ visit_time: 13193151310368000,
+ from_visit: 0,
+ transition: 805306370,
+ segment_id: 0,
+ visit_duration: 10745006,
+ incremented_omnibox_typed_score: 0,
+ },
+ ],
+ },
+ {
+ id: 2,
+ url: "http://invalid.com/",
+ title: "test2",
+ visit_count: 1,
+ typed_count: 0,
+ last_visit_time: 13193154948901000,
+ hidden: 0,
+ visits: [
+ {
+ id: 2,
+ url: 2,
+ visit_time: 13193154948901000,
+ from_visit: 0,
+ transition: 805306376,
+ segment_id: 0,
+ visit_duration: 6568270,
+ incremented_omnibox_typed_score: 0,
+ },
+ ],
+ },
+];
+
+async function setVisitTimes(time) {
+ let loginDataFile = do_get_file(`${SOURCE_PROFILE_DIR}History`);
+ let dbConn = await Sqlite.openConnection({ path: loginDataFile.path });
+
+ await dbConn.execute(`UPDATE urls SET last_visit_time = :last_visit_time`, {
+ last_visit_time: time,
+ });
+ await dbConn.execute(`UPDATE visits SET visit_time = :visit_time`, {
+ visit_time: time,
+ });
+
+ await dbConn.close();
+}
+
+function setExpectedVisitTimes(time) {
+ for (let urlInfo of TEST_URLS) {
+ urlInfo.last_visit_time = time;
+ urlInfo.visits[0].visit_time = time;
+ }
+}
+
+function assertEntryMatches(entry, urlInfo, dateWasInFuture = false) {
+ info(`Checking url: ${urlInfo.url}`);
+ Assert.ok(entry, `Should have stored an entry`);
+
+ Assert.equal(entry.url, urlInfo.url, "Should have the correct URL");
+ Assert.equal(entry.title, urlInfo.title, "Should have the correct title");
+ Assert.equal(
+ entry.visits.length,
+ urlInfo.visits.length,
+ "Should have the correct number of visits"
+ );
+
+ for (let index in urlInfo.visits) {
+ Assert.equal(
+ entry.visits[index].transition,
+ PlacesUtils.history.TRANSITIONS.LINK,
+ "Should have Link type transition"
+ );
+
+ if (dateWasInFuture) {
+ Assert.lessOrEqual(
+ entry.visits[index].date.getTime(),
+ new Date().getTime(),
+ "Should have moved the date to no later than the current date."
+ );
+ } else {
+ Assert.equal(
+ entry.visits[index].date.getTime(),
+ ChromeMigrationUtils.chromeTimeToDate(
+ urlInfo.visits[index].visit_time,
+ new Date()
+ ).getTime(),
+ "Should have the correct date"
+ );
+ }
+ }
+}
+
+function setupHistoryFile() {
+ removeHistoryFile();
+ let file = do_get_file(`${SOURCE_PROFILE_DIR}HistoryMaster`);
+ file.copyTo(file.parent, "History");
+}
+
+function removeHistoryFile() {
+ let file = do_get_file(`${SOURCE_PROFILE_DIR}History`, true);
+ try {
+ file.remove(false);
+ } catch (ex) {
+ // It is ok if this doesn't exist.
+ if (ex.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
+ throw ex;
+ }
+ }
+}
+
+add_task(async function setup() {
+ registerFakePath("ULibDir", do_get_file("Library/"));
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ removeHistoryFile();
+ });
+});
+
+add_task(async function test_import() {
+ setupHistoryFile();
+ await PlacesUtils.history.clear();
+ // Update to ~10 days ago since the date can't be too old or Places may expire it.
+ const pastDate = new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 10);
+ const pastChromeTime = ChromeMigrationUtils.dateToChromeTime(pastDate);
+ await setVisitTimes(pastChromeTime);
+ setExpectedVisitTimes(pastChromeTime);
+
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.HISTORY,
+ PROFILE
+ );
+
+ for (let urlInfo of TEST_URLS) {
+ let entry = await PlacesUtils.history.fetch(urlInfo.url, {
+ includeVisits: true,
+ });
+ assertEntryMatches(entry, urlInfo);
+ }
+});
+
+add_task(async function test_import_future_date() {
+ setupHistoryFile();
+ await PlacesUtils.history.clear();
+ const futureDate = new Date().getTime() + 6000 * 60 * 24;
+ await setVisitTimes(ChromeMigrationUtils.dateToChromeTime(futureDate));
+
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.HISTORY,
+ PROFILE
+ );
+
+ for (let urlInfo of TEST_URLS) {
+ let entry = await PlacesUtils.history.fetch(urlInfo.url, {
+ includeVisits: true,
+ });
+ assertEntryMatches(entry, urlInfo, true);
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_passwords.js b/browser/components/migration/tests/unit/test_Chrome_passwords.js
new file mode 100644
index 0000000000..374b697c75
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_passwords.js
@@ -0,0 +1,373 @@
+"use strict";
+
+const PROFILE = {
+ id: "Default",
+ name: "Person 1",
+};
+
+const TEST_LOGINS = [
+ {
+ id: 1, // id of the row in the chrome login db
+ username: "username",
+ password: "password",
+ origin: "https://c9.io",
+ formActionOrigin: "https://c9.io",
+ httpRealm: null,
+ usernameField: "inputEmail",
+ passwordField: "inputPassword",
+ timeCreated: 1437418416037,
+ timePasswordChanged: 1437418416037,
+ timesUsed: 1,
+ },
+ {
+ id: 2,
+ username: "username@gmail.com",
+ password: "password2",
+ origin: "https://accounts.google.com",
+ formActionOrigin: "https://accounts.google.com",
+ httpRealm: null,
+ usernameField: "Email",
+ passwordField: "Passwd",
+ timeCreated: 1437418446598,
+ timePasswordChanged: 1437418446598,
+ timesUsed: 6,
+ },
+ {
+ id: 3,
+ username: "username",
+ password: "password3",
+ origin: "https://www.facebook.com",
+ formActionOrigin: "https://www.facebook.com",
+ httpRealm: null,
+ usernameField: "email",
+ passwordField: "pass",
+ timeCreated: 1437418478851,
+ timePasswordChanged: 1437418478851,
+ timesUsed: 1,
+ },
+ {
+ id: 4,
+ username: "user",
+ password: "اقرأPÀßwörd",
+ origin: "http://httpbin.org",
+ formActionOrigin: null,
+ httpRealm: "me@kennethreitz.com", // Digest auth.
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1437787462368,
+ timePasswordChanged: 1437787462368,
+ timesUsed: 1,
+ },
+ {
+ id: 5,
+ username: "buser",
+ password: "bpassword",
+ origin: "http://httpbin.org",
+ formActionOrigin: null,
+ httpRealm: "Fake Realm", // Basic auth.
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1437787539233,
+ timePasswordChanged: 1437787539233,
+ timesUsed: 1,
+ },
+ {
+ id: 6,
+ username: "username",
+ password: "password6",
+ origin: "https://www.example.com",
+ formActionOrigin: "", // NULL `action_url`
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "pass",
+ timeCreated: 1557291348878,
+ timePasswordChanged: 1557291348878,
+ timesUsed: 1,
+ },
+ {
+ id: 7,
+ version: "v10",
+ username: "username",
+ password: "password",
+ origin: "https://v10.io",
+ formActionOrigin: "https://v10.io",
+ httpRealm: null,
+ usernameField: "inputEmail",
+ passwordField: "inputPassword",
+ timeCreated: 1437418416037,
+ timePasswordChanged: 1437418416037,
+ timesUsed: 1,
+ },
+];
+
+var loginCrypto;
+var dbConn;
+
+async function promiseSetPassword(login) {
+ const encryptedString = await loginCrypto.encryptData(
+ login.password,
+ login.version
+ );
+ info(`promiseSetPassword: ${encryptedString}`);
+ const passwordValue = new Uint8Array(
+ loginCrypto.stringToArray(encryptedString)
+ );
+ return dbConn.execute(
+ `UPDATE logins
+ SET password_value = :password_value
+ WHERE rowid = :rowid
+ `,
+ { password_value: passwordValue, rowid: login.id }
+ );
+}
+
+function checkLoginsAreEqual(passwordManagerLogin, chromeLogin, id) {
+ passwordManagerLogin.QueryInterface(Ci.nsILoginMetaInfo);
+
+ Assert.equal(
+ passwordManagerLogin.username,
+ chromeLogin.username,
+ "The two logins ID " + id + " have the same username"
+ );
+ Assert.equal(
+ passwordManagerLogin.password,
+ chromeLogin.password,
+ "The two logins ID " + id + " have the same password"
+ );
+ Assert.equal(
+ passwordManagerLogin.origin,
+ chromeLogin.origin,
+ "The two logins ID " + id + " have the same origin"
+ );
+ Assert.equal(
+ passwordManagerLogin.formActionOrigin,
+ chromeLogin.formActionOrigin,
+ "The two logins ID " + id + " have the same formActionOrigin"
+ );
+ Assert.equal(
+ passwordManagerLogin.httpRealm,
+ chromeLogin.httpRealm,
+ "The two logins ID " + id + " have the same httpRealm"
+ );
+ Assert.equal(
+ passwordManagerLogin.usernameField,
+ chromeLogin.usernameField,
+ "The two logins ID " + id + " have the same usernameElement"
+ );
+ Assert.equal(
+ passwordManagerLogin.passwordField,
+ chromeLogin.passwordField,
+ "The two logins ID " + id + " have the same passwordElement"
+ );
+ Assert.equal(
+ passwordManagerLogin.timeCreated,
+ chromeLogin.timeCreated,
+ "The two logins ID " + id + " have the same timeCreated"
+ );
+ Assert.equal(
+ passwordManagerLogin.timePasswordChanged,
+ chromeLogin.timePasswordChanged,
+ "The two logins ID " + id + " have the same timePasswordChanged"
+ );
+ Assert.equal(
+ passwordManagerLogin.timesUsed,
+ chromeLogin.timesUsed,
+ "The two logins ID " + id + " have the same timesUsed"
+ );
+}
+
+function generateDifferentLogin(login) {
+ const newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+
+ newLogin.init(
+ login.origin,
+ login.formActionOrigin,
+ null,
+ login.username,
+ login.password + 1,
+ login.usernameField + 1,
+ login.passwordField + 1
+ );
+ newLogin.QueryInterface(Ci.nsILoginMetaInfo);
+ newLogin.timeCreated = login.timeCreated + 1;
+ newLogin.timePasswordChanged = login.timePasswordChanged + 1;
+ newLogin.timesUsed = login.timesUsed + 1;
+ return newLogin;
+}
+
+add_task(async function setup() {
+ let dirSvcPath;
+ let pathId;
+ let profilePathSegments;
+
+ // Use a mock service and account name to avoid a Keychain auth. prompt that
+ // would block the test from finishing if Chrome has already created a matching
+ // Keychain entry. This allows us to still exercise the keychain lookup code.
+ // The mock encryption passphrase is used when the Keychain item isn't found.
+ const mockMacOSKeychain = {
+ passphrase: "bW96aWxsYWZpcmVmb3g=",
+ serviceName: "TESTING Chrome Safe Storage",
+ accountName: "TESTING Chrome",
+ };
+ if (AppConstants.platform == "macosx") {
+ const { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMacOSLoginCrypto.sys.mjs"
+ );
+ loginCrypto = new ChromeMacOSLoginCrypto(
+ mockMacOSKeychain.serviceName,
+ mockMacOSKeychain.accountName,
+ mockMacOSKeychain.passphrase
+ );
+ dirSvcPath = "Library/";
+ pathId = "ULibDir";
+ profilePathSegments = [
+ "Application Support",
+ "Google",
+ "Chrome",
+ "Default",
+ "Login Data",
+ ];
+ } else if (AppConstants.platform == "win") {
+ const { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeWindowsLoginCrypto.sys.mjs"
+ );
+ loginCrypto = new ChromeWindowsLoginCrypto("Chrome");
+ dirSvcPath = "AppData/Local/";
+ pathId = "LocalAppData";
+ profilePathSegments = [
+ "Google",
+ "Chrome",
+ "User Data",
+ "Default",
+ "Login Data",
+ ];
+ } else {
+ throw new Error("Not implemented");
+ }
+ const dirSvcFile = do_get_file(dirSvcPath);
+ registerFakePath(pathId, dirSvcFile);
+
+ info(PathUtils.join(dirSvcFile.path, ...profilePathSegments));
+ const loginDataFilePath = PathUtils.join(
+ dirSvcFile.path,
+ ...profilePathSegments
+ );
+ dbConn = await Sqlite.openConnection({ path: loginDataFilePath });
+
+ if (AppConstants.platform == "macosx") {
+ const migrator = await MigrationUtils.getMigrator("chrome");
+ Object.assign(migrator, {
+ _keychainServiceName: mockMacOSKeychain.serviceName,
+ _keychainAccountName: mockMacOSKeychain.accountName,
+ _keychainMockPassphrase: mockMacOSKeychain.passphrase,
+ });
+ }
+
+ registerCleanupFunction(() => {
+ Services.logins.removeAllUserFacingLogins();
+ if (loginCrypto.finalize) {
+ loginCrypto.finalize();
+ }
+ return dbConn.close();
+ });
+});
+
+add_task(async function test_importIntoEmptyDB() {
+ for (const login of TEST_LOGINS) {
+ await promiseSetPassword(login);
+ }
+
+ const migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ let logins = await Services.logins.getAllLogins();
+ Assert.equal(logins.length, 0, "There are no logins initially");
+
+ // Migrate the logins.
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.PASSWORDS,
+ PROFILE,
+ true
+ );
+
+ logins = await Services.logins.getAllLogins();
+ Assert.equal(
+ logins.length,
+ TEST_LOGINS.length,
+ "Check login count after importing the data"
+ );
+ Assert.equal(
+ logins.length,
+ MigrationUtils._importQuantities.logins,
+ "Check telemetry matches the actual import."
+ );
+
+ for (let i = 0; i < TEST_LOGINS.length; i++) {
+ checkLoginsAreEqual(logins[i], TEST_LOGINS[i], i + 1);
+ }
+});
+
+// Test that existing logins for the same primary key don't get overwritten
+add_task(async function test_importExistingLogins() {
+ const migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ await migrator.isSourceAvailable(),
+ "Sanity check the source exists"
+ );
+
+ Services.logins.removeAllUserFacingLogins();
+ let logins = await Services.logins.getAllLogins();
+ Assert.equal(
+ logins.length,
+ 0,
+ "There are no logins after removing all of them"
+ );
+
+ const newLogins = [];
+
+ // Create 3 new logins that are different but where the key properties are still the same.
+ for (let i = 0; i < 3; i++) {
+ newLogins.push(generateDifferentLogin(TEST_LOGINS[i]));
+ await Services.logins.addLoginAsync(newLogins[i]);
+ }
+
+ logins = await Services.logins.getAllLogins();
+ Assert.equal(
+ logins.length,
+ newLogins.length,
+ "Check login count after the insertion"
+ );
+
+ for (let i = 0; i < newLogins.length; i++) {
+ checkLoginsAreEqual(logins[i], newLogins[i], i + 1);
+ }
+ // Migrate the logins.
+ await promiseMigration(
+ migrator,
+ MigrationUtils.resourceTypes.PASSWORDS,
+ PROFILE,
+ true
+ );
+
+ logins = await Services.logins.getAllLogins();
+ Assert.equal(
+ logins.length,
+ TEST_LOGINS.length,
+ "Check there are still the same number of logins after re-importing the data"
+ );
+ Assert.equal(
+ logins.length,
+ MigrationUtils._importQuantities.logins,
+ "Check telemetry matches the actual import."
+ );
+
+ for (let i = 0; i < newLogins.length; i++) {
+ checkLoginsAreEqual(logins[i], newLogins[i], i + 1);
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_passwords_emptySource.js b/browser/components/migration/tests/unit/test_Chrome_passwords_emptySource.js
new file mode 100644
index 0000000000..0e6993fded
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_passwords_emptySource.js
@@ -0,0 +1,43 @@
+"use strict";
+
+const { ChromeProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeProfileMigrator.sys.mjs"
+);
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+add_task(async function test_importEmptyDBWithoutAuthPrompts() {
+ let dirSvcPath;
+ let pathId;
+
+ if (AppConstants.platform == "macosx") {
+ dirSvcPath = "LibraryWithNoData/";
+ pathId = "ULibDir";
+ } else if (AppConstants.platform == "win") {
+ dirSvcPath = "AppData/LocalWithNoData/";
+ pathId = "LocalAppData";
+ } else {
+ throw new Error("Not implemented");
+ }
+ let dirSvcFile = do_get_file(dirSvcPath);
+ registerFakePath(pathId, dirSvcFile);
+
+ let sandbox = sinon.createSandbox();
+ sandbox
+ .stub(ChromeProfileMigrator.prototype, "canGetPermissions")
+ .resolves(true);
+ sandbox
+ .stub(ChromeProfileMigrator.prototype, "hasPermissions")
+ .resolves(true);
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = await MigrationUtils.getMigrator("chrome");
+ Assert.ok(
+ !migrator,
+ "Migrator should not be available since there are no passwords"
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_permissions.js b/browser/components/migration/tests/unit/test_Chrome_permissions.js
new file mode 100644
index 0000000000..6dfd8bcceb
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_permissions.js
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that if the migrator does not have the permission to
+ * read from the Chrome data directory, that it can request
+ * permission to read from it, if the system allows.
+ */
+
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+
+const { ChromeProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeProfileMigrator.sys.mjs"
+);
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const { MockFilePicker } = ChromeUtils.importESModule(
+ "resource://testing-common/MockFilePicker.sys.mjs"
+);
+
+let gTempDir;
+
+add_setup(async () => {
+ Services.prefs.setBoolPref(
+ "browser.migrate.chrome.get_permissions.enabled",
+ true
+ );
+ gTempDir = do_get_tempdir();
+ await IOUtils.writeJSON(PathUtils.join(gTempDir.path, "Local State"), []);
+
+ MockFilePicker.init(globalThis);
+ registerCleanupFunction(() => {
+ MockFilePicker.cleanup();
+ });
+});
+
+/**
+ * Tests that canGetPermissions will return false if the platform does
+ * not allow for folder selection in the native file picker, and returns
+ * the data path otherwise.
+ */
+add_task(async function test_canGetPermissions() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = new ChromeProfileMigrator();
+ let canGetPermissionsStub = sandbox
+ .stub(MigrationUtils, "canGetPermissionsOnPlatform")
+ .resolves(false);
+ sandbox.stub(ChromeMigrationUtils, "getDataPath").resolves(gTempDir.path);
+
+ Assert.ok(
+ !(await migrator.canGetPermissions()),
+ "Should not be able to get permissions."
+ );
+
+ canGetPermissionsStub.resolves(true);
+
+ Assert.equal(
+ await migrator.canGetPermissions(),
+ gTempDir.path,
+ "Should be able to get the permissions path."
+ );
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that getPermissions will show the native file picker in a
+ * loop until either the user cancels or selects a folder that grants
+ * read permissions to the data directory.
+ */
+add_task(async function test_getPermissions() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = new ChromeProfileMigrator();
+ sandbox.stub(MigrationUtils, "canGetPermissionsOnPlatform").resolves(true);
+ sandbox.stub(ChromeMigrationUtils, "getDataPath").resolves(gTempDir.path);
+ let hasPermissionsStub = sandbox
+ .stub(migrator, "hasPermissions")
+ .resolves(false);
+
+ Assert.equal(
+ await migrator.canGetPermissions(),
+ gTempDir.path,
+ "Should be able to get the permissions path."
+ );
+
+ let filePickerSeenCount = 0;
+
+ let filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ Assert.ok(true, "Filepicker shown.");
+ MockFilePicker.useDirectory([gTempDir.path]);
+ filePickerSeenCount++;
+ if (filePickerSeenCount > 3) {
+ Assert.ok(true, "File picker looped 3 times.");
+ hasPermissionsStub.resolves(true);
+ resolve();
+ }
+ };
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ Assert.ok(
+ await migrator.getPermissions(),
+ "Should report that we got permissions."
+ );
+
+ await filePickerShownPromise;
+
+ // Make sure that the user can also hit "cancel" and that we
+ // file picker loop.
+
+ hasPermissionsStub.resolves(false);
+
+ filePickerSeenCount = 0;
+ filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ Assert.ok(true, "Filepicker shown.");
+ filePickerSeenCount++;
+ Assert.equal(filePickerSeenCount, 1, "Saw the picker once.");
+ resolve();
+ };
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnCancel;
+ Assert.ok(
+ !(await migrator.getPermissions()),
+ "Should report that we didn't get permissions."
+ );
+ await filePickerShownPromise;
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that if the native file picker chooses a different directory
+ * than the one we originally asked for, that we remap attempts to
+ * read profiles from that new directory. This is because Ubuntu Snaps
+ * will return us paths from the native file picker that are symlinks
+ * to the original directories.
+ */
+add_task(async function test_remapDirectories() {
+ let remapDir = new FileUtils.File(
+ await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "test-chrome-migration"
+ )
+ );
+ let localStatePath = PathUtils.join(remapDir.path, "Local State");
+ await IOUtils.writeJSON(localStatePath, []);
+
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = new ChromeProfileMigrator();
+ sandbox.stub(MigrationUtils, "canGetPermissionsOnPlatform").resolves(true);
+ sandbox.stub(ChromeMigrationUtils, "getDataPath").resolves(gTempDir.path);
+ let hasPermissionsStub = sandbox
+ .stub(migrator, "hasPermissions")
+ .resolves(false);
+
+ Assert.equal(
+ await migrator.canGetPermissions(),
+ gTempDir.path,
+ "Should be able to get the permissions path."
+ );
+
+ let filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ Assert.ok(true, "Filepicker shown.");
+ MockFilePicker.useDirectory([remapDir.path]);
+ hasPermissionsStub.resolves(true);
+ resolve();
+ };
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ Assert.ok(
+ await migrator.getPermissions(),
+ "Should report that we got permissions."
+ );
+
+ Assert.equal(
+ PathUtils.normalize(await migrator.canGetPermissions()),
+ PathUtils.normalize(remapDir.path),
+ "Should be able to get the remapped permissions path."
+ );
+
+ await filePickerShownPromise;
+
+ sandbox.restore();
+});
diff --git a/browser/components/migration/tests/unit/test_Edge_db_migration.js b/browser/components/migration/tests/unit/test_Edge_db_migration.js
new file mode 100644
index 0000000000..3b2672d9d8
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Edge_db_migration.js
@@ -0,0 +1,849 @@
+"use strict";
+
+const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+);
+const { ESE, KERNEL, gLibs, COLUMN_TYPES, declareESEFunction, loadLibraries } =
+ ChromeUtils.importESModule("resource:///modules/ESEDBReader.sys.mjs");
+const { EdgeProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/EdgeProfileMigrator.sys.mjs"
+);
+
+let gESEInstanceCounter = 1;
+
+ESE.JET_COLUMNCREATE_W = new ctypes.StructType("JET_COLUMNCREATE_W", [
+ { cbStruct: ctypes.unsigned_long },
+ { szColumnName: ESE.JET_PCWSTR },
+ { coltyp: ESE.JET_COLTYP },
+ { cbMax: ctypes.unsigned_long },
+ { grbit: ESE.JET_GRBIT },
+ { pvDefault: ctypes.voidptr_t },
+ { cbDefault: ctypes.unsigned_long },
+ { cp: ctypes.unsigned_long },
+ { columnid: ESE.JET_COLUMNID },
+ { err: ESE.JET_ERR },
+]);
+
+function createColumnCreationWrapper({ name, type, cbMax }) {
+ // We use a wrapper object because we need to be sure the JS engine won't GC
+ // data that we're "only" pointing to.
+ let wrapper = {};
+ wrapper.column = new ESE.JET_COLUMNCREATE_W();
+ wrapper.column.cbStruct = ESE.JET_COLUMNCREATE_W.size;
+ let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
+ wrapper.name = new wchar_tArray(name.length + 1);
+ wrapper.name.value = String(name);
+ wrapper.column.szColumnName = wrapper.name;
+ wrapper.column.coltyp = type;
+ let fallback = 0;
+ switch (type) {
+ case COLUMN_TYPES.JET_coltypText:
+ fallback = 255;
+ // Intentional fall-through
+ case COLUMN_TYPES.JET_coltypLongText:
+ wrapper.column.cbMax = cbMax || fallback || 64 * 1024;
+ break;
+ case COLUMN_TYPES.JET_coltypGUID:
+ wrapper.column.cbMax = 16;
+ break;
+ case COLUMN_TYPES.JET_coltypBit:
+ wrapper.column.cbMax = 1;
+ break;
+ case COLUMN_TYPES.JET_coltypLongLong:
+ wrapper.column.cbMax = 8;
+ break;
+ default:
+ throw new Error("Unknown column type!");
+ }
+
+ wrapper.column.columnid = new ESE.JET_COLUMNID();
+ wrapper.column.grbit = 0;
+ wrapper.column.pvDefault = null;
+ wrapper.column.cbDefault = 0;
+ wrapper.column.cp = 0;
+
+ return wrapper;
+}
+
+// "forward declarations" of indexcreate and setinfo structs, which we don't use.
+ESE.JET_INDEXCREATE = new ctypes.StructType("JET_INDEXCREATE");
+ESE.JET_SETINFO = new ctypes.StructType("JET_SETINFO");
+
+ESE.JET_TABLECREATE_W = new ctypes.StructType("JET_TABLECREATE_W", [
+ { cbStruct: ctypes.unsigned_long },
+ { szTableName: ESE.JET_PCWSTR },
+ { szTemplateTableName: ESE.JET_PCWSTR },
+ { ulPages: ctypes.unsigned_long },
+ { ulDensity: ctypes.unsigned_long },
+ { rgcolumncreate: ESE.JET_COLUMNCREATE_W.ptr },
+ { cColumns: ctypes.unsigned_long },
+ { rgindexcreate: ESE.JET_INDEXCREATE.ptr },
+ { cIndexes: ctypes.unsigned_long },
+ { grbit: ESE.JET_GRBIT },
+ { tableid: ESE.JET_TABLEID },
+ { cCreated: ctypes.unsigned_long },
+]);
+
+function createTableCreationWrapper(tableName, columns) {
+ let wrapper = {};
+ let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
+ wrapper.name = new wchar_tArray(tableName.length + 1);
+ wrapper.name.value = String(tableName);
+ wrapper.table = new ESE.JET_TABLECREATE_W();
+ wrapper.table.cbStruct = ESE.JET_TABLECREATE_W.size;
+ wrapper.table.szTableName = wrapper.name;
+ wrapper.table.szTemplateTableName = null;
+ wrapper.table.ulPages = 1;
+ wrapper.table.ulDensity = 0;
+ let columnArrayType = ESE.JET_COLUMNCREATE_W.array(columns.length);
+ wrapper.columnAry = new columnArrayType();
+ wrapper.table.rgcolumncreate = wrapper.columnAry.addressOfElement(0);
+ wrapper.table.cColumns = columns.length;
+ wrapper.columns = [];
+ for (let i = 0; i < columns.length; i++) {
+ let column = columns[i];
+ let columnWrapper = createColumnCreationWrapper(column);
+ wrapper.columnAry.addressOfElement(i).contents = columnWrapper.column;
+ wrapper.columns.push(columnWrapper);
+ }
+ wrapper.table.rgindexcreate = null;
+ wrapper.table.cIndexes = 0;
+ return wrapper;
+}
+
+function convertValueForWriting(value, valueType) {
+ let buffer;
+ let valueOfValueType = ctypes.UInt64.lo(valueType);
+ switch (valueOfValueType) {
+ case COLUMN_TYPES.JET_coltypLongLong:
+ if (value instanceof Date) {
+ buffer = new KERNEL.FILETIME();
+ let sysTime = new KERNEL.SYSTEMTIME();
+ sysTime.wYear = value.getUTCFullYear();
+ sysTime.wMonth = value.getUTCMonth() + 1;
+ sysTime.wDay = value.getUTCDate();
+ sysTime.wHour = value.getUTCHours();
+ sysTime.wMinute = value.getUTCMinutes();
+ sysTime.wSecond = value.getUTCSeconds();
+ sysTime.wMilliseconds = value.getUTCMilliseconds();
+ let rv = KERNEL.SystemTimeToFileTime(
+ sysTime.address(),
+ buffer.address()
+ );
+ if (!rv) {
+ throw new Error("Failed to get FileTime.");
+ }
+ return [buffer, KERNEL.FILETIME.size];
+ }
+ throw new Error("Unrecognized value for longlong column");
+ case COLUMN_TYPES.JET_coltypLongText:
+ let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
+ buffer = new wchar_tArray(value.length + 1);
+ buffer.value = String(value);
+ return [buffer, buffer.length * 2];
+ case COLUMN_TYPES.JET_coltypBit:
+ buffer = new ctypes.uint8_t();
+ // Bizarre boolean values, but whatever:
+ buffer.value = value ? 255 : 0;
+ return [buffer, 1];
+ case COLUMN_TYPES.JET_coltypGUID:
+ let byteArray = ctypes.ArrayType(ctypes.uint8_t);
+ buffer = new byteArray(16);
+ let j = 0;
+ for (let i = 0; i < value.length; i++) {
+ if (!/[0-9a-f]/i.test(value[i])) {
+ continue;
+ }
+ let byteAsHex = value.substr(i, 2);
+ buffer[j++] = parseInt(byteAsHex, 16);
+ i++;
+ }
+ return [buffer, 16];
+ }
+
+ throw new Error("Unknown type " + valueType);
+}
+
+let initializedESE = false;
+
+let eseDBWritingHelpers = {
+ setupDB(dbFile, tables) {
+ if (!initializedESE) {
+ initializedESE = true;
+ loadLibraries();
+
+ KERNEL.SystemTimeToFileTime = gLibs.kernel.declare(
+ "SystemTimeToFileTime",
+ ctypes.winapi_abi,
+ ctypes.bool,
+ KERNEL.SYSTEMTIME.ptr,
+ KERNEL.FILETIME.ptr
+ );
+
+ declareESEFunction(
+ "CreateDatabaseW",
+ ESE.JET_SESID,
+ ESE.JET_PCWSTR,
+ ESE.JET_PCWSTR,
+ ESE.JET_DBID.ptr,
+ ESE.JET_GRBIT
+ );
+ declareESEFunction(
+ "CreateTableColumnIndexW",
+ ESE.JET_SESID,
+ ESE.JET_DBID,
+ ESE.JET_TABLECREATE_W.ptr
+ );
+ declareESEFunction("BeginTransaction", ESE.JET_SESID);
+ declareESEFunction("CommitTransaction", ESE.JET_SESID, ESE.JET_GRBIT);
+ declareESEFunction(
+ "PrepareUpdate",
+ ESE.JET_SESID,
+ ESE.JET_TABLEID,
+ ctypes.unsigned_long
+ );
+ declareESEFunction(
+ "Update",
+ ESE.JET_SESID,
+ ESE.JET_TABLEID,
+ ctypes.voidptr_t,
+ ctypes.unsigned_long,
+ ctypes.unsigned_long.ptr
+ );
+ declareESEFunction(
+ "SetColumn",
+ ESE.JET_SESID,
+ ESE.JET_TABLEID,
+ ESE.JET_COLUMNID,
+ ctypes.voidptr_t,
+ ctypes.unsigned_long,
+ ESE.JET_GRBIT,
+ ESE.JET_SETINFO.ptr
+ );
+ ESE.SetSystemParameterW(
+ null,
+ 0,
+ 64 /* JET_paramDatabasePageSize*/,
+ 8192,
+ null
+ );
+ }
+
+ let rootPath = dbFile.parent.path + "\\";
+ let logPath = rootPath + "LogFiles\\";
+
+ try {
+ this._instanceId = new ESE.JET_INSTANCE();
+ ESE.CreateInstanceW(
+ this._instanceId.address(),
+ "firefox-dbwriter-" + gESEInstanceCounter++
+ );
+ this._instanceCreated = true;
+
+ ESE.SetSystemParameterW(
+ this._instanceId.address(),
+ 0,
+ 0 /* JET_paramSystemPath*/,
+ 0,
+ rootPath
+ );
+ ESE.SetSystemParameterW(
+ this._instanceId.address(),
+ 0,
+ 1 /* JET_paramTempPath */,
+ 0,
+ rootPath
+ );
+ ESE.SetSystemParameterW(
+ this._instanceId.address(),
+ 0,
+ 2 /* JET_paramLogFilePath*/,
+ 0,
+ logPath
+ );
+ // Shouldn't try to call JetTerm if the following call fails.
+ this._instanceCreated = false;
+ ESE.Init(this._instanceId.address());
+ this._instanceCreated = true;
+ this._sessionId = new ESE.JET_SESID();
+ ESE.BeginSessionW(
+ this._instanceId,
+ this._sessionId.address(),
+ null,
+ null
+ );
+ this._sessionCreated = true;
+
+ this._dbId = new ESE.JET_DBID();
+ this._dbPath = rootPath + "spartan.edb";
+ ESE.CreateDatabaseW(
+ this._sessionId,
+ this._dbPath,
+ null,
+ this._dbId.address(),
+ 0
+ );
+ this._opened = this._attached = true;
+
+ for (let [tableName, data] of tables) {
+ let { rows, columns } = data;
+ let tableCreationWrapper = createTableCreationWrapper(
+ tableName,
+ columns
+ );
+ ESE.CreateTableColumnIndexW(
+ this._sessionId,
+ this._dbId,
+ tableCreationWrapper.table.address()
+ );
+ this._tableId = tableCreationWrapper.table.tableid;
+
+ let columnIdMap = new Map();
+ if (rows.length) {
+ // Iterate over the struct we passed into ESENT because they have the
+ // created column ids.
+ let columnCount = ctypes.UInt64.lo(
+ tableCreationWrapper.table.cColumns
+ );
+ let columnsPassed = tableCreationWrapper.table.rgcolumncreate;
+ for (let i = 0; i < columnCount; i++) {
+ let column = columnsPassed.contents;
+ columnIdMap.set(column.szColumnName.readString(), column);
+ columnsPassed = columnsPassed.increment();
+ }
+ ESE.ManualMove(
+ this._sessionId,
+ this._tableId,
+ -2147483648 /* JET_MoveFirst */,
+ 0
+ );
+ ESE.BeginTransaction(this._sessionId);
+ for (let row of rows) {
+ ESE.PrepareUpdate(
+ this._sessionId,
+ this._tableId,
+ 0 /* JET_prepInsert */
+ );
+ for (let columnName in row) {
+ let col = columnIdMap.get(columnName);
+ let colId = col.columnid;
+ let [val, valSize] = convertValueForWriting(
+ row[columnName],
+ col.coltyp
+ );
+ /* JET_bitSetOverwriteLV */
+ ESE.SetColumn(
+ this._sessionId,
+ this._tableId,
+ colId,
+ val.address(),
+ valSize,
+ 4,
+ null
+ );
+ }
+ let actualBookmarkSize = new ctypes.unsigned_long();
+ ESE.Update(
+ this._sessionId,
+ this._tableId,
+ null,
+ 0,
+ actualBookmarkSize.address()
+ );
+ }
+ ESE.CommitTransaction(
+ this._sessionId,
+ 0 /* JET_bitWaitLastLevel0Commit */
+ );
+ }
+ }
+ } finally {
+ try {
+ this._close();
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ },
+
+ _close() {
+ if (this._tableId) {
+ ESE.FailSafeCloseTable(this._sessionId, this._tableId);
+ delete this._tableId;
+ }
+ if (this._opened) {
+ ESE.FailSafeCloseDatabase(this._sessionId, this._dbId, 0);
+ this._opened = false;
+ }
+ if (this._attached) {
+ ESE.FailSafeDetachDatabaseW(this._sessionId, this._dbPath);
+ this._attached = false;
+ }
+ if (this._sessionCreated) {
+ ESE.FailSafeEndSession(this._sessionId, 0);
+ this._sessionCreated = false;
+ }
+ if (this._instanceCreated) {
+ ESE.FailSafeTerm(this._instanceId);
+ this._instanceCreated = false;
+ }
+ },
+};
+
+add_task(async function () {
+ let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tempFile.append("fx-xpcshell-edge-db");
+ tempFile.createUnique(tempFile.DIRECTORY_TYPE, 0o600);
+
+ let db = tempFile.clone();
+ db.append("spartan.edb");
+
+ let logs = tempFile.clone();
+ logs.append("LogFiles");
+ logs.create(tempFile.DIRECTORY_TYPE, 0o600);
+
+ let creationDate = new Date(Date.now() - 5000);
+ const kEdgeMenuParent = "62d07e2b-5f0d-4e41-8426-5f5ec9717beb";
+ let bookmarkReferenceItems = [
+ {
+ URL: "http://www.mozilla.org/",
+ Title: "Mozilla",
+ DateUpdated: new Date(creationDate.valueOf() + 100),
+ ItemId: "1c00c10a-15f6-4618-92dd-22575102a4da",
+ ParentId: kEdgeMenuParent,
+ IsFolder: false,
+ IsDeleted: false,
+ },
+ {
+ Title: "Folder",
+ DateUpdated: new Date(creationDate.valueOf() + 200),
+ ItemId: "564b21f2-05d6-4f7d-8499-304d00ccc3aa",
+ ParentId: kEdgeMenuParent,
+ IsFolder: true,
+ IsDeleted: false,
+ },
+ {
+ Title: "Item in folder",
+ URL: "http://www.iteminfolder.org/",
+ DateUpdated: new Date(creationDate.valueOf() + 300),
+ ItemId: "c295ddaf-04a1-424a-866c-0ebde011e7c8",
+ ParentId: "564b21f2-05d6-4f7d-8499-304d00ccc3aa",
+ IsFolder: false,
+ IsDeleted: false,
+ },
+ {
+ Title: "Deleted folder",
+ DateUpdated: new Date(creationDate.valueOf() + 400),
+ ItemId: "a547573c-4d4d-4406-a736-5b5462d93bca",
+ ParentId: kEdgeMenuParent,
+ IsFolder: true,
+ IsDeleted: true,
+ },
+ {
+ Title: "Deleted item",
+ URL: "http://www.deleteditem.org/",
+ DateUpdated: new Date(creationDate.valueOf() + 500),
+ ItemId: "37a574bb-b44b-4bbc-a414-908615536435",
+ ParentId: kEdgeMenuParent,
+ IsFolder: false,
+ IsDeleted: true,
+ },
+ {
+ Title: "Item in deleted folder (should be in root)",
+ URL: "http://www.itemindeletedfolder.org/",
+ DateUpdated: new Date(creationDate.valueOf() + 600),
+ ItemId: "74dd1cc3-4c5d-471f-bccc-7bc7c72fa621",
+ ParentId: "a547573c-4d4d-4406-a736-5b5462d93bca",
+ IsFolder: false,
+ IsDeleted: false,
+ },
+ {
+ Title: "_Favorites_Bar_",
+ DateUpdated: new Date(creationDate.valueOf() + 700),
+ ItemId: "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf",
+ ParentId: kEdgeMenuParent,
+ IsFolder: true,
+ IsDeleted: false,
+ },
+ {
+ Title: "Item in favorites bar",
+ URL: "http://www.iteminfavoritesbar.org/",
+ DateUpdated: new Date(creationDate.valueOf() + 800),
+ ItemId: "9f2b1ff8-b651-46cf-8f41-16da8bcb6791",
+ ParentId: "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf",
+ IsFolder: false,
+ IsDeleted: false,
+ },
+ ];
+
+ let readingListReferenceItems = [
+ {
+ Title: "Some mozilla page",
+ URL: "http://www.mozilla.org/somepage/",
+ AddedDate: new Date(creationDate.valueOf() + 900),
+ ItemId: "c88426fd-52a7-419d-acbc-d2310e8afebe",
+ IsDeleted: false,
+ },
+ {
+ Title: "Some other page",
+ URL: "https://www.example.org/somepage/",
+ AddedDate: new Date(creationDate.valueOf() + 1000),
+ ItemId: "a35fc843-5d5a-4d1e-9be8-45214be24b5c",
+ IsDeleted: false,
+ },
+ ];
+
+ // The following entries are expected to be skipped as being too old to
+ // migrate.
+ let expiredTypedURLsReferenceItems = [
+ {
+ URL: "https://expired1.invalid/",
+ AccessDateTimeUTC: dateDaysAgo(500),
+ },
+ {
+ URL: "https://expired2.invalid/",
+ AccessDateTimeUTC: dateDaysAgo(300),
+ },
+ {
+ URL: "https://expired3.invalid/",
+ AccessDateTimeUTC: dateDaysAgo(190),
+ },
+ ];
+
+ // The following entries should be new enough to migrate.
+ let unexpiredTypedURLsReferenceItems = [
+ {
+ URL: "https://unexpired1.invalid/",
+ AccessDateTimeUTC: dateDaysAgo(179),
+ },
+ {
+ URL: "https://unexpired2.invalid/",
+ AccessDateTimeUTC: dateDaysAgo(50),
+ },
+ {
+ URL: "https://unexpired3.invalid/",
+ },
+ ];
+
+ let typedURLsReferenceItems = [
+ ...expiredTypedURLsReferenceItems,
+ ...unexpiredTypedURLsReferenceItems,
+ ];
+
+ Assert.ok(
+ MigrationUtils.HISTORY_MAX_AGE_IN_DAYS < 300,
+ "This test expects the current pref to be less than the youngest expired visit."
+ );
+ Assert.ok(
+ MigrationUtils.HISTORY_MAX_AGE_IN_DAYS > 160,
+ "This test expects the current pref to be greater than the oldest unexpired visit."
+ );
+
+ eseDBWritingHelpers.setupDB(
+ db,
+ new Map([
+ [
+ "Favorites",
+ {
+ columns: [
+ { type: COLUMN_TYPES.JET_coltypLongText, name: "URL", cbMax: 4096 },
+ {
+ type: COLUMN_TYPES.JET_coltypLongText,
+ name: "Title",
+ cbMax: 4096,
+ },
+ { type: COLUMN_TYPES.JET_coltypLongLong, name: "DateUpdated" },
+ { type: COLUMN_TYPES.JET_coltypGUID, name: "ItemId" },
+ { type: COLUMN_TYPES.JET_coltypBit, name: "IsDeleted" },
+ { type: COLUMN_TYPES.JET_coltypBit, name: "IsFolder" },
+ { type: COLUMN_TYPES.JET_coltypGUID, name: "ParentId" },
+ ],
+ rows: bookmarkReferenceItems,
+ },
+ ],
+ [
+ "ReadingList",
+ {
+ columns: [
+ { type: COLUMN_TYPES.JET_coltypLongText, name: "URL", cbMax: 4096 },
+ {
+ type: COLUMN_TYPES.JET_coltypLongText,
+ name: "Title",
+ cbMax: 4096,
+ },
+ { type: COLUMN_TYPES.JET_coltypLongLong, name: "AddedDate" },
+ { type: COLUMN_TYPES.JET_coltypGUID, name: "ItemId" },
+ { type: COLUMN_TYPES.JET_coltypBit, name: "IsDeleted" },
+ ],
+ rows: readingListReferenceItems,
+ },
+ ],
+ [
+ "TypedURLs",
+ {
+ columns: [
+ { type: COLUMN_TYPES.JET_coltypLongText, name: "URL", cbMax: 4096 },
+ {
+ type: COLUMN_TYPES.JET_coltypLongLong,
+ name: "AccessDateTimeUTC",
+ },
+ ],
+ rows: typedURLsReferenceItems,
+ },
+ ],
+ ])
+ );
+
+ // Manually create an EdgeProfileMigrator rather than going through
+ // MigrationUtils.getMigrator to avoid the user data availability check, since
+ // we're mocking out that stuff.
+ let migrator = new EdgeProfileMigrator();
+ let bookmarksMigrator = migrator.getBookmarksMigratorForTesting(db);
+ Assert.ok(bookmarksMigrator.exists, "Should recognize db we just created");
+
+ let seenBookmarks = [];
+ let listener = events => {
+ for (let event of events) {
+ let {
+ id,
+ itemType,
+ url,
+ title,
+ dateAdded,
+ guid,
+ index,
+ parentGuid,
+ parentId,
+ } = event;
+ if (title.startsWith("Deleted")) {
+ ok(false, "Should not see deleted items being bookmarked!");
+ }
+ seenBookmarks.push({
+ id,
+ parentId,
+ index,
+ itemType,
+ url,
+ title,
+ dateAdded,
+ guid,
+ parentGuid,
+ });
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+
+ let migrateResult = await new Promise(resolve =>
+ bookmarksMigrator.migrate(resolve)
+ ).catch(ex => {
+ console.error(ex);
+ Assert.ok(false, "Got an exception trying to migrate data! " + ex);
+ return false;
+ });
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+ Assert.ok(migrateResult, "Migration should succeed");
+ Assert.equal(
+ seenBookmarks.length,
+ 5,
+ "Should have seen 5 items being bookmarked."
+ );
+ Assert.equal(
+ seenBookmarks.length,
+ MigrationUtils._importQuantities.bookmarks,
+ "Telemetry should have items"
+ );
+
+ let menuParents = seenBookmarks.filter(
+ item => item.parentGuid == PlacesUtils.bookmarks.menuGuid
+ );
+ Assert.equal(
+ menuParents.length,
+ 3,
+ "Bookmarks are added to the menu without a folder"
+ );
+ let toolbarParents = seenBookmarks.filter(
+ item => item.parentGuid == PlacesUtils.bookmarks.toolbarGuid
+ );
+ Assert.equal(
+ toolbarParents.length,
+ 1,
+ "Should have a single item added to the toolbar"
+ );
+ let menuParentGuid = PlacesUtils.bookmarks.menuGuid;
+ let toolbarParentGuid = PlacesUtils.bookmarks.toolbarGuid;
+
+ let expectedTitlesInMenu = bookmarkReferenceItems
+ .filter(item => item.ParentId == kEdgeMenuParent)
+ .map(item => item.Title);
+ // Hacky, but seems like much the simplest way:
+ expectedTitlesInMenu.push("Item in deleted folder (should be in root)");
+ let expectedTitlesInToolbar = bookmarkReferenceItems
+ .filter(item => item.ParentId == "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf")
+ .map(item => item.Title);
+
+ for (let bookmark of seenBookmarks) {
+ let shouldBeInMenu = expectedTitlesInMenu.includes(bookmark.title);
+ let shouldBeInToolbar = expectedTitlesInToolbar.includes(bookmark.title);
+ if (bookmark.title == "Folder") {
+ Assert.equal(
+ bookmark.itemType,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ "Bookmark " + bookmark.title + " should be a folder"
+ );
+ } else {
+ Assert.notEqual(
+ bookmark.itemType,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ "Bookmark " + bookmark.title + " should not be a folder"
+ );
+ }
+
+ if (shouldBeInMenu) {
+ Assert.equal(
+ bookmark.parentGuid,
+ menuParentGuid,
+ "Item '" + bookmark.title + "' should be in menu"
+ );
+ } else if (shouldBeInToolbar) {
+ Assert.equal(
+ bookmark.parentGuid,
+ toolbarParentGuid,
+ "Item '" + bookmark.title + "' should be in toolbar"
+ );
+ } else if (
+ bookmark.guid == menuParentGuid ||
+ bookmark.guid == toolbarParentGuid
+ ) {
+ Assert.ok(
+ true,
+ "Expect toolbar and menu folders to not be in menu or toolbar"
+ );
+ } else {
+ // Bit hacky, but we do need to check this.
+ Assert.equal(
+ bookmark.title,
+ "Item in folder",
+ "Subfoldered item shouldn't be in menu or toolbar"
+ );
+ let parent = seenBookmarks.find(
+ maybeParent => maybeParent.guid == bookmark.parentGuid
+ );
+ Assert.equal(
+ parent && parent.title,
+ "Folder",
+ "Subfoldered item should be in subfolder labeled 'Folder'"
+ );
+ }
+
+ let dbItem = bookmarkReferenceItems.find(
+ someItem => bookmark.title == someItem.Title
+ );
+ if (!dbItem) {
+ Assert.ok(
+ [menuParentGuid, toolbarParentGuid].includes(bookmark.guid),
+ "This item should be one of the containers"
+ );
+ } else {
+ Assert.equal(dbItem.URL || "", bookmark.url, "URL is correct");
+ Assert.equal(
+ dbItem.DateUpdated.valueOf(),
+ new Date(bookmark.dateAdded).valueOf(),
+ "Date added is correct"
+ );
+ }
+ }
+
+ MigrationUtils._importQuantities.bookmarks = 0;
+ seenBookmarks = [];
+ listener = events => {
+ for (let event of events) {
+ let {
+ id,
+ itemType,
+ url,
+ title,
+ dateAdded,
+ guid,
+ index,
+ parentGuid,
+ parentId,
+ } = event;
+ seenBookmarks.push({
+ id,
+ parentId,
+ index,
+ itemType,
+ url,
+ title,
+ dateAdded,
+ guid,
+ parentGuid,
+ });
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+
+ let readingListMigrator = migrator.getReadingListMigratorForTesting(db);
+ Assert.ok(readingListMigrator.exists, "Should recognize db we just created");
+ migrateResult = await new Promise(resolve =>
+ readingListMigrator.migrate(resolve)
+ ).catch(ex => {
+ console.error(ex);
+ Assert.ok(false, "Got an exception trying to migrate data! " + ex);
+ return false;
+ });
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+ Assert.ok(migrateResult, "Migration should succeed");
+ Assert.equal(
+ seenBookmarks.length,
+ 3,
+ "Should have seen 3 items being bookmarked (2 items + 1 folder)."
+ );
+ Assert.equal(
+ seenBookmarks.length,
+ MigrationUtils._importQuantities.bookmarks,
+ "Telemetry should have items"
+ );
+ let readingListContainerLabel = await MigrationUtils.getLocalizedString(
+ "migration-imported-edge-reading-list"
+ );
+
+ for (let bookmark of seenBookmarks) {
+ if (readingListContainerLabel == bookmark.title) {
+ continue;
+ }
+ let referenceItem = readingListReferenceItems.find(
+ item => item.Title == bookmark.title
+ );
+ Assert.ok(referenceItem, "Should have imported what we expected");
+ Assert.equal(referenceItem.URL, bookmark.url, "Should have the right URL");
+ readingListReferenceItems.splice(
+ readingListReferenceItems.findIndex(item => item.Title == bookmark.title),
+ 1
+ );
+ }
+ Assert.ok(
+ !readingListReferenceItems.length,
+ "Should have seen all expected items."
+ );
+
+ let historyDBMigrator = migrator.getHistoryDBMigratorForTesting(db);
+ await new Promise(resolve => {
+ historyDBMigrator.migrate(resolve);
+ });
+ Assert.ok(true, "History DB migration done!");
+ for (let expiredEntry of expiredTypedURLsReferenceItems) {
+ let entry = await PlacesUtils.history.fetch(expiredEntry.URL, {
+ includeVisits: true,
+ });
+ Assert.equal(entry, null, "Should not have found an entry.");
+ }
+
+ for (let unexpiredEntry of unexpiredTypedURLsReferenceItems) {
+ let entry = await PlacesUtils.history.fetch(unexpiredEntry.URL, {
+ includeVisits: true,
+ });
+ Assert.equal(entry.url, unexpiredEntry.URL, "Should have the correct URL");
+ Assert.ok(!!entry.visits.length, "Should have some visits");
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_Edge_registry_migration.js b/browser/components/migration/tests/unit/test_Edge_registry_migration.js
new file mode 100644
index 0000000000..2a400f7858
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Edge_registry_migration.js
@@ -0,0 +1,81 @@
+/* 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 { EdgeProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/EdgeProfileMigrator.sys.mjs"
+);
+const { MSMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/MSMigrationUtils.sys.mjs"
+);
+
+/**
+ * Tests that history visits loaded from the registry from Edge (EdgeHTML)
+ * that have a visit date older than maxAgeInDays days do not get imported.
+ */
+add_task(async function test_Edge_history_past_max_days() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ Assert.ok(
+ MigrationUtils.HISTORY_MAX_AGE_IN_DAYS < 300,
+ "This test expects the current pref to be less than the youngest expired visit."
+ );
+ Assert.ok(
+ MigrationUtils.HISTORY_MAX_AGE_IN_DAYS > 160,
+ "This test expects the current pref to be greater than the oldest unexpired visit."
+ );
+
+ const EXPIRED_VISITS = [
+ ["https://test1.invalid/", dateDaysAgo(500).getTime() * 1000],
+ ["https://test2.invalid/", dateDaysAgo(450).getTime() * 1000],
+ ["https://test3.invalid/", dateDaysAgo(300).getTime() * 1000],
+ ];
+
+ const UNEXPIRED_VISITS = [
+ ["https://test4.invalid/"],
+ ["https://test5.invalid/", dateDaysAgo(160).getTime() * 1000],
+ ["https://test6.invalid/", dateDaysAgo(50).getTime() * 1000],
+ ["https://test7.invalid/", dateDaysAgo(0).getTime() * 1000],
+ ];
+
+ const ALL_VISITS = [...EXPIRED_VISITS, ...UNEXPIRED_VISITS];
+
+ // Fake out the getResources method of the migrator so that we return
+ // a single fake MigratorResource per availableResourceType.
+ sandbox.stub(MSMigrationUtils, "getTypedURLs").callsFake(() => {
+ return new Map(ALL_VISITS);
+ });
+
+ // Manually create an EdgeProfileMigrator rather than going through
+ // MigrationUtils.getMigrator to avoid the user data availability check, since
+ // we're mocking out that stuff.
+ let migrator = new EdgeProfileMigrator();
+ let registryTypedHistoryMigrator =
+ migrator.getHistoryRegistryMigratorForTesting();
+ await new Promise(resolve => {
+ registryTypedHistoryMigrator.migrate(resolve);
+ });
+ Assert.ok(true, "History from registry migration done!");
+
+ for (let expiredEntry of EXPIRED_VISITS) {
+ let entry = await PlacesUtils.history.fetch(expiredEntry[0], {
+ includeVisits: true,
+ });
+ Assert.equal(entry, null, "Should not have found an entry.");
+ }
+
+ for (let unexpiredEntry of UNEXPIRED_VISITS) {
+ let entry = await PlacesUtils.history.fetch(unexpiredEntry[0], {
+ includeVisits: true,
+ });
+ Assert.equal(entry.url, unexpiredEntry[0], "Should have the correct URL");
+ Assert.ok(!!entry.visits.length, "Should have some visits");
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_IE_bookmarks.js b/browser/components/migration/tests/unit/test_IE_bookmarks.js
new file mode 100644
index 0000000000..9816bb16e3
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_IE_bookmarks.js
@@ -0,0 +1,30 @@
+"use strict";
+
+add_task(async function () {
+ let migrator = await MigrationUtils.getMigrator("ie");
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable(), "Check migrator source");
+
+ // Since this test doesn't mock out the favorites, execution is dependent
+ // on the actual favorites stored on the local machine's IE favorites database.
+ // As such, we can't assert that bookmarks were migrated to both the bookmarks
+ // menu and the bookmarks toolbar.
+ let itemCount = 0;
+ let listener = events => {
+ for (let event of events) {
+ if (event.itemType == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
+ info("bookmark added: " + event.parentGuid);
+ itemCount++;
+ }
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+ Assert.equal(
+ MigrationUtils._importQuantities.bookmarks,
+ itemCount,
+ "Ensure telemetry matches actual number of imported items."
+ );
+});
diff --git a/browser/components/migration/tests/unit/test_IE_history.js b/browser/components/migration/tests/unit/test_IE_history.js
new file mode 100644
index 0000000000..f9a1e719a2
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_IE_history.js
@@ -0,0 +1,187 @@
+"use strict";
+
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+// These match what we add to IE via InsertIEHistory.exe.
+const TEST_ENTRIES = [
+ {
+ url: "http://www.mozilla.org/1",
+ title: "Mozilla HTTP Test",
+ },
+ {
+ url: "https://www.mozilla.org/2",
+ // Test character encoding with a fox emoji:
+ title: "Mozilla HTTPS Test 🦊",
+ },
+];
+
+function insertIEHistory() {
+ let file = do_get_file("InsertIEHistory.exe", false);
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(file);
+
+ let args = [];
+ process.run(true, args, args.length);
+
+ Assert.ok(!process.isRunning, "Should be done running");
+ Assert.equal(process.exitValue, 0, "Check exit code");
+}
+
+add_task(async function setup() {
+ await PlacesUtils.history.clear();
+
+ insertIEHistory();
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function test_IE_history() {
+ let migrator = await MigrationUtils.getMigrator("ie");
+ Assert.ok(await migrator.isSourceAvailable(), "Source is available");
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY);
+
+ for (let { url, title } of TEST_ENTRIES) {
+ let entry = await PlacesUtils.history.fetch(url, { includeVisits: true });
+ Assert.equal(entry.url, url, "Should have the correct URL");
+ Assert.equal(entry.title, title, "Should have the correct title");
+ Assert.ok(!!entry.visits.length, "Should have some visits");
+ }
+
+ await PlacesUtils.history.clear();
+});
+
+/**
+ * Tests that history visits from IE that have a visit date older than
+ * maxAgeInDays days do not get imported.
+ */
+add_task(async function test_IE_history_past_max_days() {
+ // The InsertIEHistory program inserts two history visits using the MS COM
+ // IUrlHistoryStg interface. That interface does not allow us to dictate
+ // the visit times of those history visits. Thankfully, we can temporarily
+ // mock out the @mozilla.org/profile/migrator/iehistoryenumerator;1 to return
+ // some entries that we expect to expire.
+
+ /**
+ * An implmentation of nsISimpleEnumerator that wraps a JavaScript Array.
+ */
+ class nsSimpleEnumerator {
+ #items;
+ #nextIndex;
+
+ constructor(items) {
+ this.#items = items;
+ this.#nextIndex = 0;
+ }
+
+ hasMoreElements() {
+ return this.#nextIndex < this.#items.length;
+ }
+
+ getNext() {
+ if (!this.hasMoreElements()) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ return this.#items[this.#nextIndex++];
+ }
+
+ [Symbol.iterator]() {
+ return this.#items.values();
+ }
+
+ QueryInterface = ChromeUtils.generateQI(["nsISimpleEnumerator"]);
+ }
+
+ Assert.ok(
+ MigrationUtils.HISTORY_MAX_AGE_IN_DAYS < 300,
+ "This test expects the current pref to be less than the youngest expired visit."
+ );
+ Assert.ok(
+ MigrationUtils.HISTORY_MAX_AGE_IN_DAYS > 160,
+ "This test expects the current pref to be greater than the oldest unexpired visit."
+ );
+
+ const EXPIRED_VISITS = [
+ new Map([
+ ["uri", Services.io.newURI("https://test1.invalid")],
+ ["title", "Test history visit 1"],
+ ["time", PRTimeDaysAgo(500)],
+ ]),
+ new Map([
+ ["uri", Services.io.newURI("https://test2.invalid")],
+ ["title", "Test history visit 2"],
+ ["time", PRTimeDaysAgo(450)],
+ ]),
+ new Map([
+ ["uri", Services.io.newURI("https://test3.invalid")],
+ ["title", "Test history visit 3"],
+ ["time", PRTimeDaysAgo(300)],
+ ]),
+ ];
+
+ const UNEXPIRED_VISITS = [
+ new Map([
+ ["uri", Services.io.newURI("https://test4.invalid")],
+ ["title", "Test history visit 4"],
+ ]),
+ new Map([
+ ["uri", Services.io.newURI("https://test5.invalid")],
+ ["title", "Test history visit 5"],
+ ["time", PRTimeDaysAgo(160)],
+ ]),
+ new Map([
+ ["uri", Services.io.newURI("https://test6.invalid")],
+ ["title", "Test history visit 6"],
+ ["time", PRTimeDaysAgo(50)],
+ ]),
+ new Map([
+ ["uri", Services.io.newURI("https://test7.invalid")],
+ ["title", "Test history visit 7"],
+ ["time", PRTimeDaysAgo(0)],
+ ]),
+ ];
+
+ let fakeIEHistoryEnumerator = MockRegistrar.register(
+ "@mozilla.org/profile/migrator/iehistoryenumerator;1",
+ new nsSimpleEnumerator([...EXPIRED_VISITS, ...UNEXPIRED_VISITS])
+ );
+ registerCleanupFunction(() => {
+ MockRegistrar.unregister(fakeIEHistoryEnumerator);
+ });
+
+ let migrator = await MigrationUtils.getMigrator("ie");
+ Assert.ok(await migrator.isSourceAvailable(), "Source is available");
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY);
+
+ for (let visit of EXPIRED_VISITS) {
+ let entry = await PlacesUtils.history.fetch(visit.get("uri").spec, {
+ includeVisits: true,
+ });
+ Assert.equal(entry, null, "Should not have found an entry.");
+ }
+
+ for (let visit of UNEXPIRED_VISITS) {
+ let entry = await PlacesUtils.history.fetch(visit.get("uri"), {
+ includeVisits: true,
+ });
+ Assert.equal(
+ entry.url,
+ visit.get("uri").spec,
+ "Should have the correct URL"
+ );
+ Assert.equal(
+ entry.title,
+ visit.get("title"),
+ "Should have the correct title"
+ );
+ Assert.ok(!!entry.visits.length, "Should have some visits");
+ }
+
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/migration/tests/unit/test_MigrationUtils_timedRetry.js b/browser/components/migration/tests/unit/test_MigrationUtils_timedRetry.js
new file mode 100644
index 0000000000..83748d870d
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_MigrationUtils_timedRetry.js
@@ -0,0 +1,29 @@
+"use strict";
+
+let tmpFile = FileUtils.getDir("TmpD", []);
+let dbConn;
+
+add_task(async function setup() {
+ tmpFile.append("TestDB");
+ dbConn = await Sqlite.openConnection({ path: tmpFile.path });
+
+ registerCleanupFunction(async () => {
+ await dbConn.close();
+ await IOUtils.remove(tmpFile.path);
+ });
+});
+
+add_task(async function testgetRowsFromDBWithoutLocksRetries() {
+ let deferred = Promise.withResolvers();
+ let promise = MigrationUtils.getRowsFromDBWithoutLocks(
+ tmpFile.path,
+ "Temp DB",
+ "SELECT * FROM moz_temp_table",
+ deferred.promise
+ );
+ await new Promise(resolve => do_timeout(50, resolve));
+ dbConn
+ .execute("CREATE TABLE moz_temp_table (id INTEGER PRIMARY KEY)")
+ .then(deferred.resolve);
+ await promise;
+});
diff --git a/browser/components/migration/tests/unit/test_PasswordFileMigrator.js b/browser/components/migration/tests/unit/test_PasswordFileMigrator.js
new file mode 100644
index 0000000000..e22f207c5d
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_PasswordFileMigrator.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PasswordFileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/FileMigrators.sys.mjs"
+);
+const { LoginCSVImport } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginCSVImport.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const { MigrationWizardConstants } = ChromeUtils.importESModule(
+ "chrome://browser/content/migration/migration-wizard-constants.mjs"
+);
+
+add_setup(async function () {
+ Services.prefs.setBoolPref("signon.management.page.fileImport.enabled", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.management.page.fileImport.enabled");
+ });
+});
+
+/**
+ * Tests that the PasswordFileMigrator properly subclasses FileMigratorBase
+ * and delegates to the LoginCSVImport module.
+ */
+add_task(async function test_PasswordFileMigrator() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = new PasswordFileMigrator();
+ Assert.ok(
+ migrator.constructor.key,
+ "PasswordFileMigrator implements static getter 'key'"
+ );
+ Assert.ok(
+ migrator.constructor.displayNameL10nID,
+ "PasswordFileMigrator implements static getter 'displayNameL10nID'"
+ );
+ Assert.ok(
+ await migrator.getFilePickerConfig(),
+ "PasswordFileMigrator returns something for getFilePickerConfig()"
+ );
+ Assert.ok(
+ migrator.displayedResourceTypes,
+ "PasswordFileMigrator returns something for displayedResourceTypes"
+ );
+ Assert.ok(migrator.enabled, "PasswordFileMigrator is enabled.");
+
+ const IMPORT_SUMMARY = [
+ {
+ result: "added",
+ },
+ {
+ result: "added",
+ },
+ {
+ result: "modified",
+ },
+ ];
+ const EXPECTED_SUCCESS_STATE = {
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_NEW]:
+ "2 added",
+ [MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES.PASSWORDS_UPDATED]:
+ "1 updated",
+ };
+ const FAKE_PATH = "some/fake/path.csv";
+
+ let importFromCSVStub = sandbox
+ .stub(LoginCSVImport, "importFromCSV")
+ .callsFake(somePath => {
+ Assert.equal(somePath, FAKE_PATH, "Got expected path");
+ return Promise.resolve(IMPORT_SUMMARY);
+ });
+ let result = await migrator.migrate(FAKE_PATH);
+
+ Assert.ok(importFromCSVStub.called, "The stub should have been called.");
+ Assert.deepEqual(
+ result,
+ EXPECTED_SUCCESS_STATE,
+ "Got back the expected success state."
+ );
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that the PasswordFileMigrator will throw an exception with a
+ * consistent error message if the LoginCSVImport function rejects.
+ */
+add_task(async function test_PasswordFileMigrator_exception() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = new PasswordFileMigrator();
+
+ const FAKE_PATH = "some/fake/path.csv";
+
+ sandbox.stub(LoginCSVImport, "importFromCSV").callsFake(() => {
+ return Promise.reject("Some error");
+ });
+
+ await Assert.rejects(
+ migrator.migrate(FAKE_PATH),
+ /The file doesn’t include any valid password data/
+ );
+
+ sandbox.restore();
+});
diff --git a/browser/components/migration/tests/unit/test_Safari_bookmarks.js b/browser/components/migration/tests/unit/test_Safari_bookmarks.js
new file mode 100644
index 0000000000..85be9f0049
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Safari_bookmarks.js
@@ -0,0 +1,85 @@
+"use strict";
+
+const { CustomizableUI } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizableUI.sys.mjs"
+);
+
+add_task(async function () {
+ registerFakePath("ULibDir", do_get_file("Library/"));
+ const faviconPath = do_get_file(
+ "Library/Safari/Favicon Cache/favicons.db"
+ ).path;
+
+ let migrator = await MigrationUtils.getMigrator("safari");
+ // Sanity check for the source.
+ Assert.ok(await migrator.isSourceAvailable());
+
+ // Wait for the imported bookmarks. We don't check that "From Safari"
+ // folders are created on the toolbar since the profile
+ // we're importing to has less than 3 bookmarks in the destination
+ // so a "From Safari" folder isn't created.
+ let expectedParentGuids = [PlacesUtils.bookmarks.toolbarGuid];
+ let itemCount = 0;
+
+ let gotFolder = false;
+ let listener = events => {
+ for (let event of events) {
+ itemCount++;
+ if (
+ event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER &&
+ event.title == "Food and Travel"
+ ) {
+ gotFolder = true;
+ }
+ if (expectedParentGuids.length) {
+ let index = expectedParentGuids.indexOf(event.parentGuid);
+ Assert.ok(index != -1, "Found expected parent");
+ expectedParentGuids.splice(index, 1);
+ }
+ }
+ };
+ PlacesUtils.observers.addListener(["bookmark-added"], listener);
+ let observerNotified = false;
+ Services.obs.addObserver((aSubject, aTopic, aData) => {
+ let [toolbar, visibility] = JSON.parse(aData);
+ Assert.equal(
+ toolbar,
+ CustomizableUI.AREA_BOOKMARKS,
+ "Notification should be received for bookmarks toolbar"
+ );
+ Assert.equal(
+ visibility,
+ "true",
+ "Notification should say to reveal the bookmarks toolbar"
+ );
+ observerNotified = true;
+ }, "browser-set-toolbar-visibility");
+
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
+ PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+
+ // Check the bookmarks have been imported to all the expected parents.
+ Assert.ok(!expectedParentGuids.length, "No more expected parents");
+ Assert.ok(gotFolder, "Should have seen the folder get imported");
+ Assert.equal(itemCount, 14, "Should import all 14 items.");
+ // Check that the telemetry matches:
+ Assert.equal(
+ MigrationUtils._importQuantities.bookmarks,
+ itemCount,
+ "Telemetry reporting correct."
+ );
+
+ // Check that favicons migrated
+ let faviconURIs = await MigrationUtils.getRowsFromDBWithoutLocks(
+ faviconPath,
+ "Safari Bookmark Favicons",
+ `SELECT I.uuid, I.url AS favicon_url, P.url
+ FROM icon_info I
+ INNER JOIN page_url P ON I.uuid = P.uuid;`
+ );
+ let pageUrls = Array.from(faviconURIs, row =>
+ Services.io.newURI(row.getResultByName("url"))
+ );
+ await assertFavicons(pageUrls);
+ Assert.ok(observerNotified, "The observer should be notified upon migration");
+});
diff --git a/browser/components/migration/tests/unit/test_Safari_history.js b/browser/components/migration/tests/unit/test_Safari_history.js
new file mode 100644
index 0000000000..c5b1210073
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Safari_history.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const HISTORY_TEMPLATE_FILE_PATH = "Library/Safari/HistoryTemplate.db";
+const HISTORY_FILE_PATH = "Library/Safari/History.db";
+
+// We want this to be some recent time, so we'll always add some time to our
+// dates to keep them ~ five days ago.
+const MS_FROM_REFERENCE_TIME =
+ new Date() - new Date("May 31, 2023 00:00:00 UTC");
+
+const TEST_URLS = [
+ {
+ url: "http://example.com/",
+ title: "Example Domain",
+ time: 706743588.04751,
+ jsTime: 1685050788047 + MS_FROM_REFERENCE_TIME,
+ visits: 1,
+ },
+ {
+ url: "http://mozilla.org/",
+ title: "",
+ time: 706743581.133386,
+ jsTime: 1685050781133 + MS_FROM_REFERENCE_TIME,
+ visits: 1,
+ },
+ {
+ url: "https://www.mozilla.org/en-CA/",
+ title: "Internet for people, not profit - Mozilla",
+ time: 706743581.133679,
+ jsTime: 1685050781133 + MS_FROM_REFERENCE_TIME,
+ visits: 1,
+ },
+];
+
+async function setupHistoryFile() {
+ removeHistoryFile();
+ let file = do_get_file(HISTORY_TEMPLATE_FILE_PATH);
+ file.copyTo(file.parent, "History.db");
+ await updateVisitTimes();
+}
+
+function removeHistoryFile() {
+ let file = do_get_file(HISTORY_FILE_PATH, true);
+ try {
+ file.remove(false);
+ } catch (ex) {
+ // It is ok if this doesn't exist.
+ if (ex.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
+ throw ex;
+ }
+ }
+}
+
+async function updateVisitTimes() {
+ let cocoaDifference = MS_FROM_REFERENCE_TIME / 1000;
+ let historyFile = do_get_file(HISTORY_FILE_PATH);
+ let dbConn = await Sqlite.openConnection({ path: historyFile.path });
+ await dbConn.execute(
+ "UPDATE history_visits SET visit_time = visit_time + :difference;",
+ { difference: cocoaDifference }
+ );
+ await dbConn.close();
+}
+
+add_setup(async function setup() {
+ registerFakePath("ULibDir", do_get_file("Library/"));
+ await setupHistoryFile();
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ removeHistoryFile();
+ });
+});
+
+add_task(async function testHistoryImport() {
+ await PlacesUtils.history.clear();
+
+ let migrator = await MigrationUtils.getMigrator("safari");
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY);
+
+ for (let urlInfo of TEST_URLS) {
+ let entry = await PlacesUtils.history.fetch(urlInfo.url, {
+ includeVisits: true,
+ });
+
+ Assert.equal(entry.url, urlInfo.url, "Should have the correct URL");
+ Assert.equal(entry.title, urlInfo.title, "Should have the correct title");
+ Assert.equal(
+ entry.visits.length,
+ urlInfo.visits,
+ "Should have the correct number of visits"
+ );
+ Assert.equal(
+ entry.visits[0].date.getTime(),
+ urlInfo.jsTime,
+ "Should have the correct date"
+ );
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_Safari_history_strange_entries.js b/browser/components/migration/tests/unit/test_Safari_history_strange_entries.js
new file mode 100644
index 0000000000..2578353e35
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Safari_history_strange_entries.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PlacesQuery } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesQuery.sys.mjs"
+);
+
+const HISTORY_FILE_PATH = "Library/Safari/History.db";
+const HISTORY_STRANGE_ENTRIES_FILE_PATH =
+ "Library/Safari/HistoryStrangeEntries.db";
+
+// By default, our migrators will cut off migrating any history older than
+// 180 days. In order to make sure this test continues to run correctly
+// in the future, we copy the reference database to History.db, and then
+// use Sqlite.sys.mjs to connect to it and manually update all of the visit
+// times to be "now", so that they all fall within the 180 day window. The
+// Nov 10th date below is right around when the reference database visit
+// entries were created.
+//
+// This update occurs in `updateVisitTimes`.
+const MS_SINCE_SNAPSHOT_TIME =
+ new Date() - new Date("Nov 10, 2022 00:00:00 UTC");
+
+async function setupHistoryFile() {
+ removeHistoryFile();
+ let file = do_get_file(HISTORY_STRANGE_ENTRIES_FILE_PATH);
+ file.copyTo(file.parent, "History.db");
+ await updateVisitTimes();
+}
+
+function removeHistoryFile() {
+ let file = do_get_file(HISTORY_FILE_PATH, true);
+ try {
+ file.remove(false);
+ } catch (ex) {
+ // It is ok if this doesn't exist.
+ if (ex.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
+ throw ex;
+ }
+ }
+}
+
+add_setup(async function setup() {
+ registerFakePath("ULibDir", do_get_file("Library/"));
+ await setupHistoryFile();
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ removeHistoryFile();
+ });
+});
+
+async function updateVisitTimes() {
+ let cocoaSnapshotDelta = MS_SINCE_SNAPSHOT_TIME / 1000;
+ let historyFile = do_get_file(HISTORY_FILE_PATH);
+ let dbConn = await Sqlite.openConnection({ path: historyFile.path });
+
+ await dbConn.execute(
+ "UPDATE history_visits SET visit_time = visit_time + :cocoaSnapshotDelta;",
+ {
+ cocoaSnapshotDelta,
+ }
+ );
+
+ await dbConn.close();
+}
+
+/**
+ * Tests that we can import successfully from Safari when Safari's history
+ * database contains malformed URLs.
+ */
+add_task(async function testHistoryImportStrangeEntries() {
+ await PlacesUtils.history.clear();
+
+ let placesQuery = new PlacesQuery();
+ let emptyHistory = await placesQuery.getHistory();
+ Assert.equal(emptyHistory.size, 0, "Empty history should indeed be empty.");
+
+ const EXPECTED_MIGRATED_SITES = 10;
+ const EXPECTED_MIGRATED_VISTS = 23;
+
+ let historyFile = do_get_file(HISTORY_FILE_PATH);
+ let dbConn = await Sqlite.openConnection({ path: historyFile.path });
+ let [rowCountResult] = await dbConn.execute(
+ "SELECT COUNT(*) FROM history_visits"
+ );
+ Assert.greater(
+ rowCountResult.getResultByName("COUNT(*)"),
+ EXPECTED_MIGRATED_VISTS,
+ "There are more total rows than valid rows"
+ );
+ await dbConn.close();
+
+ let migrator = await MigrationUtils.getMigrator("safari");
+ await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY);
+ let migratedHistory = await placesQuery.getHistory({ sortBy: "site" });
+ let siteCount = migratedHistory.size;
+ let visitCount = 0;
+ for (let [, visits] of migratedHistory) {
+ visitCount += visits.length;
+ }
+ Assert.equal(
+ siteCount,
+ EXPECTED_MIGRATED_SITES,
+ "Should have migrated all valid history sites"
+ );
+ Assert.equal(
+ visitCount,
+ EXPECTED_MIGRATED_VISTS,
+ "Should have migrated all valid history visits"
+ );
+
+ placesQuery.close();
+});
diff --git a/browser/components/migration/tests/unit/test_Safari_permissions.js b/browser/components/migration/tests/unit/test_Safari_permissions.js
new file mode 100644
index 0000000000..eaa6c7788e
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Safari_permissions.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that if the migrator does not have the permission to
+ * read from the Safari data directory, that it can request
+ * permission to read from it, if the system allows.
+ */
+
+const { SafariProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/SafariProfileMigrator.sys.mjs"
+);
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const { MockFilePicker } = ChromeUtils.importESModule(
+ "resource://testing-common/MockFilePicker.sys.mjs"
+);
+
+let gDataDir;
+
+add_setup(async () => {
+ let tempDir = do_get_tempdir();
+ gDataDir = PathUtils.join(tempDir.path, "Safari");
+ await IOUtils.makeDirectory(gDataDir);
+
+ registerFakePath("ULibDir", tempDir);
+
+ MockFilePicker.init(globalThis);
+ registerCleanupFunction(() => {
+ MockFilePicker.cleanup();
+ });
+});
+
+/**
+ * Tests that canGetPermissions will return false if the platform does
+ * not allow for folder selection in the native file picker, and returns
+ * the data path otherwise.
+ */
+add_task(async function test_canGetPermissions() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = new SafariProfileMigrator();
+ // Not being able to get a folder picker is not a problem on macOS, but
+ // we'll test that case anyways.
+ let canGetPermissionsStub = sandbox
+ .stub(MigrationUtils, "canGetPermissionsOnPlatform")
+ .resolves(false);
+
+ Assert.ok(
+ !(await migrator.canGetPermissions()),
+ "Should not be able to get permissions."
+ );
+
+ canGetPermissionsStub.resolves(true);
+
+ Assert.equal(
+ await migrator.canGetPermissions(),
+ gDataDir,
+ "Should be able to get the permissions path."
+ );
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that getPermissions will show the native file picker in a
+ * loop until either the user cancels or selects a folder that grants
+ * read permissions to the data directory.
+ */
+add_task(async function test_getPermissions() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let migrator = new SafariProfileMigrator();
+ sandbox.stub(MigrationUtils, "canGetPermissionsOnPlatform").resolves(true);
+ let hasPermissionsStub = sandbox
+ .stub(migrator, "hasPermissions")
+ .resolves(false);
+
+ Assert.equal(
+ await migrator.canGetPermissions(),
+ gDataDir,
+ "Should be able to get the permissions path."
+ );
+
+ let filePickerSeenCount = 0;
+
+ let filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ Assert.ok(true, "Filepicker shown.");
+ MockFilePicker.useDirectory([gDataDir]);
+ filePickerSeenCount++;
+ if (filePickerSeenCount > 3) {
+ Assert.ok(true, "File picker looped 3 times.");
+ hasPermissionsStub.resolves(true);
+ resolve();
+ }
+ };
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ // This is a little awkward, but we need to ensure that the
+ // filePickerShownPromise resolves first before we await
+ // the getPermissionsPromise in order to get the correct
+ // filePickerSeenCount.
+ let getPermissionsPromise = migrator.getPermissions();
+ await filePickerShownPromise;
+ Assert.ok(
+ await getPermissionsPromise,
+ "Should report that we got permissions."
+ );
+
+ // Make sure that the user can also hit "cancel" and that we
+ // file picker loop.
+
+ hasPermissionsStub.resolves(false);
+
+ filePickerSeenCount = 0;
+ filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ Assert.ok(true, "Filepicker shown.");
+ filePickerSeenCount++;
+ Assert.equal(filePickerSeenCount, 1, "Saw the picker once.");
+ resolve();
+ };
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnCancel;
+ Assert.ok(
+ !(await migrator.getPermissions()),
+ "Should report that we didn't get permissions."
+ );
+ await filePickerShownPromise;
+
+ sandbox.restore();
+});
diff --git a/browser/components/migration/tests/unit/test_fx_telemetry.js b/browser/components/migration/tests/unit/test_fx_telemetry.js
new file mode 100644
index 0000000000..68e34beab3
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_fx_telemetry.js
@@ -0,0 +1,436 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const { FirefoxProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/FirefoxProfileMigrator.sys.mjs"
+);
+const { InternalTestingProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/InternalTestingProfileMigrator.sys.mjs"
+);
+const { LoginCSVImport } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginCSVImport.sys.mjs"
+);
+const { PasswordFileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/FileMigrators.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+// These preferences are set to true anytime MigratorBase.migrate
+// successfully completes a migration of their type.
+const BOOKMARKS_PREF = "browser.migrate.interactions.bookmarks";
+const CSV_PASSWORDS_PREF = "browser.migrate.interactions.csvpasswords";
+const HISTORY_PREF = "browser.migrate.interactions.history";
+const PASSWORDS_PREF = "browser.migrate.interactions.passwords";
+
+function readFile(file) {
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ stream.init(file, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
+
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sis.init(stream);
+ let contents = sis.read(file.fileSize);
+ sis.close();
+ return contents;
+}
+
+function checkDirectoryContains(dir, files) {
+ print("checking " + dir.path + " - should contain " + Object.keys(files));
+ let seen = new Set();
+ for (let file of dir.directoryEntries) {
+ print("found file: " + file.path);
+ Assert.ok(file.leafName in files, file.leafName + " exists, but shouldn't");
+
+ let expectedContents = files[file.leafName];
+ if (typeof expectedContents != "string") {
+ // it's a subdir - recurse!
+ Assert.ok(file.isDirectory(), "should be a subdir");
+ let newDir = dir.clone();
+ newDir.append(file.leafName);
+ checkDirectoryContains(newDir, expectedContents);
+ } else {
+ Assert.ok(!file.isDirectory(), "should be a regular file");
+ let contents = readFile(file);
+ Assert.equal(contents, expectedContents);
+ }
+ seen.add(file.leafName);
+ }
+ let missing = [];
+ for (let x in files) {
+ if (!seen.has(x)) {
+ missing.push(x);
+ }
+ }
+ Assert.deepEqual(missing, [], "no missing files in " + dir.path);
+}
+
+function getTestDirs() {
+ // we make a directory structure in a temp dir which mirrors what we are
+ // testing.
+ let tempDir = do_get_tempdir();
+ let srcDir = tempDir.clone();
+ srcDir.append("test_source_dir");
+ srcDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ let targetDir = tempDir.clone();
+ targetDir.append("test_target_dir");
+ targetDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ // no need to cleanup these dirs - the xpcshell harness will do it for us.
+ return [srcDir, targetDir];
+}
+
+function writeToFile(dir, leafName, contents) {
+ let file = dir.clone();
+ file.append(leafName);
+
+ let outputStream = FileUtils.openFileOutputStream(file);
+ outputStream.write(contents, contents.length);
+ outputStream.close();
+}
+
+function createSubDir(dir, subDirName) {
+ let subDir = dir.clone();
+ subDir.append(subDirName);
+ subDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ return subDir;
+}
+
+async function promiseMigrator(name, srcDir, targetDir) {
+ // As the FirefoxProfileMigrator is a startup-only migrator, we import its
+ // module and instantiate it directly rather than going through MigrationUtils,
+ // to bypass that availability check.
+ let migrator = new FirefoxProfileMigrator();
+ let migrators = migrator._getResourcesInternal(srcDir, targetDir);
+ for (let m of migrators) {
+ if (m.name == name) {
+ return new Promise(resolve => m.migrate(resolve));
+ }
+ }
+ throw new Error("failed to find the " + name + " migrator");
+}
+
+function promiseTelemetryMigrator(srcDir, targetDir) {
+ return promiseMigrator("telemetry", srcDir, targetDir);
+}
+
+add_task(async function test_empty() {
+ let [srcDir, targetDir] = getTestDirs();
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true with empty directories");
+ // check both are empty
+ checkDirectoryContains(srcDir, {});
+ checkDirectoryContains(targetDir, {});
+});
+
+add_task(async function test_migrate_files() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Set up datareporting files, some to copy, some not.
+ let stateContent = JSON.stringify({
+ clientId: "68d5474e-19dc-45c1-8e9a-81fca592707c",
+ });
+ let sessionStateContent = "foobar 5432";
+ let subDir = createSubDir(srcDir, "datareporting");
+ writeToFile(subDir, "state.json", stateContent);
+ writeToFile(subDir, "session-state.json", sessionStateContent);
+ writeToFile(subDir, "other.file", "do not copy");
+
+ let archived = createSubDir(subDir, "archived");
+ writeToFile(archived, "other.file", "do not copy");
+
+ // Set up FHR files, they should not be copied.
+ writeToFile(srcDir, "healthreport.sqlite", "do not copy");
+ writeToFile(srcDir, "healthreport.sqlite-wal", "do not copy");
+ subDir = createSubDir(srcDir, "healthreport");
+ writeToFile(subDir, "state.json", "do not copy");
+ writeToFile(subDir, "other.file", "do not copy");
+
+ // Perform migration.
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(
+ ok,
+ "callback should have been true with important telemetry files copied"
+ );
+
+ checkDirectoryContains(targetDir, {
+ datareporting: {
+ "state.json": stateContent,
+ "session-state.json": sessionStateContent,
+ },
+ });
+});
+
+add_task(async function test_datareporting_not_dir() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ writeToFile(srcDir, "datareporting", "I'm a file but should be a directory");
+
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(
+ ok,
+ "callback should have been true even though the directory was a file"
+ );
+
+ checkDirectoryContains(targetDir, {});
+});
+
+add_task(async function test_datareporting_empty() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Migrate with an empty 'datareporting' subdir.
+ createSubDir(srcDir, "datareporting");
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ // We should end up with no migrated files.
+ checkDirectoryContains(targetDir, {
+ datareporting: {},
+ });
+});
+
+add_task(async function test_healthreport_empty() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Migrate with no 'datareporting' and an empty 'healthreport' subdir.
+ createSubDir(srcDir, "healthreport");
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ // We should end up with no migrated files.
+ checkDirectoryContains(targetDir, {});
+});
+
+add_task(async function test_datareporting_many() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Create some datareporting files.
+ let subDir = createSubDir(srcDir, "datareporting");
+ let shouldBeCopied = "should be copied";
+ writeToFile(subDir, "state.json", shouldBeCopied);
+ writeToFile(subDir, "session-state.json", shouldBeCopied);
+ writeToFile(subDir, "something.else", "should not");
+ createSubDir(subDir, "emptyDir");
+
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ checkDirectoryContains(targetDir, {
+ datareporting: {
+ "state.json": shouldBeCopied,
+ "session-state.json": shouldBeCopied,
+ },
+ });
+});
+
+add_task(async function test_no_session_state() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Check that migration still works properly if we only have state.json.
+ let subDir = createSubDir(srcDir, "datareporting");
+ let stateContent = "abcd984";
+ writeToFile(subDir, "state.json", stateContent);
+
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ checkDirectoryContains(targetDir, {
+ datareporting: {
+ "state.json": stateContent,
+ },
+ });
+});
+
+add_task(async function test_no_state() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Check that migration still works properly if we only have session-state.json.
+ let subDir = createSubDir(srcDir, "datareporting");
+ let sessionStateContent = "abcd512";
+ writeToFile(subDir, "session-state.json", sessionStateContent);
+
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ checkDirectoryContains(targetDir, {
+ datareporting: {
+ "session-state.json": sessionStateContent,
+ },
+ });
+});
+
+add_task(async function test_times_migration() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // create a times.json in the source directory.
+ let contents = JSON.stringify({ created: 1234 });
+ writeToFile(srcDir, "times.json", contents);
+
+ let earliest = Date.now();
+ let ok = await promiseMigrator("times", srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+ let latest = Date.now();
+
+ let timesFile = targetDir.clone();
+ timesFile.append("times.json");
+
+ let raw = readFile(timesFile);
+ let times = JSON.parse(raw);
+ Assert.ok(times.reset >= earliest && times.reset <= latest);
+ // and it should have left the creation time alone.
+ Assert.equal(times.created, 1234);
+});
+
+/**
+ * Tests that when importing bookmarks, history, or passwords, we
+ * set interaction prefs. These preferences are sent using
+ * TelemetryEnvironment.sys.mjs.
+ */
+add_task(async function test_interaction_telemetry() {
+ let testingMigrator = await MigrationUtils.getMigrator(
+ InternalTestingProfileMigrator.key
+ );
+
+ Services.prefs.clearUserPref(BOOKMARKS_PREF);
+ Services.prefs.clearUserPref(HISTORY_PREF);
+ Services.prefs.clearUserPref(PASSWORDS_PREF);
+
+ // Ensure that these prefs start false.
+ Assert.ok(!Services.prefs.getBoolPref(BOOKMARKS_PREF));
+ Assert.ok(!Services.prefs.getBoolPref(HISTORY_PREF));
+ Assert.ok(!Services.prefs.getBoolPref(PASSWORDS_PREF));
+
+ await testingMigrator.migrate(
+ MigrationUtils.resourceTypes.BOOKMARKS,
+ false,
+ InternalTestingProfileMigrator.testProfile
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(BOOKMARKS_PREF),
+ "Bookmarks pref should have been set."
+ );
+ Assert.ok(!Services.prefs.getBoolPref(HISTORY_PREF));
+ Assert.ok(!Services.prefs.getBoolPref(PASSWORDS_PREF));
+
+ await testingMigrator.migrate(
+ MigrationUtils.resourceTypes.HISTORY,
+ false,
+ InternalTestingProfileMigrator.testProfile
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(BOOKMARKS_PREF),
+ "Bookmarks pref should have been set."
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(HISTORY_PREF),
+ "History pref should have been set."
+ );
+ Assert.ok(!Services.prefs.getBoolPref(PASSWORDS_PREF));
+
+ await testingMigrator.migrate(
+ MigrationUtils.resourceTypes.PASSWORDS,
+ false,
+ InternalTestingProfileMigrator.testProfile
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(BOOKMARKS_PREF),
+ "Bookmarks pref should have been set."
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(HISTORY_PREF),
+ "History pref should have been set."
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(PASSWORDS_PREF),
+ "Passwords pref should have been set."
+ );
+
+ // Now make sure that we still record these if we migrate a
+ // series of resources at the same time.
+ Services.prefs.clearUserPref(BOOKMARKS_PREF);
+ Services.prefs.clearUserPref(HISTORY_PREF);
+ Services.prefs.clearUserPref(PASSWORDS_PREF);
+
+ await testingMigrator.migrate(
+ MigrationUtils.resourceTypes.ALL,
+ false,
+ InternalTestingProfileMigrator.testProfile
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(BOOKMARKS_PREF),
+ "Bookmarks pref should have been set."
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(HISTORY_PREF),
+ "History pref should have been set."
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(PASSWORDS_PREF),
+ "Passwords pref should have been set."
+ );
+});
+
+/**
+ * Tests that when importing passwords from a CSV file using the
+ * migration wizard, we set an interaction pref. This preference
+ * is sent using TelemetryEnvironment.sys.mjs.
+ */
+add_task(async function test_csv_password_interaction_telemetry() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let testingMigrator = new PasswordFileMigrator();
+
+ Services.prefs.clearUserPref(CSV_PASSWORDS_PREF);
+ Assert.ok(!Services.prefs.getBoolPref(CSV_PASSWORDS_PREF));
+
+ sandbox.stub(LoginCSVImport, "importFromCSV").resolves([]);
+ await testingMigrator.migrate("some/fake/path.csv");
+
+ Assert.ok(
+ Services.prefs.getBoolPref(CSV_PASSWORDS_PREF),
+ "CSV import pref should have been set."
+ );
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that interaction preferences used for TelemetryEnvironment are
+ * persisted across profile resets.
+ */
+add_task(async function test_interaction_telemetry_persist_across_reset() {
+ const PREFS = `
+user_pref("${BOOKMARKS_PREF}", true);
+user_pref("${CSV_PASSWORDS_PREF}", true);
+user_pref("${HISTORY_PREF}", true);
+user_pref("${PASSWORDS_PREF}", true);
+ `;
+
+ let [srcDir, targetDir] = getTestDirs();
+ writeToFile(srcDir, "prefs.js", PREFS);
+
+ let ok = await promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ let prefsPath = PathUtils.join(targetDir.path, "prefs.js");
+ Assert.ok(await IOUtils.exists(prefsPath), "Prefs should have been written.");
+ let writtenPrefsString = await IOUtils.readUTF8(prefsPath);
+ for (let prefKey of [
+ BOOKMARKS_PREF,
+ CSV_PASSWORDS_PREF,
+ HISTORY_PREF,
+ PASSWORDS_PREF,
+ ]) {
+ const EXPECTED = `user_pref("${prefKey}", true);`;
+ Assert.ok(writtenPrefsString.includes(EXPECTED), "Found persisted pref.");
+ }
+});
diff --git a/browser/components/migration/tests/unit/xpcshell.toml b/browser/components/migration/tests/unit/xpcshell.toml
new file mode 100644
index 0000000000..b599a64362
--- /dev/null
+++ b/browser/components/migration/tests/unit/xpcshell.toml
@@ -0,0 +1,95 @@
+[DEFAULT]
+head = "head_migration.js"
+tags = "condprof"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"] # bug 1730213
+prefs = ["browser.migrate.showBookmarksToolbarAfterMigration=true"]
+support-files = [
+ "Library/**",
+ "AppData/**",
+ "bookmarks.exported.html",
+ "bookmarks.exported.json",
+ "bookmarks.invalid.html",
+]
+
+["test_360seMigrationUtils.js"]
+run-if = ["os == 'win'"]
+
+["test_360se_bookmarks.js"]
+run-if = ["os == 'win'"]
+
+["test_BookmarksFileMigrator.js"]
+
+["test_ChromeMigrationUtils.js"]
+
+["test_ChromeMigrationUtils_path.js"]
+
+["test_ChromeMigrationUtils_path_chromium_snap.js"]
+run-if = ["os == 'linux'"]
+
+["test_Chrome_bookmarks.js"]
+
+["test_Chrome_corrupt_history.js"]
+
+["test_Chrome_credit_cards.js"]
+skip-if = [
+ "os == 'linux'",
+ "os == 'android'",
+ "condprof", # bug 1769154 - not realistic for condprof
+]
+
+["test_Chrome_extensions.js"]
+
+["test_Chrome_formdata.js"]
+
+["test_Chrome_history.js"]
+skip-if = ["os != 'mac'"] # Relies on ULibDir
+
+["test_Chrome_passwords.js"]
+skip-if = [
+ "os == 'linux'",
+ "os == 'android'",
+ "condprof", # bug 1769154 - not realistic for condprof
+]
+
+["test_Chrome_passwords_emptySource.js"]
+skip-if = [
+ "os == 'linux'",
+ "os == 'android'",
+ "condprof", # bug 1769154 - not realistic for condprof
+]
+support-files = ["LibraryWithNoData/**"]
+
+["test_Chrome_permissions.js"]
+
+["test_Edge_db_migration.js"]
+run-if = ["os == 'win'"]
+
+["test_Edge_registry_migration.js"]
+run-if = ["os == 'win'"]
+
+["test_IE_bookmarks.js"]
+run-if = ["os == 'win' && bits == 64"] # bug 1392396
+
+["test_IE_history.js"]
+run-if = ["os == 'win'"]
+skip-if = ["os == 'win' && msix"] # https://bugzilla.mozilla.org/show_bug.cgi?id=1807928
+
+["test_MigrationUtils_timedRetry.js"]
+skip-if = ["os == 'mac' && !debug"] #Bug 1558330
+
+["test_PasswordFileMigrator.js"]
+
+["test_Safari_bookmarks.js"]
+run-if = ["os == 'mac'"]
+
+["test_Safari_history.js"]
+run-if = ["os == 'mac'"]
+
+["test_Safari_history_strange_entries.js"]
+run-if = ["os == 'mac'"]
+
+["test_Safari_permissions.js"]
+run-if = ["os == 'mac'"]
+
+["test_fx_telemetry.js"]