summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/update/tests/browser/head.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/mozapps/update/tests/browser/head.js1347
1 files changed, 1347 insertions, 0 deletions
diff --git a/toolkit/mozapps/update/tests/browser/head.js b/toolkit/mozapps/update/tests/browser/head.js
new file mode 100644
index 0000000000..756b2c7ea5
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/head.js
@@ -0,0 +1,1347 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs",
+ DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
+ UpdateListener: "resource://gre/modules/UpdateListener.sys.mjs",
+});
+const { XPIInstall } = ChromeUtils.import(
+ "resource://gre/modules/addons/XPIInstall.jsm"
+);
+
+const BIN_SUFFIX = AppConstants.platform == "win" ? ".exe" : "";
+const FILE_UPDATER_BIN =
+ "updater" + (AppConstants.platform == "macosx" ? ".app" : BIN_SUFFIX);
+const FILE_UPDATER_BIN_BAK = FILE_UPDATER_BIN + ".bak";
+
+const LOG_FUNCTION = info;
+
+const MAX_UPDATE_COPY_ATTEMPTS = 10;
+
+const DATA_URI_SPEC =
+ "chrome://mochitests/content/browser/toolkit/mozapps/update/tests/browser/";
+/* import-globals-from testConstants.js */
+Services.scriptloader.loadSubScript(DATA_URI_SPEC + "testConstants.js", this);
+
+var gURLData = URL_HOST + "/" + REL_PATH_DATA;
+const URL_MANUAL_UPDATE = gURLData + "downloadPage.html";
+
+const gBadSizeResult = Cr.NS_ERROR_UNEXPECTED.toString();
+
+/* import-globals-from ../data/shared.js */
+Services.scriptloader.loadSubScript(DATA_URI_SPEC + "shared.js", this);
+
+let gOriginalUpdateAutoValue = null;
+
+// Some elements append a trailing /. After the chrome tests are removed this
+// code can be changed so URL_HOST already has a trailing /.
+const gDetailsURL = URL_HOST + "/";
+
+// Set to true to log additional information for debugging. To log additional
+// information for individual tests set gDebugTest to false here and to true
+// globally in the test.
+gDebugTest = false;
+
+// This is to accommodate the TV task which runs the tests with --verify.
+requestLongerTimeout(10);
+
+/**
+ * Common tasks to perform for all tests before each one has started.
+ */
+add_setup(async function setupTestCommon() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_APP_UPDATE_BADGEWAITTIME, 1800],
+ [PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS, 0],
+ [PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS, 2],
+ [PREF_APP_UPDATE_LOG, gDebugTest],
+ [PREF_APP_UPDATE_PROMPTWAITTIME, 3600],
+ [PREF_APP_UPDATE_SERVICE_ENABLED, false],
+ ],
+ });
+
+ // We need to keep the update sync manager from thinking two instances are
+ // running because of the mochitest parent instance, which means we need to
+ // override the directory service with a fake executable path and then reset
+ // the lock. But leaving the directory service overridden causes problems for
+ // these tests, so we need to restore the real service immediately after.
+ // To form the path, we'll use the real executable path with a token appended
+ // (the path needs to be absolute, but not to point to a real file).
+ // This block is loosely copied from adjustGeneralPaths() in another update
+ // test file, xpcshellUtilsAUS.js, but this is a much more limited version;
+ // it's been copied here both because the full function is overkill and also
+ // because making it general enough to run in both xpcshell and mochitest
+ // would have been unreasonably difficult.
+ let exePath = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile);
+ let dirProvider = {
+ getFile: function AGP_DP_getFile(aProp, aPersistent) {
+ // Set the value of persistent to false so when this directory provider is
+ // unregistered it will revert back to the original provider.
+ aPersistent.value = false;
+ switch (aProp) {
+ case XRE_EXECUTABLE_FILE:
+ exePath.append("browser-test");
+ return exePath;
+ }
+ return null;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]),
+ };
+ let ds = Services.dirsvc.QueryInterface(Ci.nsIDirectoryService);
+ ds.QueryInterface(Ci.nsIProperties).undefine(XRE_EXECUTABLE_FILE);
+ ds.registerProvider(dirProvider);
+
+ let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService(
+ Ci.nsIUpdateSyncManager
+ );
+ syncManager.resetLock();
+
+ ds.unregisterProvider(dirProvider);
+
+ setUpdateTimerPrefs();
+ reloadUpdateManagerData(true);
+ removeUpdateFiles(true);
+ UpdateListener.reset();
+ AppMenuNotifications.removeNotification(/.*/);
+ // Most app update mochitest-browser-chrome tests expect auto update to be
+ // enabled. Those that don't will explicitly change this.
+ await setAppUpdateAutoEnabledHelper(true);
+});
+
+/**
+ * Common tasks to perform for all tests after each one has finished.
+ */
+registerCleanupFunction(async () => {
+ AppMenuNotifications.removeNotification(/.*/);
+ Services.env.set("MOZ_TEST_SKIP_UPDATE_STAGE", "");
+ Services.env.set("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE", "");
+ Services.env.set("MOZ_TEST_STAGING_ERROR", "");
+ UpdateListener.reset();
+ AppMenuNotifications.removeNotification(/.*/);
+ reloadUpdateManagerData(true);
+ // Pass false when the log files are needed for troubleshooting the tests.
+ removeUpdateFiles(true);
+ // Always try to restore the original updater files. If none of the updater
+ // backup files are present then this is just a no-op.
+ await finishTestRestoreUpdaterBackup();
+ // Reset the update lock once again so that we know the lock we're
+ // interested in here will be closed properly (normally that happens during
+ // XPCOM shutdown, but that isn't consistent during tests).
+ let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService(
+ Ci.nsIUpdateSyncManager
+ );
+ syncManager.resetLock();
+});
+
+/**
+ * Overrides the add-ons manager language pack staging with a mocked version.
+ * The returned promise resolves when language pack staging begins returning an
+ * object with the new appVersion and platformVersion and functions to resolve
+ * or reject the install.
+ */
+function mockLangpackInstall() {
+ let original = XPIInstall.stageLangpacksForAppUpdate;
+ registerCleanupFunction(() => {
+ XPIInstall.stageLangpacksForAppUpdate = original;
+ });
+
+ let stagingCall = PromiseUtils.defer();
+ XPIInstall.stageLangpacksForAppUpdate = (appVersion, platformVersion) => {
+ let result = PromiseUtils.defer();
+ stagingCall.resolve({
+ appVersion,
+ platformVersion,
+ resolve: result.resolve,
+ reject: result.reject,
+ });
+
+ return result.promise;
+ };
+
+ return stagingCall.promise;
+}
+
+/**
+ * Creates and locks the app update write test file so it is possible to test
+ * when the user doesn't have write access to update. Since this is only
+ * possible on Windows the function throws when it is called on other platforms.
+ * This uses registerCleanupFunction to remove the lock and the file when the
+ * test completes.
+ *
+ * @throws If the function is called on a platform other than Windows.
+ */
+function lockWriteTestFile() {
+ if (AppConstants.platform != "win") {
+ throw new Error("Windows only test function called");
+ }
+ let file = getUpdateDirFile(FILE_UPDATE_TEST).QueryInterface(
+ Ci.nsILocalFileWin
+ );
+ // Remove the file if it exists just in case.
+ if (file.exists()) {
+ file.readOnly = false;
+ file.remove(false);
+ }
+ file.create(file.NORMAL_FILE_TYPE, 0o444);
+ file.readOnly = true;
+ registerCleanupFunction(() => {
+ file.readOnly = false;
+ file.remove(false);
+ });
+}
+
+/**
+ * Closes the update mutex handle in nsUpdateService.js if it exists and then
+ * creates a new update mutex handle so the update code thinks there is another
+ * instance of the application handling updates.
+ *
+ * @throws If the function is called on a platform other than Windows.
+ */
+function setOtherInstanceHandlingUpdates() {
+ if (AppConstants.platform != "win") {
+ throw new Error("Windows only test function called");
+ }
+ gAUS.observe(null, "test-close-handle-update-mutex", "");
+ let handle = createMutex(getPerInstallationMutexName());
+ registerCleanupFunction(() => {
+ closeHandle(handle);
+ });
+}
+
+/**
+ * Gets the update version info for the update url parameters to send to
+ * app_update.sjs.
+ *
+ * @param aAppVersion (optional)
+ * The application version for the update snippet. If not specified the
+ * current application version will be used.
+ * @return The url parameters for the application and platform version to send
+ * to app_update.sjs.
+ */
+function getVersionParams(aAppVersion) {
+ let appInfo = Services.appinfo;
+ return "&appVersion=" + (aAppVersion ? aAppVersion : appInfo.version);
+}
+
+/**
+ * Prevent nsIUpdateTimerManager from notifying nsIApplicationUpdateService
+ * to check for updates by setting the app update last update time to the
+ * current time minus one minute in seconds and the interval time to 12 hours
+ * in seconds.
+ */
+function setUpdateTimerPrefs() {
+ let now = Math.round(Date.now() / 1000) - 60;
+ Services.prefs.setIntPref(PREF_APP_UPDATE_LASTUPDATETIME, now);
+ Services.prefs.setIntPref(PREF_APP_UPDATE_INTERVAL, 43200);
+}
+
+/*
+ * Sets the value of the App Auto Update setting and sets it back to the
+ * original value at the start of the test when the test finishes.
+ *
+ * @param enabled
+ * The value to set App Auto Update to.
+ */
+async function setAppUpdateAutoEnabledHelper(enabled) {
+ if (gOriginalUpdateAutoValue == null) {
+ gOriginalUpdateAutoValue = await UpdateUtils.getAppUpdateAutoEnabled();
+ registerCleanupFunction(async () => {
+ await UpdateUtils.setAppUpdateAutoEnabled(gOriginalUpdateAutoValue);
+ });
+ }
+ await UpdateUtils.setAppUpdateAutoEnabled(enabled);
+}
+
+/**
+ * Gets the specified button for the notification.
+ *
+ * @param win
+ * The window to get the notification button for.
+ * @param notificationId
+ * The ID of the notification to get the button for.
+ * @param button
+ * The anonid of the button to get.
+ * @return The button element.
+ */
+function getNotificationButton(win, notificationId, button) {
+ let notification = win.document.getElementById(
+ `appMenu-${notificationId}-notification`
+ );
+ ok(!notification.hidden, `${notificationId} notification is showing`);
+ return notification[button];
+}
+
+/**
+ * For staging tests the test updater must be used and this restores the backed
+ * up real updater if it exists and tries again on failure since Windows debug
+ * builds at times leave the file in use. After success moveRealUpdater is
+ * called to continue the setup of the test updater.
+ */
+function setupTestUpdater() {
+ return (async function () {
+ if (Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED)) {
+ try {
+ restoreUpdaterBackup();
+ } catch (e) {
+ logTestInfo(
+ "Attempt to restore the backed up updater failed... " +
+ "will try again, Exception: " +
+ e
+ );
+ await TestUtils.waitForTick();
+ await setupTestUpdater();
+ return;
+ }
+ await moveRealUpdater();
+ }
+ })();
+}
+
+/**
+ * Backs up the real updater and tries again on failure since Windows debug
+ * builds at times leave the file in use. After success it will call
+ * copyTestUpdater to continue the setup of the test updater.
+ */
+function moveRealUpdater() {
+ return (async function () {
+ try {
+ // Move away the real updater
+ let greBinDir = getGREBinDir();
+ let updater = greBinDir.clone();
+ updater.append(FILE_UPDATER_BIN);
+ updater.moveTo(greBinDir, FILE_UPDATER_BIN_BAK);
+
+ let greDir = getGREDir();
+ let updateSettingsIni = greDir.clone();
+ updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI);
+ if (updateSettingsIni.exists()) {
+ updateSettingsIni.moveTo(greDir, FILE_UPDATE_SETTINGS_INI_BAK);
+ }
+
+ let precomplete = greDir.clone();
+ precomplete.append(FILE_PRECOMPLETE);
+ if (precomplete.exists()) {
+ precomplete.moveTo(greDir, FILE_PRECOMPLETE_BAK);
+ }
+ } catch (e) {
+ logTestInfo(
+ "Attempt to move the real updater out of the way failed... " +
+ "will try again, Exception: " +
+ e
+ );
+ await TestUtils.waitForTick();
+ await moveRealUpdater();
+ return;
+ }
+
+ await copyTestUpdater();
+ })();
+}
+
+/**
+ * Copies the test updater and tries again on failure since Windows debug builds
+ * at times leave the file in use.
+ */
+function copyTestUpdater(attempt = 0) {
+ return (async function () {
+ try {
+ // Copy the test updater
+ let greBinDir = getGREBinDir();
+ let testUpdaterDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ let relPath = REL_PATH_DATA;
+ let pathParts = relPath.split("/");
+ for (let i = 0; i < pathParts.length; ++i) {
+ testUpdaterDir.append(pathParts[i]);
+ }
+
+ let testUpdater = testUpdaterDir.clone();
+ testUpdater.append(FILE_UPDATER_BIN);
+ testUpdater.copyToFollowingLinks(greBinDir, FILE_UPDATER_BIN);
+
+ let greDir = getGREDir();
+ let updateSettingsIni = greDir.clone();
+ updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI);
+ writeFile(updateSettingsIni, UPDATE_SETTINGS_CONTENTS);
+
+ let precomplete = greDir.clone();
+ precomplete.append(FILE_PRECOMPLETE);
+ writeFile(precomplete, PRECOMPLETE_CONTENTS);
+ } catch (e) {
+ if (attempt < MAX_UPDATE_COPY_ATTEMPTS) {
+ logTestInfo(
+ "Attempt to copy the test updater failed... " +
+ "will try again, Exception: " +
+ e
+ );
+ await TestUtils.waitForTick();
+ await copyTestUpdater(attempt++);
+ }
+ }
+ })();
+}
+
+/**
+ * Restores the updater and updater related file that if there a backup exists.
+ * This is called in setupTestUpdater before the backup of the real updater is
+ * done in case the previous test failed to restore the file when a test has
+ * finished. This is also called in finishTestRestoreUpdaterBackup to restore
+ * the files when a test finishes.
+ */
+function restoreUpdaterBackup() {
+ let greBinDir = getGREBinDir();
+ let updater = greBinDir.clone();
+ let updaterBackup = greBinDir.clone();
+ updater.append(FILE_UPDATER_BIN);
+ updaterBackup.append(FILE_UPDATER_BIN_BAK);
+ if (updaterBackup.exists()) {
+ if (updater.exists()) {
+ updater.remove(true);
+ }
+ updaterBackup.moveTo(greBinDir, FILE_UPDATER_BIN);
+ }
+
+ let greDir = getGREDir();
+ let updateSettingsIniBackup = greDir.clone();
+ updateSettingsIniBackup.append(FILE_UPDATE_SETTINGS_INI_BAK);
+ if (updateSettingsIniBackup.exists()) {
+ let updateSettingsIni = greDir.clone();
+ updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI);
+ if (updateSettingsIni.exists()) {
+ updateSettingsIni.remove(false);
+ }
+ updateSettingsIniBackup.moveTo(greDir, FILE_UPDATE_SETTINGS_INI);
+ }
+
+ let precomplete = greDir.clone();
+ let precompleteBackup = greDir.clone();
+ precomplete.append(FILE_PRECOMPLETE);
+ precompleteBackup.append(FILE_PRECOMPLETE_BAK);
+ if (precompleteBackup.exists()) {
+ if (precomplete.exists()) {
+ precomplete.remove(false);
+ }
+ precompleteBackup.moveTo(greDir, FILE_PRECOMPLETE);
+ } else if (precomplete.exists()) {
+ if (readFile(precomplete) == PRECOMPLETE_CONTENTS) {
+ precomplete.remove(false);
+ }
+ }
+}
+
+/**
+ * When a test finishes this will repeatedly attempt to restore the real updater
+ * and the other files for the updater if a backup of the file exists.
+ */
+function finishTestRestoreUpdaterBackup() {
+ return (async function () {
+ try {
+ // Windows debug builds keep the updater file in use for a short period of
+ // time after the updater process exits.
+ restoreUpdaterBackup();
+ } catch (e) {
+ logTestInfo(
+ "Attempt to restore the backed up updater failed... " +
+ "will try again, Exception: " +
+ e
+ );
+
+ await TestUtils.waitForTick();
+ await finishTestRestoreUpdaterBackup();
+ }
+ })();
+}
+
+/**
+ * Waits for the About Dialog to load.
+ *
+ * @return A promise that returns the domWindow for the About Dialog and
+ * resolves when the About Dialog loads.
+ */
+function waitForAboutDialog() {
+ return new Promise(resolve => {
+ var listener = {
+ onOpenWindow: aXULWindow => {
+ debugDump("About dialog shown...");
+ Services.wm.removeListener(listener);
+
+ async function aboutDialogOnLoad() {
+ domwindow.removeEventListener("load", aboutDialogOnLoad, true);
+ let chromeURI = "chrome://browser/content/aboutDialog.xhtml";
+ is(
+ domwindow.document.location.href,
+ chromeURI,
+ "About dialog appeared"
+ );
+ resolve(domwindow);
+ }
+
+ var domwindow = aXULWindow.docShell.domWindow;
+ domwindow.addEventListener("load", aboutDialogOnLoad, true);
+ },
+ onCloseWindow: aXULWindow => {},
+ };
+
+ Services.wm.addListener(listener);
+ openAboutDialog();
+ });
+}
+
+/**
+ * Return the first UpdatePatch with the given type.
+ *
+ * @param type
+ * The type of the patch ("complete" or "partial")
+ * @param update
+ * The nsIUpdate to select a patch from.
+ * @return A nsIUpdatePatch object matching the type specified
+ */
+function getPatchOfType(type, update) {
+ if (update) {
+ for (let i = 0; i < update.patchCount; ++i) {
+ let patch = update.getPatchAt(i);
+ if (patch && patch.type == type) {
+ return patch;
+ }
+ }
+ }
+ return null;
+}
+
+/**
+ * Runs a Doorhanger update test. This will set various common prefs for
+ * updating and runs the provided list of steps.
+ *
+ * @param params
+ * An object containing parameters used to run the test.
+ * @param steps
+ * An array of test steps to perform. A step will either be an object
+ * containing expected conditions and actions or a function to call.
+ * @return A promise which will resolve once all of the steps have been run.
+ */
+function runDoorhangerUpdateTest(params, steps) {
+ function processDoorhangerStep(step) {
+ if (typeof step == "function") {
+ return step();
+ }
+
+ const {
+ notificationId,
+ button,
+ checkActiveUpdate,
+ pageURLs,
+ expectedStateOverride,
+ } = step;
+ return (async function () {
+ if (!params.popupShown && !PanelUI.isNotificationPanelOpen) {
+ await BrowserTestUtils.waitForEvent(
+ PanelUI.notificationPanel,
+ "popupshown"
+ );
+ }
+ const shownNotificationId = AppMenuNotifications.activeNotification.id;
+ is(
+ shownNotificationId,
+ notificationId,
+ "The right notification showed up."
+ );
+
+ let expectedState = Ci.nsIApplicationUpdateService.STATE_IDLE;
+ if (expectedStateOverride) {
+ expectedState = expectedStateOverride;
+ } else if (notificationId == "update-restart") {
+ expectedState = Ci.nsIApplicationUpdateService.STATE_PENDING;
+ }
+ let actualState = gAUS.currentState;
+ is(
+ actualState,
+ expectedState,
+ `The current update state should be ` +
+ `"${gAUS.getStateName(expectedState)}". Actual: ` +
+ `"${gAUS.getStateName(actualState)}"`
+ );
+
+ if (checkActiveUpdate) {
+ let activeUpdate =
+ checkActiveUpdate.state == STATE_DOWNLOADING
+ ? gUpdateManager.downloadingUpdate
+ : gUpdateManager.readyUpdate;
+ ok(!!activeUpdate, "There should be an active update");
+ is(
+ activeUpdate.state,
+ checkActiveUpdate.state,
+ `The active update state should equal ${checkActiveUpdate.state}`
+ );
+ } else {
+ ok(
+ !gUpdateManager.downloadingUpdate,
+ "There should not be a downloading update"
+ );
+ ok(!gUpdateManager.readyUpdate, "There should not be a ready update");
+ }
+
+ let buttonEl = getNotificationButton(window, notificationId, button);
+ buttonEl.click();
+
+ if (pageURLs && pageURLs.manual !== undefined) {
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ pageURLs.manual,
+ `The page's url should equal ${pageURLs.manual}`
+ );
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+ })();
+ }
+
+ return (async function () {
+ if (params.slowStaging) {
+ Services.env.set("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE", "1");
+ } else {
+ Services.env.set("MOZ_TEST_SKIP_UPDATE_STAGE", "1");
+ }
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_APP_UPDATE_DISABLEDFORTESTING, false],
+ [PREF_APP_UPDATE_URL_DETAILS, gDetailsURL],
+ [PREF_APP_UPDATE_URL_MANUAL, URL_MANUAL_UPDATE],
+ ],
+ });
+
+ await setupTestUpdater();
+
+ let baseURL = URL_HTTP_UPDATE_SJS;
+ if (params.baseURL) {
+ baseURL = params.baseURL;
+ }
+ let queryString = params.queryString ? params.queryString : "";
+ let updateURL =
+ baseURL +
+ "?detailsURL=" +
+ gDetailsURL +
+ queryString +
+ getVersionParams(params.version);
+ setUpdateURL(updateURL);
+ if (params.checkAttempts) {
+ // Perform a background check doorhanger test.
+ executeSoon(() => {
+ (async function () {
+ gAUS.checkForBackgroundUpdates();
+ for (var i = 0; i < params.checkAttempts - 1; i++) {
+ await waitForEvent("update-error", "check-attempt-failed");
+ gAUS.checkForBackgroundUpdates();
+ }
+ })();
+ });
+ } else {
+ // Perform a startup processing doorhanger test.
+ writeStatusFile(STATE_FAILED_CRC_ERROR);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(params.updates), true);
+ reloadUpdateManagerData();
+ testPostUpdateProcessing();
+ }
+
+ for (let step of steps) {
+ await processDoorhangerStep(step);
+ }
+ })();
+}
+
+/**
+ * Runs an About Dialog update test. This will set various common prefs for
+ * updating and runs the provided list of steps.
+ *
+ * @param params
+ * An object containing parameters used to run the test.
+ * @param steps
+ * An array of test steps to perform. A step will either be an object
+ * containing expected conditions and actions or a function to call.
+ * @return A promise which will resolve once all of the steps have been run.
+ */
+function runAboutDialogUpdateTest(params, steps) {
+ let aboutDialog;
+ function processAboutDialogStep(step) {
+ if (typeof step == "function") {
+ return step(aboutDialog);
+ }
+
+ const {
+ panelId,
+ checkActiveUpdate,
+ continueFile,
+ downloadInfo,
+ forceApply,
+ noContinue,
+ expectedStateOverride,
+ } = step;
+ return (async function () {
+ await TestUtils.waitForCondition(
+ () =>
+ aboutDialog.gAppUpdater &&
+ aboutDialog.gAppUpdater.selectedPanel?.id == panelId,
+ "Waiting for the expected panel ID: " + panelId,
+ undefined,
+ 200
+ ).catch(e => {
+ // Instead of throwing let the check below fail the test so the panel
+ // ID and the expected panel ID is printed in the log.
+ logTestInfo(e);
+ });
+ let { selectedPanel } = aboutDialog.gAppUpdater;
+ is(selectedPanel.id, panelId, "The panel ID should equal " + panelId);
+ ok(
+ BrowserTestUtils.is_visible(selectedPanel),
+ "The panel should be visible"
+ );
+
+ if (
+ panelId == "downloading" &&
+ gAUS.currentState == Ci.nsIApplicationUpdateService.STATE_IDLE
+ ) {
+ // Now that `AUS.downloadUpdate` is async, we start showing the
+ // downloading panel while `AUS.downloadUpdate` is still resolving.
+ // But the below checks assume that this resolution has already
+ // happened. So we need to wait for things to actually resolve.
+ debugDump("Waiting for downloading state to actually start");
+ await gAUS.stateTransition;
+
+ // Check that the checks that we made above are still valid.
+ selectedPanel = aboutDialog.gAppUpdater.selectedPanel;
+ is(selectedPanel.id, panelId, "The panel ID should equal " + panelId);
+ ok(
+ BrowserTestUtils.is_visible(selectedPanel),
+ "The panel should be visible"
+ );
+ }
+
+ let expectedState = Ci.nsIApplicationUpdateService.STATE_IDLE;
+ if (expectedStateOverride) {
+ expectedState = expectedStateOverride;
+ } else if (panelId == "apply") {
+ expectedState = Ci.nsIApplicationUpdateService.STATE_PENDING;
+ } else if (panelId == "downloading") {
+ expectedState = Ci.nsIApplicationUpdateService.STATE_DOWNLOADING;
+ } else if (panelId == "applying") {
+ expectedState = Ci.nsIApplicationUpdateService.STATE_STAGING;
+ }
+ let actualState = gAUS.currentState;
+ is(
+ actualState,
+ expectedState,
+ `The current update state should be ` +
+ `"${gAUS.getStateName(expectedState)}". Actual: ` +
+ `"${gAUS.getStateName(actualState)}"`
+ );
+
+ if (checkActiveUpdate) {
+ let activeUpdate =
+ checkActiveUpdate.state == STATE_DOWNLOADING
+ ? gUpdateManager.downloadingUpdate
+ : gUpdateManager.readyUpdate;
+ ok(!!activeUpdate, "There should be an active update");
+ is(
+ activeUpdate.state,
+ checkActiveUpdate.state,
+ "The active update state should equal " + checkActiveUpdate.state
+ );
+ } else {
+ ok(
+ !gUpdateManager.downloadingUpdate,
+ "There should not be a downloading update"
+ );
+ ok(!gUpdateManager.readyUpdate, "There should not be a ready update");
+ }
+
+ // Some tests just want to stop at the downloading state. These won't
+ // include a continue file in that state.
+ if (panelId == "downloading" && continueFile) {
+ for (let i = 0; i < downloadInfo.length; ++i) {
+ let data = downloadInfo[i];
+ await continueFileHandler(continueFile);
+ let patch = getPatchOfType(
+ data.patchType,
+ gUpdateManager.downloadingUpdate
+ );
+ // The update is removed early when the last download fails so check
+ // that there is a patch before proceeding.
+ let isLastPatch = i == downloadInfo.length - 1;
+ if (!isLastPatch || patch) {
+ let resultName = data.bitsResult ? "bitsResult" : "internalResult";
+ patch.QueryInterface(Ci.nsIWritablePropertyBag);
+ await TestUtils.waitForCondition(
+ () => patch.getProperty(resultName) == data[resultName],
+ "Waiting for expected patch property " +
+ resultName +
+ " value: " +
+ data[resultName],
+ undefined,
+ 200
+ ).catch(e => {
+ // Instead of throwing let the check below fail the test so the
+ // property value and the expected property value is printed in
+ // the log.
+ logTestInfo(e);
+ });
+ is(
+ "" + patch.getProperty(resultName),
+ data[resultName],
+ "The patch property " +
+ resultName +
+ " value should equal " +
+ data[resultName]
+ );
+
+ // Check the download status text. It should be something like,
+ // "1.4 of 1.4 KB".
+ let expectedText = DownloadUtils.getTransferTotal(
+ data[resultName] == gBadSizeResult ? 0 : patch.size,
+ patch.size
+ );
+ Assert.ok(
+ expectedText,
+ "Sanity check: Expected download status text should be non-empty"
+ );
+ if (aboutDialog.document.hasPendingL10nMutations) {
+ await BrowserTestUtils.waitForEvent(
+ aboutDialog.document,
+ "L10nMutationsFinished"
+ );
+ }
+ Assert.equal(
+ aboutDialog.document.querySelector(
+ `#downloading label[data-l10n-name="download-status"]`
+ ).textContent,
+ expectedText,
+ "Download status text should be correct"
+ );
+ }
+ }
+ } else if (continueFile) {
+ await continueFileHandler(continueFile);
+ }
+
+ let linkPanels = [
+ "downloadFailed",
+ "manualUpdate",
+ "unsupportedSystem",
+ "internalError",
+ ];
+ if (linkPanels.includes(panelId)) {
+ // The unsupportedSystem panel uses the update's detailsURL and the
+ // downloadFailed and manualUpdate panels use the app.update.url.manual
+ // preference.
+ let selector = "label.text-link";
+ if (selectedPanel.ownerDocument.hasPendingL10nMutations) {
+ await BrowserTestUtils.waitForEvent(
+ selectedPanel.ownerDocument,
+ "L10nMutationsFinished"
+ );
+ }
+ let link = selectedPanel.querySelector(selector);
+ is(
+ link.href,
+ gDetailsURL,
+ `The panel's link href should equal ${gDetailsURL}`
+ );
+ const assertNonEmptyText = (node, description) => {
+ let textContent = node.textContent.trim();
+ ok(textContent, `${description}, got "${textContent}"`);
+ };
+ assertNonEmptyText(
+ link,
+ `The panel's link should have non-empty textContent`
+ );
+ let linkWrapperClone = link.parentNode.cloneNode(true);
+ linkWrapperClone.querySelector(selector).remove();
+ assertNonEmptyText(
+ linkWrapperClone,
+ `The panel's link should have text around the link`
+ );
+ }
+
+ // Automatically click the download button unless `noContinue` was passed.
+ let buttonPanels = ["downloadAndInstall", "apply"];
+ if (buttonPanels.includes(panelId) && !noContinue) {
+ let buttonEl = selectedPanel.querySelector("button");
+ await TestUtils.waitForCondition(
+ () => aboutDialog.document.activeElement == buttonEl,
+ "The button should receive focus"
+ );
+ ok(!buttonEl.disabled, "The button should be enabled");
+ // Don't click the button on the apply panel since this will restart the
+ // application.
+ if (panelId != "apply" || forceApply) {
+ buttonEl.click();
+ }
+ }
+ })();
+ }
+
+ return (async function () {
+ Services.env.set("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE", "1");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_APP_UPDATE_DISABLEDFORTESTING, false],
+ [PREF_APP_UPDATE_URL_MANUAL, gDetailsURL],
+ ],
+ });
+
+ await setupTestUpdater();
+
+ let baseURL = URL_HTTP_UPDATE_SJS;
+ if (params.baseURL) {
+ baseURL = params.baseURL;
+ }
+ let queryString = params.queryString ? params.queryString : "";
+ let updateURL =
+ baseURL +
+ "?detailsURL=" +
+ gDetailsURL +
+ queryString +
+ getVersionParams(params.version);
+ if (params.backgroundUpdate) {
+ setUpdateURL(updateURL);
+ gAUS.checkForBackgroundUpdates();
+ if (params.continueFile) {
+ await continueFileHandler(params.continueFile);
+ }
+ if (params.waitForUpdateState) {
+ let whichUpdate =
+ params.waitForUpdateState == STATE_DOWNLOADING
+ ? "downloadingUpdate"
+ : "readyUpdate";
+ await TestUtils.waitForCondition(
+ () =>
+ gUpdateManager[whichUpdate] &&
+ gUpdateManager[whichUpdate].state == params.waitForUpdateState,
+ "Waiting for update state: " + params.waitForUpdateState,
+ undefined,
+ 200
+ ).catch(e => {
+ // Instead of throwing let the check below fail the test so the panel
+ // ID and the expected panel ID is printed in the log.
+ logTestInfo(e);
+ });
+ // Display the UI after the update state equals the expected value.
+ is(
+ gUpdateManager[whichUpdate].state,
+ params.waitForUpdateState,
+ "The update state value should equal " + params.waitForUpdateState
+ );
+ }
+ } else {
+ updateURL += "&slowUpdateCheck=1&useSlowDownloadMar=1";
+ setUpdateURL(updateURL);
+ }
+
+ aboutDialog = await waitForAboutDialog();
+ registerCleanupFunction(() => {
+ aboutDialog.close();
+ });
+
+ for (let step of steps) {
+ await processAboutDialogStep(step);
+ }
+ })();
+}
+
+/**
+ * Runs an about:preferences update test. This will set various common prefs for
+ * updating and runs the provided list of steps.
+ *
+ * @param params
+ * An object containing parameters used to run the test.
+ * @param steps
+ * An array of test steps to perform. A step will either be an object
+ * containing expected conditions and actions or a function to call.
+ * @return A promise which will resolve once all of the steps have been run.
+ */
+function runAboutPrefsUpdateTest(params, steps) {
+ let tab;
+ function processAboutPrefsStep(step) {
+ if (typeof step == "function") {
+ return step(tab);
+ }
+
+ const {
+ panelId,
+ checkActiveUpdate,
+ continueFile,
+ downloadInfo,
+ forceApply,
+ expectedStateOverride,
+ } = step;
+ return (async function () {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [{ panelId }],
+ async ({ panelId }) => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.gAppUpdater.selectedPanel?.id == panelId,
+ "Waiting for the expected panel ID: " + panelId,
+ undefined,
+ 200
+ ).catch(e => {
+ // Instead of throwing let the check below fail the test so the panel
+ // ID and the expected panel ID is printed in the log. Use info here
+ // instead of logTestInfo since logTestInfo isn't available in the
+ // content task.
+ info(e);
+ });
+ is(
+ content.gAppUpdater.selectedPanel.id,
+ panelId,
+ "The panel ID should equal " + panelId
+ );
+ }
+ );
+
+ if (
+ panelId == "downloading" &&
+ gAUS.currentState == Ci.nsIApplicationUpdateService.STATE_IDLE
+ ) {
+ // Now that `AUS.downloadUpdate` is async, we start showing the
+ // downloading panel while `AUS.downloadUpdate` is still resolving.
+ // But the below checks assume that this resolution has already
+ // happened. So we need to wait for things to actually resolve.
+ debugDump("Waiting for downloading state to actually start");
+ await gAUS.stateTransition;
+
+ // Check that the checks that we made above are still valid.
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [{ panelId }],
+ ({ panelId }) => {
+ is(
+ content.gAppUpdater.selectedPanel.id,
+ panelId,
+ "The panel ID should equal " + panelId
+ );
+ }
+ );
+ }
+
+ let expectedState = Ci.nsIApplicationUpdateService.STATE_IDLE;
+ if (expectedStateOverride) {
+ expectedState = expectedStateOverride;
+ } else if (panelId == "apply") {
+ expectedState = Ci.nsIApplicationUpdateService.STATE_PENDING;
+ } else if (panelId == "downloading") {
+ expectedState = Ci.nsIApplicationUpdateService.STATE_DOWNLOADING;
+ } else if (panelId == "applying") {
+ expectedState = Ci.nsIApplicationUpdateService.STATE_STAGING;
+ }
+ let actualState = gAUS.currentState;
+ is(
+ actualState,
+ expectedState,
+ `The current update state should be ` +
+ `"${gAUS.getStateName(expectedState)}". Actual: ` +
+ `"${gAUS.getStateName(actualState)}"`
+ );
+
+ if (checkActiveUpdate) {
+ let activeUpdate =
+ checkActiveUpdate.state == STATE_DOWNLOADING
+ ? gUpdateManager.downloadingUpdate
+ : gUpdateManager.readyUpdate;
+ ok(!!activeUpdate, "There should be an active update");
+ is(
+ activeUpdate.state,
+ checkActiveUpdate.state,
+ "The active update state should equal " + checkActiveUpdate.state
+ );
+ } else {
+ ok(
+ !gUpdateManager.downloadingUpdate,
+ "There should not be a downloading update"
+ );
+ ok(!gUpdateManager.readyUpdate, "There should not be a ready update");
+ }
+
+ if (panelId == "downloading") {
+ if (!downloadInfo) {
+ logTestInfo("no downloadinfo, possible error?");
+ }
+ for (let i = 0; i < downloadInfo.length; ++i) {
+ let data = downloadInfo[i];
+ // The About Dialog tests always specify a continue file.
+ await continueFileHandler(continueFile);
+ let patch = getPatchOfType(
+ data.patchType,
+ gUpdateManager.downloadingUpdate
+ );
+ // The update is removed early when the last download fails so check
+ // that there is a patch before proceeding.
+ let isLastPatch = i == downloadInfo.length - 1;
+ if (!isLastPatch || patch) {
+ let resultName = data.bitsResult ? "bitsResult" : "internalResult";
+ patch.QueryInterface(Ci.nsIWritablePropertyBag);
+ await TestUtils.waitForCondition(
+ () => patch.getProperty(resultName) == data[resultName],
+ "Waiting for expected patch property " +
+ resultName +
+ " value: " +
+ data[resultName],
+ undefined,
+ 200
+ ).catch(e => {
+ // Instead of throwing let the check below fail the test so the
+ // property value and the expected property value is printed in
+ // the log.
+ logTestInfo(e);
+ });
+ is(
+ "" + patch.getProperty(resultName),
+ data[resultName],
+ "The patch property " +
+ resultName +
+ " value should equal " +
+ data[resultName]
+ );
+
+ // Check the download status text. It should be something like,
+ // "Downloading update — 1.4 of 1.4 KB". We check only the second
+ // part to make sure that the downloaded size is updated correctly.
+ let actualText = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async () => {
+ const { document } = content;
+ if (document.hasPendingL10nMutations) {
+ await ContentTaskUtils.waitForEvent(
+ document,
+ "L10nMutationsFinished"
+ );
+ }
+ return document.getElementById("downloading").textContent;
+ }
+ );
+ let expectedSuffix = DownloadUtils.getTransferTotal(
+ data[resultName] == gBadSizeResult ? 0 : patch.size,
+ patch.size
+ );
+ Assert.ok(
+ expectedSuffix,
+ "Sanity check: Expected download status text should be non-empty"
+ );
+ Assert.ok(
+ actualText.endsWith(expectedSuffix),
+ "Download status text should end as expected: " +
+ JSON.stringify({ actualText, expectedSuffix })
+ );
+ }
+ }
+ } else if (continueFile) {
+ await continueFileHandler(continueFile);
+ }
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [{ panelId, gDetailsURL, forceApply }],
+ async ({ panelId, gDetailsURL, forceApply }) => {
+ let linkPanels = [
+ "downloadFailed",
+ "manualUpdate",
+ "unsupportedSystem",
+ "internalError",
+ ];
+ if (linkPanels.includes(panelId)) {
+ let { selectedPanel } = content.gAppUpdater;
+ // The unsupportedSystem panel uses the update's detailsURL and the
+ // downloadFailed and manualUpdate panels use the app.update.url.manual
+ // preference.
+ let selector = "label.text-link";
+ // The downloadFailed panel in about:preferences uses an anchor
+ // instead of a label for the link.
+ if (selectedPanel.id == "downloadFailed") {
+ selector = "a.text-link";
+ }
+ // The manualUpdate panel in about:preferences uses
+ // the moz-support-link element which doesn't have
+ // the .text-link class.
+ if (selectedPanel.id == "manualUpdate") {
+ selector = "a.manualLink";
+ }
+ if (selectedPanel.ownerDocument.hasPendingL10nMutations) {
+ await ContentTaskUtils.waitForEvent(
+ selectedPanel.ownerDocument,
+ "L10nMutationsFinished"
+ );
+ }
+ let link = selectedPanel.querySelector(selector);
+ is(
+ link.href,
+ gDetailsURL,
+ `The panel's link href should equal ${gDetailsURL}`
+ );
+ const assertNonEmptyText = (node, description) => {
+ let textContent = node.textContent.trim();
+ ok(textContent, `${description}, got "${textContent}"`);
+ };
+ assertNonEmptyText(
+ link,
+ `The panel's link should have non-empty textContent`
+ );
+ let linkWrapperClone = link.parentNode.cloneNode(true);
+ linkWrapperClone.querySelector(selector).remove();
+ assertNonEmptyText(
+ linkWrapperClone,
+ `The panel's link should have text around the link`
+ );
+ }
+
+ let buttonPanels = ["downloadAndInstall", "apply"];
+ if (buttonPanels.includes(panelId)) {
+ let { selectedPanel } = content.gAppUpdater;
+ let buttonEl = selectedPanel.querySelector("button");
+ // Note: The about:preferences doesn't focus the button like the
+ // About Dialog does.
+ ok(!buttonEl.disabled, "The button should be enabled");
+ // Don't click the button on the apply panel since this will restart
+ // the application.
+ if (selectedPanel.id != "apply" || forceApply) {
+ buttonEl.click();
+ }
+ }
+ }
+ );
+ })();
+ }
+
+ return (async function () {
+ Services.env.set("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE", "1");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_APP_UPDATE_DISABLEDFORTESTING, false],
+ [PREF_APP_UPDATE_URL_MANUAL, gDetailsURL],
+ ],
+ });
+
+ await setupTestUpdater();
+
+ let baseURL = URL_HTTP_UPDATE_SJS;
+ if (params.baseURL) {
+ baseURL = params.baseURL;
+ }
+ let queryString = params.queryString ? params.queryString : "";
+ let updateURL =
+ baseURL +
+ "?detailsURL=" +
+ gDetailsURL +
+ queryString +
+ getVersionParams(params.version);
+ if (params.backgroundUpdate) {
+ setUpdateURL(updateURL);
+ gAUS.checkForBackgroundUpdates();
+ if (params.continueFile) {
+ await continueFileHandler(params.continueFile);
+ }
+ if (params.waitForUpdateState) {
+ // Wait until the update state equals the expected value before
+ // displaying the UI.
+ let whichUpdate =
+ params.waitForUpdateState == STATE_DOWNLOADING
+ ? "downloadingUpdate"
+ : "readyUpdate";
+ await TestUtils.waitForCondition(
+ () =>
+ gUpdateManager[whichUpdate] &&
+ gUpdateManager[whichUpdate].state == params.waitForUpdateState,
+ "Waiting for update state: " + params.waitForUpdateState,
+ undefined,
+ 200
+ ).catch(e => {
+ // Instead of throwing let the check below fail the test so the panel
+ // ID and the expected panel ID is printed in the log.
+ logTestInfo(e);
+ });
+ is(
+ gUpdateManager[whichUpdate].state,
+ params.waitForUpdateState,
+ "The update state value should equal " + params.waitForUpdateState
+ );
+ }
+ } else {
+ updateURL += "&slowUpdateCheck=1&useSlowDownloadMar=1";
+ setUpdateURL(updateURL);
+ }
+
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences"
+ );
+ registerCleanupFunction(async () => {
+ await BrowserTestUtils.removeTab(tab);
+ });
+
+ // Scroll the UI into view so it is easier to troubleshoot tests.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ content.document.getElementById("updatesCategory").scrollIntoView();
+ });
+
+ for (let step of steps) {
+ await processAboutPrefsStep(step);
+ }
+ })();
+}
+
+/**
+ * Removes the modified update-settings.ini file so the updater will fail to
+ * stage an update.
+ */
+function removeUpdateSettingsIni() {
+ if (Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED)) {
+ let greDir = getGREDir();
+ let updateSettingsIniBak = greDir.clone();
+ updateSettingsIniBak.append(FILE_UPDATE_SETTINGS_INI_BAK);
+ if (updateSettingsIniBak.exists()) {
+ let updateSettingsIni = greDir.clone();
+ updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI);
+ updateSettingsIni.remove(false);
+ }
+ }
+}
+
+/**
+ * Runs a telemetry update test. This will set various common prefs for
+ * updating, checks for an update, and waits for the specified observer
+ * notification.
+ *
+ * @param updateParams
+ * Params which will be sent to app_update.sjs.
+ * @param event
+ * The observer notification to wait for before proceeding.
+ * @param stageFailure (optional)
+ * Whether to force a staging failure by removing the modified
+ * update-settings.ini file.
+ * @return A promise which will resolve after the .
+ */
+function runTelemetryUpdateTest(updateParams, event, stageFailure = false) {
+ return (async function () {
+ Services.telemetry.clearScalars();
+ Services.env.set("MOZ_TEST_SKIP_UPDATE_STAGE", "1");
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_APP_UPDATE_DISABLEDFORTESTING, false]],
+ });
+
+ await setupTestUpdater();
+
+ if (stageFailure) {
+ removeUpdateSettingsIni();
+ }
+
+ let updateURL =
+ URL_HTTP_UPDATE_SJS +
+ "?detailsURL=" +
+ gDetailsURL +
+ updateParams +
+ getVersionParams();
+ setUpdateURL(updateURL);
+ gAUS.checkForBackgroundUpdates();
+ await waitForEvent(event);
+ })();
+}