/* eslint no-unused-vars: ["error", {vars: "local", args: "none"}] */ const { PermissionTestUtils } = ChromeUtils.importESModule( "resource://testing-common/PermissionTestUtils.sys.mjs" ); const { PromptTestUtils } = ChromeUtils.importESModule( "resource://testing-common/PromptTestUtils.sys.mjs" ); const RELATIVE_DIR = "toolkit/mozapps/extensions/test/xpinstall/"; const TESTROOT = "http://example.com/browser/" + RELATIVE_DIR; const TESTROOT2 = "http://example.org/browser/" + RELATIVE_DIR; const PROMPT_URL = "chrome://global/content/commonDialog.xhtml"; const ADDONS_URL = "chrome://mozapps/content/extensions/aboutaddons.html"; const PREF_LOGGING_ENABLED = "extensions.logging.enabled"; const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts"; const PREF_INSTALL_REQUIRESECUREORIGIN = "extensions.install.requireSecureOrigin"; const CHROME_NAME = "mochikit"; function getChromeRoot(path) { if (path === undefined) { return "chrome://" + CHROME_NAME + "/content/browser/" + RELATIVE_DIR; } return getRootDirectory(path); } function extractChromeRoot(path) { var chromeRootPath = getChromeRoot(path); var jar = getJar(chromeRootPath); if (jar) { var tmpdir = extractJarToTmp(jar); return "file://" + tmpdir.path + "/"; } return chromeRootPath; } function setInstallTriggerPrefs() { Services.prefs.setBoolPref("extensions.InstallTrigger.enabled", true); Services.prefs.setBoolPref("extensions.InstallTriggerImpl.enabled", true); // Relax the user input requirements while running tests that call this test helper. Services.prefs.setBoolPref("xpinstall.userActivation.required", false); registerCleanupFunction(clearInstallTriggerPrefs); } function clearInstallTriggerPrefs() { Services.prefs.clearUserPref("extensions.InstallTrigger.enabled"); Services.prefs.clearUserPref("extensions.InstallTriggerImpl.enabled"); Services.prefs.clearUserPref("xpinstall.userActivation.required"); } /** * This is a test harness designed to handle responding to UI during the process * of installing an XPI. A test can set callbacks to hear about specific parts * of the sequence. * Before use setup must be called and finish must be called afterwards. */ var Harness = { // If set then the callback is called when an install is attempted and // software installation is disabled. installDisabledCallback: null, // If set then the callback is called when an install is attempted and // then canceled. installCancelledCallback: null, // If set then the callback will be called when an install's origin is blocked. installOriginBlockedCallback: null, // If set then the callback will be called when an install is blocked by the // whitelist. The callback should return true to continue with the install // anyway. installBlockedCallback: null, // If set will be called in the event of authentication being needed to get // the xpi. Should return a 2 element array of username and password, or // null to not authenticate. authenticationCallback: null, // If set this will be called to allow checking the contents of the xpinstall // confirmation dialog. The callback should return true to continue the install. installConfirmCallback: null, // If set will be called when downloading of an item has begun. downloadStartedCallback: null, // If set will be called during the download of an item. downloadProgressCallback: null, // If set will be called when an xpi fails to download. downloadFailedCallback: null, // If set will be called when an xpi download is cancelled. downloadCancelledCallback: null, // If set will be called when downloading of an item has ended. downloadEndedCallback: null, // If set will be called when installation by the extension manager of an xpi // item starts installStartedCallback: null, // If set will be called when an xpi fails to install. installFailedCallback: null, // If set will be called when each xpi item to be installed completes // installation. installEndedCallback: null, // If set will be called when all triggered items are installed or the install // is canceled. installsCompletedCallback: null, // If set the harness will wait for this DOM event before calling // installsCompletedCallback finalContentEvent: null, waitingForEvent: false, pendingCount: null, installCount: null, runningInstalls: null, waitingForFinish: false, // A unique value to return from the installConfirmCallback to indicate that // the install UI shouldn't be closed automatically leaveOpen: {}, // Setup and tear down functions setup(win = window) { if (!this.waitingForFinish) { waitForExplicitFinish(); this.waitingForFinish = true; Services.prefs.setBoolPref(PREF_INSTALL_REQUIRESECUREORIGIN, false); Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true); Services.prefs.setBoolPref( "network.cookieJarSettings.unblocked_for_testing", true ); Services.obs.addObserver(this, "addon-install-started"); Services.obs.addObserver(this, "addon-install-disabled"); Services.obs.addObserver(this, "addon-install-origin-blocked"); Services.obs.addObserver(this, "addon-install-blocked"); Services.obs.addObserver(this, "addon-install-failed"); // For browser_auth tests which trigger auth dialogs. Services.obs.addObserver(this, "common-dialog-loaded"); this._boundWin = Cu.getWeakReference(win); // need this so our addon manager listener knows which window to use. AddonManager.addInstallListener(this); AddonManager.addAddonListener(this); win.addEventListener("popupshown", this); win.PanelUI.notificationPanel.addEventListener("popupshown", this); var self = this; registerCleanupFunction(async function () { Services.prefs.clearUserPref(PREF_LOGGING_ENABLED); Services.prefs.clearUserPref(PREF_INSTALL_REQUIRESECUREORIGIN); Services.prefs.clearUserPref( "network.cookieJarSettings.unblocked_for_testing" ); Services.obs.removeObserver(self, "addon-install-started"); Services.obs.removeObserver(self, "addon-install-disabled"); Services.obs.removeObserver(self, "addon-install-origin-blocked"); Services.obs.removeObserver(self, "addon-install-blocked"); Services.obs.removeObserver(self, "addon-install-failed"); Services.obs.removeObserver(self, "common-dialog-loaded"); AddonManager.removeInstallListener(self); AddonManager.removeAddonListener(self); win.removeEventListener("popupshown", self); win.PanelUI.notificationPanel.removeEventListener("popupshown", self); win = null; let aInstalls = await AddonManager.getAllInstalls(); is( aInstalls.length, 0, "Should be no active installs at the end of the test" ); await Promise.all( aInstalls.map(async function (aInstall) { info( "Install for " + aInstall.sourceURI + " is in state " + aInstall.state ); if (aInstall.state == AddonManager.STATE_INSTALLED) { await aInstall.addon.uninstall(); } else { aInstall.cancel(); } }) ); }); } this.installCount = 0; this.pendingCount = 0; this.runningInstalls = []; }, finish(win = window) { // Some tests using this harness somehow finish leaving // the addon-installed panel open. hiding here addresses // that which fixes the rest of the tests. Since no test // here cares about this panel, we just need it to close. win.PanelUI.notificationPanel.hidePopup(); win.AppMenuNotifications.removeNotification("addon-installed"); delete this._boundWin; finish(); }, endTest() { let callback = this.installsCompletedCallback; let count = this.installCount; is(this.runningInstalls.length, 0, "Should be no running installs left"); this.runningInstalls.forEach(function (aInstall) { info( "Install for " + aInstall.sourceURI + " is in state " + aInstall.state ); }); this.installOriginBlockedCallback = null; this.installBlockedCallback = null; this.authenticationCallback = null; this.installConfirmCallback = null; this.downloadStartedCallback = null; this.downloadProgressCallback = null; this.downloadCancelledCallback = null; this.downloadFailedCallback = null; this.downloadEndedCallback = null; this.installStartedCallback = null; this.installFailedCallback = null; this.installEndedCallback = null; this.installsCompletedCallback = null; this.runningInstalls = null; if (callback) { executeSoon(() => callback(count)); } }, promptReady(dialog) { let promptType = dialog.args.promptType; switch (promptType) { case "alert": case "alertCheck": case "confirmCheck": case "confirm": case "confirmEx": PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 0 }); break; case "promptUserAndPass": // This is a login dialog, hopefully an authentication prompt // for the xpi. if (this.authenticationCallback) { var auth = this.authenticationCallback(); if (auth && auth.length == 2) { PromptTestUtils.handlePrompt(dialog, { loginInput: auth[0], passwordInput: auth[1], buttonNumClick: 0, }); } else { PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 1 }); } } else { PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 1 }); } break; default: ok(false, "prompt type " + promptType + " not handled in test."); break; } }, popupReady(panel) { if (this.installBlockedCallback) { ok(false, "Should have been blocked by the whitelist"); } this.pendingCount++; // If there is a confirm callback then its return status determines whether // to install the items or not. If not the test is over. let result = true; if (this.installConfirmCallback) { result = this.installConfirmCallback(panel); if (result === this.leaveOpen) { return; } } const panelEl = panel.closest("panel"); const panelState = panelEl.state; const clickButton = () => { info(`Clicking ${result ? "primary" : "secondary"} panel button`); Assert.equal( panelEl.state, "open", "Expect panel state to be open when clicking panel buttons" ); if (!result) { panel.secondaryButton.click(); } else { panel.button.click(); } }; if (panelState === "showing") { info( "panel is still showing, wait for 'popup-shown' topic to be notified" ); BrowserUtils.promiseObserved( "popup-shown", shownPanel => shownPanel === panelEl ).then(clickButton); } else { clickButton(); } }, handleEvent(event) { if (event.type === "popupshown") { if (event.target == event.view.PanelUI.notificationPanel) { event.view.PanelUI.notificationPanel.hidePopup(); } else if (event.target.firstElementChild) { let popupId = event.target.firstElementChild.getAttribute("popupid"); if (popupId === "addon-webext-permissions") { this.popupReady(event.target.firstElementChild); } else if (popupId === "addon-install-failed") { event.target.firstElementChild.button.click(); } } } }, // Install blocked handling installDisabled(installInfo) { ok( !!this.installDisabledCallback, "Installation shouldn't have been disabled" ); if (this.installDisabledCallback) { this.installDisabledCallback(installInfo); } this.expectingCancelled = true; this.expectingCancelled = false; this.endTest(); }, installCancelled(installInfo) { if (this.expectingCancelled) { return; } ok( !!this.installCancelledCallback, "Installation shouldn't have been cancelled" ); if (this.installCancelledCallback) { this.installCancelledCallback(installInfo); } this.endTest(); }, installOriginBlocked(installInfo) { ok(!!this.installOriginBlockedCallback, "Shouldn't have been blocked"); if (this.installOriginBlockedCallback) { this.installOriginBlockedCallback(installInfo); } this.endTest(); }, installBlocked(installInfo) { ok( !!this.installBlockedCallback, "Shouldn't have been blocked by the whitelist" ); if ( this.installBlockedCallback && this.installBlockedCallback(installInfo) ) { this.installBlockedCallback = null; installInfo.install(); } else { this.expectingCancelled = true; installInfo.installs.forEach(function (install) { install.cancel(); }); this.expectingCancelled = false; this.endTest(); } }, // Addon Install Listener onNewInstall(install) { this.runningInstalls.push(install); if (this.finalContentEvent && !this.waitingForEvent) { this.waitingForEvent = true; info("Waiting for " + this.finalContentEvent); BrowserTestUtils.waitForContentEvent( this._boundWin.get().gBrowser.selectedBrowser, this.finalContentEvent, true, null, true ).then(() => { info("Saw " + this.finalContentEvent + "," + this.waitingForEvent); this.waitingForEvent = false; if (this.pendingCount == 0) { this.endTest(); } }); } }, onDownloadStarted(install) { this.pendingCount++; if (this.downloadStartedCallback) { this.downloadStartedCallback(install); } }, onDownloadProgress(install) { if (this.downloadProgressCallback) { this.downloadProgressCallback(install); } }, onDownloadEnded(install) { if (this.downloadEndedCallback) { this.downloadEndedCallback(install); } }, onDownloadCancelled(install) { isnot( this.runningInstalls.indexOf(install), -1, "Should only see cancelations for started installs" ); this.runningInstalls.splice(this.runningInstalls.indexOf(install), 1); if ( this.downloadCancelledCallback && this.downloadCancelledCallback(install) === false ) { return; } this.checkTestEnded(); }, onDownloadFailed(install) { if (this.downloadFailedCallback) { this.downloadFailedCallback(install); } this.checkTestEnded(); }, onInstallStarted(install) { if (this.installStartedCallback) { this.installStartedCallback(install); } }, async onInstallEnded(install, addon) { this.installCount++; if (this.installEndedCallback) { await this.installEndedCallback(install, addon); } this.checkTestEnded(); }, onInstallFailed(install) { if (this.installFailedCallback) { this.installFailedCallback(install); } this.checkTestEnded(); }, onUninstalled(addon) { let idx = this.runningInstalls.findIndex(install => install.addon == addon); if (idx != -1) { this.runningInstalls.splice(idx, 1); this.checkTestEnded(); } }, onInstallCancelled(install) { // This is ugly. We have a bunch of tests that cancel installs // but don't expect this event to be raised. // For at least one test (browser_whitelist3.js), we used to generate // onDownloadCancelled when the user cancelled the installation at the // confirmation prompt. We're now generating onInstallCancelled instead // of onDownloadCancelled but making this code unconditional breaks a // bunch of other tests. Ugh. let idx = this.runningInstalls.indexOf(install); if (idx != -1) { this.runningInstalls.splice(this.runningInstalls.indexOf(install), 1); this.checkTestEnded(); } }, checkTestEnded() { if (--this.pendingCount == 0 && !this.waitingForEvent) { this.endTest(); } }, // nsIObserver observe(subject, topic) { var installInfo = subject.wrappedJSObject; switch (topic) { case "addon-install-started": is( this.runningInstalls.length, installInfo.installs.length, "Should have seen the expected number of installs started" ); break; case "addon-install-disabled": this.installDisabled(installInfo); break; case "addon-install-cancelled": this.installCancelled(installInfo); break; case "addon-install-origin-blocked": this.installOriginBlocked(installInfo); break; case "addon-install-blocked": this.installBlocked(installInfo); break; case "addon-install-failed": installInfo.installs.forEach(function (aInstall) { isnot( this.runningInstalls.indexOf(aInstall), -1, "Should only see failures for started installs" ); ok( aInstall.error != 0 || aInstall.addon.appDisabled, "Failed installs should have an error or be appDisabled" ); this.runningInstalls.splice( this.runningInstalls.indexOf(aInstall), 1 ); }, this); break; case "common-dialog-loaded": this.promptReady(subject.Dialog); break; } }, QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), };