michael@0: const RELATIVE_DIR = "toolkit/mozapps/extensions/test/xpinstall/"; michael@0: michael@0: const TESTROOT = "http://example.com/browser/" + RELATIVE_DIR; michael@0: const TESTROOT2 = "http://example.org/browser/" + RELATIVE_DIR; michael@0: const XPINSTALL_URL = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul"; michael@0: const PROMPT_URL = "chrome://global/content/commonDialog.xul"; michael@0: const ADDONS_URL = "chrome://mozapps/content/extensions/extensions.xul"; michael@0: const PREF_LOGGING_ENABLED = "extensions.logging.enabled"; michael@0: const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts"; michael@0: const CHROME_NAME = "mochikit"; michael@0: michael@0: function getChromeRoot(path) { michael@0: if (path === undefined) { michael@0: return "chrome://" + CHROME_NAME + "/content/browser/" + RELATIVE_DIR michael@0: } michael@0: return getRootDirectory(path); michael@0: } michael@0: michael@0: function extractChromeRoot(path) { michael@0: var chromeRootPath = getChromeRoot(path); michael@0: var jar = getJar(chromeRootPath); michael@0: if (jar) { michael@0: var tmpdir = extractJarToTmp(jar); michael@0: return "file://" + tmpdir.path + "/"; michael@0: } michael@0: return chromeRootPath; michael@0: } michael@0: michael@0: /** michael@0: * This is a test harness designed to handle responding to UI during the process michael@0: * of installing an XPI. A test can set callbacks to hear about specific parts michael@0: * of the sequence. michael@0: * Before use setup must be called and finish must be called afterwards. michael@0: */ michael@0: var Harness = { michael@0: // If set then the callback is called when an install is attempted and michael@0: // software installation is disabled. michael@0: installDisabledCallback: null, michael@0: // If set then the callback is called when an install is attempted and michael@0: // then canceled. michael@0: installCancelledCallback: null, michael@0: // If set then the callback will be called when an install is blocked by the michael@0: // whitelist. The callback should return true to continue with the install michael@0: // anyway. michael@0: installBlockedCallback: null, michael@0: // If set will be called in the event of authentication being needed to get michael@0: // the xpi. Should return a 2 element array of username and password, or michael@0: // null to not authenticate. michael@0: authenticationCallback: null, michael@0: // If set this will be called to allow checking the contents of the xpinstall michael@0: // confirmation dialog. The callback should return true to continue the install. michael@0: installConfirmCallback: null, michael@0: // If set will be called when downloading of an item has begun. michael@0: downloadStartedCallback: null, michael@0: // If set will be called during the download of an item. michael@0: downloadProgressCallback: null, michael@0: // If set will be called when an xpi fails to download. michael@0: downloadFailedCallback: null, michael@0: // If set will be called when an xpi download is cancelled. michael@0: downloadCancelledCallback: null, michael@0: // If set will be called when downloading of an item has ended. michael@0: downloadEndedCallback: null, michael@0: // If set will be called when installation by the extension manager of an xpi michael@0: // item starts michael@0: installStartedCallback: null, michael@0: // If set will be called when an xpi fails to install. michael@0: installFailedCallback: null, michael@0: // If set will be called when each xpi item to be installed completes michael@0: // installation. michael@0: installEndedCallback: null, michael@0: // If set will be called when all triggered items are installed or the install michael@0: // is canceled. michael@0: installsCompletedCallback: null, michael@0: michael@0: pendingCount: null, michael@0: installCount: null, michael@0: runningInstalls: null, michael@0: michael@0: waitingForFinish: false, michael@0: michael@0: // Setup and tear down functions michael@0: setup: function() { michael@0: if (!this.waitingForFinish) { michael@0: waitForExplicitFinish(); michael@0: this.waitingForFinish = true; michael@0: michael@0: Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true); michael@0: Services.obs.addObserver(this, "addon-install-started", false); michael@0: Services.obs.addObserver(this, "addon-install-disabled", false); michael@0: Services.obs.addObserver(this, "addon-install-blocked", false); michael@0: Services.obs.addObserver(this, "addon-install-failed", false); michael@0: Services.obs.addObserver(this, "addon-install-complete", false); michael@0: michael@0: AddonManager.addInstallListener(this); michael@0: michael@0: Services.wm.addListener(this); michael@0: michael@0: var self = this; michael@0: registerCleanupFunction(function() { michael@0: Services.prefs.clearUserPref(PREF_LOGGING_ENABLED); michael@0: Services.obs.removeObserver(self, "addon-install-started"); michael@0: Services.obs.removeObserver(self, "addon-install-disabled"); michael@0: Services.obs.removeObserver(self, "addon-install-blocked"); michael@0: Services.obs.removeObserver(self, "addon-install-failed"); michael@0: Services.obs.removeObserver(self, "addon-install-complete"); michael@0: michael@0: AddonManager.removeInstallListener(self); michael@0: michael@0: Services.wm.removeListener(self); michael@0: michael@0: AddonManager.getAllInstalls(function(aInstalls) { michael@0: is(aInstalls.length, 0, "Should be no active installs at the end of the test"); michael@0: aInstalls.forEach(function(aInstall) { michael@0: info("Install for " + aInstall.sourceURI + " is in state " + aInstall.state); michael@0: aInstall.cancel(); michael@0: }); michael@0: }); michael@0: }); michael@0: } michael@0: michael@0: this.installCount = 0; michael@0: this.pendingCount = 0; michael@0: this.runningInstalls = []; michael@0: }, michael@0: michael@0: finish: function() { michael@0: finish(); michael@0: }, michael@0: michael@0: endTest: function() { michael@0: // Defer the final notification to allow things like the InstallTrigger michael@0: // callback to complete michael@0: var self = this; michael@0: executeSoon(function() { michael@0: let callback = self.installsCompletedCallback; michael@0: let count = self.installCount; michael@0: michael@0: is(self.runningInstalls.length, 0, "Should be no running installs left"); michael@0: self.runningInstalls.forEach(function(aInstall) { michael@0: info("Install for " + aInstall.sourceURI + " is in state " + aInstall.state); michael@0: }); michael@0: michael@0: self.installBlockedCallback = null; michael@0: self.authenticationCallback = null; michael@0: self.installConfirmCallback = null; michael@0: self.downloadStartedCallback = null; michael@0: self.downloadProgressCallback = null; michael@0: self.downloadCancelledCallback = null; michael@0: self.downloadFailedCallback = null; michael@0: self.downloadEndedCallback = null; michael@0: self.installStartedCallback = null; michael@0: self.installFailedCallback = null; michael@0: self.installEndedCallback = null; michael@0: self.installsCompletedCallback = null; michael@0: self.runningInstalls = null; michael@0: michael@0: if (callback) michael@0: callback(count); michael@0: }); michael@0: }, michael@0: michael@0: // Window open handling michael@0: windowReady: function(window) { michael@0: if (window.document.location.href == XPINSTALL_URL) { michael@0: if (this.installBlockedCallback) michael@0: ok(false, "Should have been blocked by the whitelist"); michael@0: this.pendingCount = window.document.getElementById("itemList").childNodes.length; michael@0: michael@0: // If there is a confirm callback then its return status determines whether michael@0: // to install the items or not. If not the test is over. michael@0: if (this.installConfirmCallback && !this.installConfirmCallback(window)) { michael@0: window.document.documentElement.cancelDialog(); michael@0: } michael@0: else { michael@0: // Initially the accept button is disabled on a countdown timer michael@0: var button = window.document.documentElement.getButton("accept"); michael@0: button.disabled = false; michael@0: window.document.documentElement.acceptDialog(); michael@0: } michael@0: } michael@0: else if (window.document.location.href == PROMPT_URL) { michael@0: var promptType = window.args.promptType; michael@0: switch (promptType) { michael@0: case "alert": michael@0: case "alertCheck": michael@0: case "confirmCheck": michael@0: case "confirm": michael@0: case "confirmEx": michael@0: window.document.documentElement.acceptDialog(); michael@0: break; michael@0: case "promptUserAndPass": michael@0: // This is a login dialog, hopefully an authentication prompt michael@0: // for the xpi. michael@0: if (this.authenticationCallback) { michael@0: var auth = this.authenticationCallback(); michael@0: if (auth && auth.length == 2) { michael@0: window.document.getElementById("loginTextbox").value = auth[0]; michael@0: window.document.getElementById("password1Textbox").value = auth[1]; michael@0: window.document.documentElement.acceptDialog(); michael@0: } michael@0: else { michael@0: window.document.documentElement.cancelDialog(); michael@0: } michael@0: } michael@0: else { michael@0: window.document.documentElement.cancelDialog(); michael@0: } michael@0: break; michael@0: default: michael@0: ok(false, "prompt type " + promptType + " not handled in test."); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: // Install blocked handling michael@0: michael@0: installDisabled: function(installInfo) { michael@0: ok(!!this.installDisabledCallback, "Installation shouldn't have been disabled"); michael@0: if (this.installDisabledCallback) michael@0: this.installDisabledCallback(installInfo); michael@0: this.expectingCancelled = true; michael@0: installInfo.installs.forEach(function(install) { michael@0: install.cancel(); michael@0: }); michael@0: this.expectingCancelled = false; michael@0: this.endTest(); michael@0: }, michael@0: michael@0: installCancelled: function(installInfo) { michael@0: if (this.expectingCancelled) michael@0: return; michael@0: michael@0: ok(!!this.installCancelledCallback, "Installation shouldn't have been cancelled"); michael@0: if (this.installCancelledCallback) michael@0: this.installCancelledCallback(installInfo); michael@0: this.endTest(); michael@0: }, michael@0: michael@0: installBlocked: function(installInfo) { michael@0: ok(!!this.installBlockedCallback, "Shouldn't have been blocked by the whitelist"); michael@0: if (this.installBlockedCallback && this.installBlockedCallback(installInfo)) { michael@0: this.installBlockedCallback = null; michael@0: installInfo.install(); michael@0: } michael@0: else { michael@0: this.expectingCancelled = true; michael@0: installInfo.installs.forEach(function(install) { michael@0: install.cancel(); michael@0: }); michael@0: this.expectingCancelled = false; michael@0: this.endTest(); michael@0: } michael@0: }, michael@0: michael@0: // nsIWindowMediatorListener michael@0: michael@0: onWindowTitleChange: function(window, title) { michael@0: }, michael@0: michael@0: onOpenWindow: function(window) { michael@0: var domwindow = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) michael@0: .getInterface(Components.interfaces.nsIDOMWindow); michael@0: var self = this; michael@0: waitForFocus(function() { michael@0: self.windowReady(domwindow); michael@0: }, domwindow); michael@0: }, michael@0: michael@0: onCloseWindow: function(window) { michael@0: }, michael@0: michael@0: // Addon Install Listener michael@0: michael@0: onNewInstall: function(install) { michael@0: this.runningInstalls.push(install); michael@0: }, michael@0: michael@0: onDownloadStarted: function(install) { michael@0: this.pendingCount++; michael@0: if (this.downloadStartedCallback) michael@0: this.downloadStartedCallback(install); michael@0: }, michael@0: michael@0: onDownloadProgress: function(install) { michael@0: if (this.downloadProgressCallback) michael@0: this.downloadProgressCallback(install); michael@0: }, michael@0: michael@0: onDownloadEnded: function(install) { michael@0: if (this.downloadEndedCallback) michael@0: this.downloadEndedCallback(install); michael@0: }, michael@0: michael@0: onDownloadCancelled: function(install) { michael@0: isnot(this.runningInstalls.indexOf(install), -1, michael@0: "Should only see cancelations for started installs"); michael@0: this.runningInstalls.splice(this.runningInstalls.indexOf(install), 1); michael@0: michael@0: if (this.downloadCancelledCallback) michael@0: this.downloadCancelledCallback(install); michael@0: this.checkTestEnded(); michael@0: }, michael@0: michael@0: onDownloadFailed: function(install) { michael@0: if (this.downloadFailedCallback) michael@0: this.downloadFailedCallback(install); michael@0: this.checkTestEnded(); michael@0: }, michael@0: michael@0: onInstallStarted: function(install) { michael@0: if (this.installStartedCallback) michael@0: this.installStartedCallback(install); michael@0: }, michael@0: michael@0: onInstallEnded: function(install, addon) { michael@0: if (this.installEndedCallback) michael@0: this.installEndedCallback(install, addon); michael@0: this.installCount++; michael@0: this.checkTestEnded(); michael@0: }, michael@0: michael@0: onInstallFailed: function(install) { michael@0: if (this.installFailedCallback) michael@0: this.installFailedCallback(install); michael@0: this.checkTestEnded(); michael@0: }, michael@0: michael@0: checkTestEnded: function() { michael@0: if (--this.pendingCount == 0) michael@0: this.endTest(); michael@0: }, michael@0: michael@0: // nsIObserver michael@0: michael@0: observe: function(subject, topic, data) { michael@0: var installInfo = subject.QueryInterface(Components.interfaces.amIWebInstallInfo); michael@0: switch (topic) { michael@0: case "addon-install-started": michael@0: is(this.runningInstalls.length, installInfo.installs.length, michael@0: "Should have seen the expected number of installs started"); michael@0: break; michael@0: case "addon-install-disabled": michael@0: this.installDisabled(installInfo); michael@0: break; michael@0: case "addon-install-cancelled": michael@0: this.installCancelled(installInfo); michael@0: break; michael@0: case "addon-install-blocked": michael@0: this.installBlocked(installInfo); michael@0: break; michael@0: case "addon-install-failed": michael@0: installInfo.installs.forEach(function(aInstall) { michael@0: isnot(this.runningInstalls.indexOf(aInstall), -1, michael@0: "Should only see failures for started installs"); michael@0: michael@0: ok(aInstall.error != 0 || aInstall.addon.appDisabled, michael@0: "Failed installs should have an error or be appDisabled"); michael@0: michael@0: this.runningInstalls.splice(this.runningInstalls.indexOf(aInstall), 1); michael@0: }, this); michael@0: break; michael@0: case "addon-install-complete": michael@0: installInfo.installs.forEach(function(aInstall) { michael@0: isnot(this.runningInstalls.indexOf(aInstall), -1, michael@0: "Should only see completed events for started installs"); michael@0: michael@0: is(aInstall.error, 0, "Completed installs should have no error"); michael@0: ok(!aInstall.appDisabled, "Completed installs should not be appDisabled"); michael@0: michael@0: // Complete installs are either in the INSTALLED or CANCELLED state michael@0: // since the test may cancel installs the moment they complete. michael@0: ok(aInstall.state == AddonManager.STATE_INSTALLED || michael@0: aInstall.state == AddonManager.STATE_CANCELLED, michael@0: "Completed installs should be in the right state"); michael@0: michael@0: this.runningInstalls.splice(this.runningInstalls.indexOf(aInstall), 1); michael@0: }, this); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, michael@0: Ci.nsIWindowMediatorListener, michael@0: Ci.nsISupports]) michael@0: }