summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/tests/browser/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/migration/tests/browser/head.js')
-rw-r--r--browser/components/migration/tests/browser/head.js534
1 files changed, 534 insertions, 0 deletions
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
+ }
+ }
+}