/* 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} */ /** * Opens the migration wizard HTML5 dialog in about:preferences in the * current window's selected tab, runs an async taskFn, and then * cleans up by loading about:blank in the tab before resolving. * * @param {withMigrationWizardDialogCallback} taskFn * An async test function to be called while the migration wizard * dialog is open. * @returns {Promise} */ async function withMigrationWizardDialog(taskFn) { let migrationDialogPromise = waitForMigrationWizardDialogTab(); await MigrationUtils.showMigrationWizard(window, {}); let prefsBrowser = await migrationDialogPromise; try { await taskFn(prefsBrowser.contentWindow); } finally { if (gBrowser.tabs.length > 1) { BrowserTestUtils.removeTab(gBrowser.getTabForBrowser(prefsBrowser)); } else { BrowserTestUtils.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} * 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} */ 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 } } }