diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/mozapps/update/tests/browser/head.js | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | toolkit/mozapps/update/tests/browser/head.js | 1347 |
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); + })(); +} |