/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ /* globals end_test */ /* eslint no-unused-vars: ["error", {vars: "local", args: "none"}] */ const { TelemetryTestUtils } = ChromeUtils.importESModule( "resource://testing-common/TelemetryTestUtils.sys.mjs" ); let { AddonManagerPrivate } = ChromeUtils.importESModule( "resource://gre/modules/AddonManager.sys.mjs" ); var pathParts = gTestPath.split("/"); // Drop the test filename pathParts.splice(pathParts.length - 1, pathParts.length); const RELATIVE_DIR = pathParts.slice(4).join("/") + "/"; const TESTROOT = "http://example.com/" + RELATIVE_DIR; const SECURE_TESTROOT = "https://example.com/" + RELATIVE_DIR; const TESTROOT2 = "http://example.org/" + RELATIVE_DIR; const SECURE_TESTROOT2 = "https://example.org/" + RELATIVE_DIR; const CHROMEROOT = pathParts.join("/") + "/"; const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane"; const PREF_XPI_ENABLED = "xpinstall.enabled"; const PREF_UPDATEURL = "extensions.update.url"; const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled"; const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory"; const MANAGER_URI = "about:addons"; const PREF_LOGGING_ENABLED = "extensions.logging.enabled"; const PREF_STRICT_COMPAT = "extensions.strictCompatibility"; var PREF_CHECK_COMPATIBILITY; (function () { var channel = Services.prefs.getCharPref("app.update.channel", "default"); if ( channel != "aurora" && channel != "beta" && channel != "release" && channel != "esr" ) { var version = "nightly"; } else { version = Services.appinfo.version.replace( /^([^\.]+\.[0-9]+[a-z]*).*/gi, "$1" ); } PREF_CHECK_COMPATIBILITY = "extensions.checkCompatibility." + version; })(); var gPendingTests = []; var gTestsRun = 0; var gTestStart = null; var gRestorePrefs = [ { name: PREF_LOGGING_ENABLED }, { name: "extensions.webservice.discoverURL" }, { name: "extensions.update.url" }, { name: "extensions.update.background.url" }, { name: "extensions.update.enabled" }, { name: "extensions.update.autoUpdateDefault" }, { name: "extensions.getAddons.get.url" }, { name: "extensions.getAddons.getWithPerformance.url" }, { name: "extensions.getAddons.cache.enabled" }, { name: "devtools.chrome.enabled" }, { name: PREF_STRICT_COMPAT }, { name: PREF_CHECK_COMPATIBILITY }, ]; for (let pref of gRestorePrefs) { if (!Services.prefs.prefHasUserValue(pref.name)) { pref.type = "clear"; continue; } pref.type = Services.prefs.getPrefType(pref.name); if (pref.type == Services.prefs.PREF_BOOL) { pref.value = Services.prefs.getBoolPref(pref.name); } else if (pref.type == Services.prefs.PREF_INT) { pref.value = Services.prefs.getIntPref(pref.name); } else if (pref.type == Services.prefs.PREF_STRING) { pref.value = Services.prefs.getCharPref(pref.name); } } // Turn logging on for all tests Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true); function promiseFocus(window) { return new Promise(resolve => waitForFocus(resolve, window)); } // Tools to disable and re-enable the background update and blocklist timers // so that tests can protect themselves from unwanted timer events. var gCatMan = Services.catMan; // Default value from toolkit/mozapps/extensions/extensions.manifest, but disable*UpdateTimer() // records the actual value so we can put it back in enable*UpdateTimer() var backgroundUpdateConfig = "@mozilla.org/addons/integration;1,getService,addon-background-update-timer,extensions.update.interval,86400"; var UTIMER = "update-timer"; var AMANAGER = "addonManager"; var BLOCKLIST = "nsBlocklistService"; function disableBackgroundUpdateTimer() { info("Disabling " + UTIMER + " " + AMANAGER); backgroundUpdateConfig = gCatMan.getCategoryEntry(UTIMER, AMANAGER); gCatMan.deleteCategoryEntry(UTIMER, AMANAGER, true); } function enableBackgroundUpdateTimer() { info("Enabling " + UTIMER + " " + AMANAGER); gCatMan.addCategoryEntry( UTIMER, AMANAGER, backgroundUpdateConfig, false, true ); } registerCleanupFunction(function () { // Restore prefs for (let pref of gRestorePrefs) { if (pref.type == "clear") { Services.prefs.clearUserPref(pref.name); } else if (pref.type == Services.prefs.PREF_BOOL) { Services.prefs.setBoolPref(pref.name, pref.value); } else if (pref.type == Services.prefs.PREF_INT) { Services.prefs.setIntPref(pref.name, pref.value); } else if (pref.type == Services.prefs.PREF_STRING) { Services.prefs.setCharPref(pref.name, pref.value); } } return AddonManager.getAllInstalls().then(aInstalls => { for (let install of aInstalls) { if (install instanceof MockInstall) { continue; } ok( false, "Should not have seen an install of " + install.sourceURI.spec + " in state " + install.state ); install.cancel(); } }); }); function log_exceptions(aCallback, ...aArgs) { try { return aCallback.apply(null, aArgs); } catch (e) { info("Exception thrown: " + e); throw e; } } function log_callback(aPromise, aCallback) { aPromise.then(aCallback).catch(e => info("Exception thrown: " + e)); return aPromise; } function add_test(test) { gPendingTests.push(test); } function run_next_test() { // Make sure we're not calling run_next_test from inside an add_task() test // We're inside the browser_test.js 'testScope' here if (this.__tasks) { throw new Error( "run_next_test() called from an add_task() test function. " + "run_next_test() should not be called from inside add_task() " + "under any circumstances!" ); } if (gTestsRun > 0) { info("Test " + gTestsRun + " took " + (Date.now() - gTestStart) + "ms"); } if (!gPendingTests.length) { executeSoon(end_test); return; } gTestsRun++; var test = gPendingTests.shift(); if (test.name) { info("Running test " + gTestsRun + " (" + test.name + ")"); } else { info("Running test " + gTestsRun); } gTestStart = Date.now(); executeSoon(() => log_exceptions(test)); } var get_tooltip_info = async function (addonEl, managerWindow) { // Extract from title attribute. const { addon } = addonEl; const name = addon.name; let nameWithVersion = addonEl.addonNameEl.title; if (addonEl.addon.userDisabled) { // TODO - Bug 1558077: Currently Fluent is clearing the addon title // when the addon is disabled, fixing it requires changes to the // HTML about:addons localized strings, and then remove this // workaround. nameWithVersion = `${name} ${addon.version}`; } return { name, version: nameWithVersion.substring(name.length + 1), }; }; function get_addon_file_url(aFilename) { try { var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( Ci.nsIChromeRegistry ); var fileurl = cr.convertChromeURL( makeURI(CHROMEROOT + "addons/" + aFilename) ); return fileurl.QueryInterface(Ci.nsIFileURL); } catch (ex) { var jar = getJar(CHROMEROOT + "addons/" + aFilename); var tmpDir = extractJarToTmp(jar); tmpDir.append(aFilename); return Services.io.newFileURI(tmpDir).QueryInterface(Ci.nsIFileURL); } } function check_all_in_list(aManager, aIds, aIgnoreExtras) { var doc = aManager.document; var list = doc.getElementById("addon-list"); var inlist = []; var node = list.firstChild; while (node) { if (node.value) { inlist.push(node.value); } node = node.nextSibling; } for (let id of aIds) { if (!inlist.includes(id)) { ok(false, "Should find " + id + " in the list"); } } if (aIgnoreExtras) { return; } for (let inlistItem of inlist) { if (!aIds.includes(inlistItem)) { ok(false, "Shouldn't have seen " + inlistItem + " in the list"); } } } function getAddonCard(win, id) { return win.document.querySelector(`addon-card[addon-id="${id}"]`); } async function wait_for_view_load( aManagerWindow, aCallback, aForceWait, aLongerTimeout ) { // Wait one tick to make sure that the microtask related to an // async loadView call originated from outsite about:addons // is already executing (otherwise isLoading would be still false // and we wouldn't be waiting for that load before resolving // the promise returned by this test helper function). await Promise.resolve(); let p = new Promise(resolve => { requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2); if (!aForceWait && !aManagerWindow.gViewController.isLoading) { resolve(aManagerWindow); return; } aManagerWindow.document.addEventListener( "view-loaded", function () { resolve(aManagerWindow); }, { once: true } ); }); return log_callback(p, aCallback); } function wait_for_manager_load(aManagerWindow, aCallback) { info("Waiting for initialization"); return log_callback( aManagerWindow.promiseInitialized.then(() => aManagerWindow), aCallback ); } function open_manager( aView, aCallback, aLoadCallback, aLongerTimeout, aWin = window ) { let p = new Promise((resolve, reject) => { async function setup_manager(aManagerWindow) { if (aLoadCallback) { log_exceptions(aLoadCallback, aManagerWindow); } if (aView) { aManagerWindow.loadView(aView); } ok(aManagerWindow != null, "Should have an add-ons manager window"); is( aManagerWindow.location.href, MANAGER_URI, "Should be displaying the correct UI" ); await promiseFocus(aManagerWindow); info("window has focus, waiting for manager load"); await wait_for_manager_load(aManagerWindow); info("Manager waiting for view load"); await wait_for_view_load(aManagerWindow, null, null, aLongerTimeout); resolve(aManagerWindow); } info("Loading manager window in tab"); Services.obs.addObserver(function observer(aSubject, aTopic, aData) { Services.obs.removeObserver(observer, aTopic); if (aSubject.location.href != MANAGER_URI) { info("Ignoring load event for " + aSubject.location.href); return; } setup_manager(aSubject); }, "EM-loaded"); aWin.gBrowser.selectedTab = BrowserTestUtils.addTab(aWin.gBrowser); aWin.switchToTabHavingURI(MANAGER_URI, true, { triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), }); }); // The promise resolves with the manager window, so it is passed to the callback return log_callback(p, aCallback); } function close_manager(aManagerWindow, aCallback, aLongerTimeout) { let p = new Promise((resolve, reject) => { requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2); ok( aManagerWindow != null, "Should have an add-ons manager window to close" ); is( aManagerWindow.location.href, MANAGER_URI, "Should be closing window with correct URI" ); aManagerWindow.addEventListener("unload", function listener() { try { dump("Manager window unload handler\n"); this.removeEventListener("unload", listener); resolve(); } catch (e) { reject(e); } }); }); info("Telling manager window to close"); aManagerWindow.close(); info("Manager window close() call returned"); return log_callback(p, aCallback); } function restart_manager(aManagerWindow, aView, aCallback, aLoadCallback) { if (!aManagerWindow) { return open_manager(aView, aCallback, aLoadCallback); } return close_manager(aManagerWindow).then(() => open_manager(aView, aCallback, aLoadCallback) ); } function wait_for_window_open(aCallback) { let p = new Promise(resolve => { Services.wm.addListener({ onOpenWindow(aXulWin) { Services.wm.removeListener(this); let domwindow = aXulWin.docShell.domWindow; domwindow.addEventListener( "load", function () { executeSoon(function () { resolve(domwindow); }); }, { once: true } ); }, onCloseWindow(aWindow) {}, }); }); return log_callback(p, aCallback); } function formatDate(aDate) { const dtOptions = { year: "numeric", month: "long", day: "numeric" }; return aDate.toLocaleDateString(undefined, dtOptions); } function is_hidden(aElement) { var style = aElement.ownerGlobal.getComputedStyle(aElement); if (style.display == "none") { return true; } if (style.visibility != "visible") { return true; } // Hiding a parent element will hide all its children if (aElement.parentNode != aElement.ownerDocument) { return is_hidden(aElement.parentNode); } return false; } function is_element_visible(aElement, aMsg) { isnot(aElement, null, "Element should not be null, when checking visibility"); ok(!is_hidden(aElement), aMsg || aElement + " should be visible"); } function is_element_hidden(aElement, aMsg) { isnot(aElement, null, "Element should not be null, when checking visibility"); ok(is_hidden(aElement), aMsg || aElement + " should be hidden"); } function promiseAddonByID(aId) { return AddonManager.getAddonByID(aId); } function promiseAddonsByIDs(aIDs) { return AddonManager.getAddonsByIDs(aIDs); } /** * Install an add-on and call a callback when complete. * * The callback will receive the Addon for the installed add-on. */ async function install_addon(path, cb, pathPrefix = TESTROOT) { let install = await AddonManager.getInstallForURL(pathPrefix + path); let p = new Promise((resolve, reject) => { install.addListener({ onInstallEnded: () => resolve(install.addon), }); install.install(); }); return log_callback(p, cb); } function CategoryUtilities(aManagerWindow) { this.window = aManagerWindow; this.window.addEventListener("unload", () => (this.window = null), { once: true, }); } CategoryUtilities.prototype = { window: null, get _categoriesBox() { return this.window.document.querySelector("categories-box"); }, getSelectedViewId() { let selectedItem = this._categoriesBox.querySelector("[selected]"); isnot(selectedItem, null, "A category should be selected"); return selectedItem.getAttribute("viewid"); }, get selectedCategory() { isnot( this.window, null, "Should not get selected category when manager window is not loaded" ); let viewId = this.getSelectedViewId(); let view = this.window.gViewController.parseViewId(viewId); return view.type == "list" ? view.param : view.type; }, get(categoryType) { isnot( this.window, null, "Should not get category when manager window is not loaded" ); let button = this._categoriesBox.querySelector(`[name="${categoryType}"]`); if (button) { return button; } ok(false, "Should have found a category with type " + categoryType); return null; }, isVisible(categoryButton) { isnot( this.window, null, "Should not check visible state when manager window is not loaded" ); // There are some tests checking this before the categories have loaded. if (!categoryButton) { return false; } if (categoryButton.disabled || categoryButton.hidden) { return false; } return !is_hidden(categoryButton); }, isTypeVisible(categoryType) { return this.isVisible(this.get(categoryType)); }, open(categoryButton) { isnot( this.window, null, "Should not open category when manager window is not loaded" ); ok( this.isVisible(categoryButton), "Category should be visible if attempting to open it" ); EventUtils.synthesizeMouseAtCenter(categoryButton, {}, this.window); // Use wait_for_view_load until all open_manager calls are gone. return wait_for_view_load(this.window); }, openType(categoryType) { return this.open(this.get(categoryType)); }, }; // Returns a promise that will resolve when the certificate error override has been added, or reject // if there is some failure. function addCertOverride(host) { return new Promise((resolve, reject) => { let req = new XMLHttpRequest(); req.open("GET", "https://" + host + "/"); req.onload = reject; req.onerror = () => { if (req.channel && req.channel.securityInfo) { let securityInfo = req.channel.securityInfo; if (securityInfo.serverCert) { let cos = Cc["@mozilla.org/security/certoverride;1"].getService( Ci.nsICertOverrideService ); cos.rememberValidityOverride( host, -1, {}, securityInfo.serverCert, false ); resolve(); return; } } reject(); }; req.send(null); }); } // Returns a promise that will resolve when the necessary certificate overrides have been added. function addCertOverrides() { return Promise.all([ addCertOverride("nocert.example.com"), addCertOverride("self-signed.example.com"), addCertOverride("untrusted.example.com"), addCertOverride("expired.example.com"), ]); } /** *** Mock Provider *****/ function MockProvider(addonTypes) { this.addons = []; this.installs = []; this.addonTypes = addonTypes ?? ["extension"]; var self = this; registerCleanupFunction(function () { if (self.started) { self.unregister(); } }); this.register(); } MockProvider.prototype = { addons: null, installs: null, addonTypes: null, started: null, queryDelayPromise: Promise.resolve(), blockQueryResponses() { this.queryDelayPromise = new Promise(resolve => { this._unblockQueries = resolve; }); }, unblockQueryResponses() { if (this._unblockQueries) { this._unblockQueries(); this._unblockQueries = null; } else { throw new Error("Queries are not blocked"); } }, /** *** Utility functions *****/ /** * Register this provider with the AddonManager */ register: function MP_register() { info("Registering mock add-on provider"); // addonTypes is supposedly the full set of types supported by the provider. // The current list is not complete (there are tests that mock add-on types // other than "extension"), but it doesn't affect tests since addonTypes is // mainly used to determine whether any of the AddonManager's providers // support a type, and XPIProvider already defines the types of interest. AddonManagerPrivate.registerProvider(this, this.addonTypes); }, /** * Unregister this provider with the AddonManager */ unregister: function MP_unregister() { info("Unregistering mock add-on provider"); AddonManagerPrivate.unregisterProvider(this); }, /** * Adds an add-on to the list of add-ons that this provider exposes to the * AddonManager, dispatching appropriate events in the process. * * @param aAddon * The add-on to add */ addAddon: function MP_addAddon(aAddon) { var oldAddons = this.addons.filter(aOldAddon => aOldAddon.id == aAddon.id); var oldAddon = oldAddons.length ? oldAddons[0] : null; this.addons = this.addons.filter(aOldAddon => aOldAddon.id != aAddon.id); this.addons.push(aAddon); aAddon._provider = this; if (!this.started) { return; } let requiresRestart = (aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_INSTALL) != 0; AddonManagerPrivate.callInstallListeners( "onExternalInstall", null, aAddon, oldAddon, requiresRestart ); }, /** * Removes an add-on from the list of add-ons that this provider exposes to * the AddonManager, dispatching the onUninstalled event in the process. * * @param aAddon * The add-on to add */ removeAddon: function MP_removeAddon(aAddon) { var pos = this.addons.indexOf(aAddon); if (pos == -1) { ok( false, "Tried to remove an add-on that wasn't registered with the mock provider" ); return; } this.addons.splice(pos, 1); if (!this.started) { return; } AddonManagerPrivate.callAddonListeners("onUninstalled", aAddon); }, /** * Adds an add-on install to the list of installs that this provider exposes * to the AddonManager, dispatching appropriate events in the process. * * @param aInstall * The add-on install to add */ addInstall: function MP_addInstall(aInstall) { this.installs.push(aInstall); aInstall._provider = this; if (!this.started) { return; } aInstall.callListeners("onNewInstall"); }, removeInstall: function MP_removeInstall(aInstall) { var pos = this.installs.indexOf(aInstall); if (pos == -1) { ok( false, "Tried to remove an install that wasn't registered with the mock provider" ); return; } this.installs.splice(pos, 1); }, /** * Creates a set of mock add-on objects and adds them to the list of add-ons * managed by this provider. * * @param aAddonProperties * An array of objects containing properties describing the add-ons * @return Array of the new MockAddons */ createAddons: function MP_createAddons(aAddonProperties) { var newAddons = []; for (let addonProp of aAddonProperties) { let addon = new MockAddon(addonProp.id); for (let prop in addonProp) { if (prop == "id") { continue; } if (prop == "applyBackgroundUpdates") { addon._applyBackgroundUpdates = addonProp[prop]; } else if (prop == "appDisabled") { addon._appDisabled = addonProp[prop]; } else if (prop == "userDisabled") { addon.setUserDisabled(addonProp[prop]); } else { addon[prop] = addonProp[prop]; } } if (!addon.optionsType && !!addon.optionsURL) { addon.optionsType = AddonManager.OPTIONS_TYPE_DIALOG; } // Make sure the active state matches the passed in properties addon.isActive = addon.shouldBeActive; this.addAddon(addon); newAddons.push(addon); } return newAddons; }, /** * Creates a set of mock add-on install objects and adds them to the list * of installs managed by this provider. * * @param aInstallProperties * An array of objects containing properties describing the installs * @return Array of the new MockInstalls */ createInstalls: function MP_createInstalls(aInstallProperties) { var newInstalls = []; for (let installProp of aInstallProperties) { let install = new MockInstall( installProp.name || null, installProp.type || null, null ); for (let prop in installProp) { switch (prop) { case "name": case "type": break; case "sourceURI": install[prop] = NetUtil.newURI(installProp[prop]); break; default: install[prop] = installProp[prop]; } } this.addInstall(install); newInstalls.push(install); } return newInstalls; }, /** *** AddonProvider implementation *****/ /** * Called to initialize the provider. */ startup: function MP_startup() { this.started = true; }, /** * Called when the provider should shutdown. */ shutdown: function MP_shutdown() { this.started = false; }, /** * Called to get an Addon with a particular ID. * * @param aId * The ID of the add-on to retrieve */ async getAddonByID(aId) { await this.queryDelayPromise; for (let addon of this.addons) { if (addon.id == aId) { return addon; } } return null; }, /** * Called to get Addons of a particular type. * * @param aTypes * An array of types to fetch. Can be null to get all types. */ async getAddonsByTypes(aTypes) { await this.queryDelayPromise; var addons = this.addons.filter(function (aAddon) { if (aTypes && !!aTypes.length && !aTypes.includes(aAddon.type)) { return false; } return true; }); return addons; }, /** * Called to get the current AddonInstalls, optionally restricting by type. * * @param aTypes * An array of types or null to get all types */ async getInstallsByTypes(aTypes) { await this.queryDelayPromise; var installs = this.installs.filter(function (aInstall) { // Appear to have actually removed cancelled installs from the provider if (aInstall.state == AddonManager.STATE_CANCELLED) { return false; } if (aTypes && !!aTypes.length && !aTypes.includes(aInstall.type)) { return false; } return true; }); return installs; }, /** * Called when a new add-on has been enabled when only one add-on of that type * can be enabled. * * @param aId * The ID of the newly enabled add-on * @param aType * The type of the newly enabled add-on * @param aPendingRestart * true if the newly enabled add-on will only become enabled after a * restart */ addonChanged: function MP_addonChanged(aId, aType, aPendingRestart) { // Not implemented }, /** * Update the appDisabled property for all add-ons. */ updateAddonAppDisabledStates: function MP_updateAddonAppDisabledStates() { // Not needed }, /** * Called to get an AddonInstall to download and install an add-on from a URL. * * @param {string} aUrl * The URL to be installed * @param {object} aOptions * Options for the install */ getInstallForURL: function MP_getInstallForURL(aUrl, aOptions) { // Not yet implemented }, /** * Called to get an AddonInstall to install an add-on from a local file. * * @param aFile * The file to be installed */ getInstallForFile: function MP_getInstallForFile(aFile) { // Not yet implemented }, /** * Called to test whether installing add-ons is enabled. * * @return true if installing is enabled */ isInstallEnabled: function MP_isInstallEnabled() { return false; }, /** * Called to test whether this provider supports installing a particular * mimetype. * * @param aMimetype * The mimetype to check for * @return true if the mimetype is supported */ supportsMimetype: function MP_supportsMimetype(aMimetype) { return false; }, /** * Called to test whether installing add-ons from a URI is allowed. * * @param aUri * The URI being installed from * @return true if installing is allowed */ isInstallAllowed: function MP_isInstallAllowed(aUri) { return false; }, }; /** *** Mock Addon object for the Mock Provider *****/ function MockAddon(aId, aName, aType, aOperationsRequiringRestart) { // Only set required attributes. this.id = aId || ""; this.name = aName || ""; this.type = aType || "extension"; this.version = ""; this.isCompatible = true; this.providesUpdatesSecurely = true; this.blocklistState = 0; this._appDisabled = false; this._userDisabled = false; this._applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE; this.scope = AddonManager.SCOPE_PROFILE; this.isActive = true; this.creator = ""; this.pendingOperations = 0; this._permissions = AddonManager.PERM_CAN_UNINSTALL | AddonManager.PERM_CAN_ENABLE | AddonManager.PERM_CAN_DISABLE | AddonManager.PERM_CAN_UPGRADE | AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS; this.operationsRequiringRestart = aOperationsRequiringRestart != undefined ? aOperationsRequiringRestart : AddonManager.OP_NEEDS_RESTART_INSTALL | AddonManager.OP_NEEDS_RESTART_UNINSTALL | AddonManager.OP_NEEDS_RESTART_ENABLE | AddonManager.OP_NEEDS_RESTART_DISABLE; } MockAddon.prototype = { get isCorrectlySigned() { if (this.signedState === AddonManager.SIGNEDSTATE_NOT_REQUIRED) { return true; } return this.signedState > AddonManager.SIGNEDSTATE_MISSING; }, get shouldBeActive() { return ( !this.appDisabled && !this._userDisabled && !(this.pendingOperations & AddonManager.PENDING_UNINSTALL) ); }, get appDisabled() { return this._appDisabled; }, set appDisabled(val) { if (val == this._appDisabled) { return; } AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, [ "appDisabled", ]); var currentActive = this.shouldBeActive; this._appDisabled = val; var newActive = this.shouldBeActive; this._updateActiveState(currentActive, newActive); }, get userDisabled() { return this._userDisabled; }, set userDisabled(val) { throw new Error("No. Bad."); }, setUserDisabled(val) { if (val == this._userDisabled) { return; } var currentActive = this.shouldBeActive; this._userDisabled = val; var newActive = this.shouldBeActive; this._updateActiveState(currentActive, newActive); }, async enable() { await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); this.setUserDisabled(false); }, async disable() { await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); this.setUserDisabled(true); }, get permissions() { let permissions = this._permissions; if (this.appDisabled || !this._userDisabled) { permissions &= ~AddonManager.PERM_CAN_ENABLE; } if (this.appDisabled || this._userDisabled) { permissions &= ~AddonManager.PERM_CAN_DISABLE; } return permissions; }, set permissions(val) { this._permissions = val; }, get applyBackgroundUpdates() { return this._applyBackgroundUpdates; }, set applyBackgroundUpdates(val) { if ( val != AddonManager.AUTOUPDATE_DEFAULT && val != AddonManager.AUTOUPDATE_DISABLE && val != AddonManager.AUTOUPDATE_ENABLE ) { ok(false, "addon.applyBackgroundUpdates set to an invalid value: " + val); } this._applyBackgroundUpdates = val; AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, [ "applyBackgroundUpdates", ]); }, isCompatibleWith(aAppVersion, aPlatformVersion) { return true; }, findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) { // Tests can implement this if they need to }, async getBlocklistURL() { return this.blocklistURL; }, uninstall(aAlwaysAllowUndo = false) { if ( this.operationsRequiringRestart & AddonManager.OP_NEED_RESTART_UNINSTALL && this.pendingOperations & AddonManager.PENDING_UNINSTALL ) { throw Components.Exception("Add-on is already pending uninstall"); } var needsRestart = aAlwaysAllowUndo || !!( this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL ); this.pendingOperations |= AddonManager.PENDING_UNINSTALL; AddonManagerPrivate.callAddonListeners( "onUninstalling", this, needsRestart ); if (!needsRestart) { this.pendingOperations -= AddonManager.PENDING_UNINSTALL; this._provider.removeAddon(this); } else if ( !(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE) ) { this.isActive = false; } }, cancelUninstall() { if (!(this.pendingOperations & AddonManager.PENDING_UNINSTALL)) { throw Components.Exception("Add-on is not pending uninstall"); } this.pendingOperations -= AddonManager.PENDING_UNINSTALL; this.isActive = this.shouldBeActive; AddonManagerPrivate.callAddonListeners("onOperationCancelled", this); }, markAsSeen() { this.seen = true; }, _updateActiveState(currentActive, newActive) { if (currentActive == newActive) { return; } if (newActive == this.isActive) { this.pendingOperations -= newActive ? AddonManager.PENDING_DISABLE : AddonManager.PENDING_ENABLE; AddonManagerPrivate.callAddonListeners("onOperationCancelled", this); } else if (newActive) { let needsRestart = !!( this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_ENABLE ); this.pendingOperations |= AddonManager.PENDING_ENABLE; AddonManagerPrivate.callAddonListeners("onEnabling", this, needsRestart); if (!needsRestart) { this.isActive = newActive; this.pendingOperations -= AddonManager.PENDING_ENABLE; AddonManagerPrivate.callAddonListeners("onEnabled", this); } } else { let needsRestart = !!( this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE ); this.pendingOperations |= AddonManager.PENDING_DISABLE; AddonManagerPrivate.callAddonListeners("onDisabling", this, needsRestart); if (!needsRestart) { this.isActive = newActive; this.pendingOperations -= AddonManager.PENDING_DISABLE; AddonManagerPrivate.callAddonListeners("onDisabled", this); } } }, }; /** *** Mock AddonInstall object for the Mock Provider *****/ function MockInstall(aName, aType, aAddonToInstall) { this.name = aName || ""; // Don't expose type until download completed this._type = aType || "extension"; this.type = null; this.version = "1.0"; this.iconURL = ""; this.infoURL = ""; this.state = AddonManager.STATE_AVAILABLE; this.error = 0; this.sourceURI = null; this.file = null; this.progress = 0; this.maxProgress = -1; this.certificate = null; this.certName = ""; this.existingAddon = null; this.addon = null; this._addonToInstall = aAddonToInstall; this.listeners = []; // Another type of install listener for tests that want to check the results // of code run from standard install listeners this.testListeners = []; } MockInstall.prototype = { install() { switch (this.state) { case AddonManager.STATE_AVAILABLE: this.state = AddonManager.STATE_DOWNLOADING; if (!this.callListeners("onDownloadStarted")) { this.state = AddonManager.STATE_CANCELLED; this.callListeners("onDownloadCancelled"); return; } this.type = this._type; // Adding addon to MockProvider to be implemented when needed if (this._addonToInstall) { this.addon = this._addonToInstall; } else { this.addon = new MockAddon("", this.name, this.type); this.addon.version = this.version; this.addon.pendingOperations = AddonManager.PENDING_INSTALL; } this.addon.install = this; if (this.existingAddon) { if (!this.addon.id) { this.addon.id = this.existingAddon.id; } this.existingAddon.pendingUpgrade = this.addon; this.existingAddon.pendingOperations |= AddonManager.PENDING_UPGRADE; } this.state = AddonManager.STATE_DOWNLOADED; this.callListeners("onDownloadEnded"); // fall through case AddonManager.STATE_DOWNLOADED: this.state = AddonManager.STATE_INSTALLING; if (!this.callListeners("onInstallStarted")) { this.state = AddonManager.STATE_CANCELLED; this.callListeners("onInstallCancelled"); return; } let needsRestart = this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_INSTALL; AddonManagerPrivate.callAddonListeners( "onInstalling", this.addon, needsRestart ); if (!needsRestart) { AddonManagerPrivate.callAddonListeners("onInstalled", this.addon); } this.state = AddonManager.STATE_INSTALLED; this.callListeners("onInstallEnded"); break; case AddonManager.STATE_DOWNLOADING: case AddonManager.STATE_CHECKING_UPDATE: case AddonManager.STATE_INSTALLING: // Installation is already running return; default: ok(false, "Cannot start installing when state = " + this.state); } }, cancel() { switch (this.state) { case AddonManager.STATE_AVAILABLE: this.state = AddonManager.STATE_CANCELLED; break; case AddonManager.STATE_INSTALLED: this.state = AddonManager.STATE_CANCELLED; this._provider.removeInstall(this); this.callListeners("onInstallCancelled"); break; default: // Handling cancelling when downloading to be implemented when needed ok(false, "Cannot cancel when state = " + this.state); } }, addListener(aListener) { if (!this.listeners.some(i => i == aListener)) { this.listeners.push(aListener); } }, removeListener(aListener) { this.listeners = this.listeners.filter(i => i != aListener); }, addTestListener(aListener) { if (!this.testListeners.some(i => i == aListener)) { this.testListeners.push(aListener); } }, removeTestListener(aListener) { this.testListeners = this.testListeners.filter(i => i != aListener); }, callListeners(aMethod) { var result = AddonManagerPrivate.callInstallListeners( aMethod, this.listeners, this, this.addon ); // Call test listeners after standard listeners to remove race condition // between standard and test listeners for (let listener of this.testListeners) { try { if (aMethod in listener) { if (listener[aMethod](this, this.addon) === false) { result = false; } } } catch (e) { ok(false, "Test listener threw exception: " + e); } } return result; }, }; function waitForCondition(condition, nextTest, errorMsg) { let tries = 0; let interval = setInterval(function () { if (tries >= 30) { ok(false, errorMsg); moveOn(); } var conditionPassed; try { conditionPassed = condition(); } catch (e) { ok(false, e + "\n" + e.stack); conditionPassed = false; } if (conditionPassed) { moveOn(); } tries++; }, 100); let moveOn = function () { clearInterval(interval); nextTest(); }; } // Wait for and then acknowledge (by pressing the primary button) the // given notification. function promiseNotification(id = "addon-webext-permissions") { return new Promise(resolve => { function popupshown() { let notification = PopupNotifications.getNotification(id); if (notification) { PopupNotifications.panel.removeEventListener("popupshown", popupshown); PopupNotifications.panel.firstElementChild.button.click(); resolve(); } } PopupNotifications.panel.addEventListener("popupshown", popupshown); }); } /** * Wait for the given PopupNotification to display * * @param {string} name * The name of the notification to wait for. * * @returns {Promise} * Resolves with the notification window. */ function promisePopupNotificationShown(name = "addon-webext-permissions") { return new Promise(resolve => { function popupshown() { let notification = PopupNotifications.getNotification(name); if (!notification) { return; } ok(notification, `${name} notification shown`); ok(PopupNotifications.isPanelOpen, "notification panel open"); PopupNotifications.panel.removeEventListener("popupshown", popupshown); resolve(PopupNotifications.panel.firstChild); } PopupNotifications.panel.addEventListener("popupshown", popupshown); }); } function waitAppMenuNotificationShown( id, addonId, accept = false, win = window ) { const { AppMenuNotifications } = ChromeUtils.importESModule( "resource://gre/modules/AppMenuNotifications.sys.mjs" ); return new Promise(resolve => { let { document, PanelUI } = win; async function popupshown() { let notification = AppMenuNotifications.activeNotification; if (!notification) { return; } is(notification.id, id, `${id} notification shown`); ok(PanelUI.isNotificationPanelOpen, "notification panel open"); PanelUI.notificationPanel.removeEventListener("popupshown", popupshown); if (id == "addon-installed" && addonId) { let addon = await AddonManager.getAddonByID(addonId); if (!addon) { ok(false, `Addon with id "${addonId}" not found`); } let hidden = !( addon.permissions & AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS ); let checkbox = document.getElementById("addon-incognito-checkbox"); is(checkbox.hidden, hidden, "checkbox visibility is correct"); } if (accept) { let popupnotificationID = PanelUI._getPopupId(notification); let popupnotification = document.getElementById(popupnotificationID); popupnotification.button.click(); } resolve(); } // If it's already open just run the test. let notification = AppMenuNotifications.activeNotification; if (notification && PanelUI.isNotificationPanelOpen) { popupshown(); return; } PanelUI.notificationPanel.addEventListener("popupshown", popupshown); }); } function acceptAppMenuNotificationWhenShown(id, addonId) { return waitAppMenuNotificationShown(id, addonId, true); } /* HTML view helpers */ async function loadInitialView(type, opts) { if (type) { // Force the first page load to be the view we want. let viewId; if (type.startsWith("addons://")) { viewId = type; } else { viewId = type == "discover" ? "addons://discover/" : `addons://list/${type}`; } Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, viewId); } let loadCallback; let loadCallbackDone = Promise.resolve(); if (opts && opts.loadCallback) { loadCallback = win => { loadCallbackDone = (async () => { // Wait for the test code to finish running before proceeding. await opts.loadCallback(win); })(); }; } let win = await open_manager(null, null, loadCallback); if (!opts || !opts.withAnimations) { win.document.body.setAttribute("skip-animations", ""); } // Let any load callback code to run before the rest of the test continues. await loadCallbackDone; return win; } function getSection(doc, className) { return doc.querySelector(`section.${className}`); } function waitForViewLoad(win) { return wait_for_view_load(win, undefined, true); } function closeView(win) { return close_manager(win); } function switchView(win, type) { return new CategoryUtilities(win).openType(type); } function isCategoryVisible(win, type) { return new CategoryUtilities(win).isTypeVisible(type); } function mockPromptService() { let { prompt } = Services; let promptService = { // The prompt returns 1 for cancelled and 0 for accepted. _response: 1, QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]), confirmEx: () => promptService._response, }; Services.prompt = promptService; registerCleanupFunction(() => { Services.prompt = prompt; }); return promptService; } function assertHasPendingUninstalls(addonList, expectedPendingUninstallsCount) { const pendingUninstalls = addonList.querySelector( "message-bar-stack.pending-uninstall" ); ok(pendingUninstalls, "Got a pending-uninstall message-bar-stack"); is( pendingUninstalls.childElementCount, expectedPendingUninstallsCount, "Got a message bar in the pending-uninstall message-bar-stack" ); } function assertHasPendingUninstallAddon(addonList, addon) { const pendingUninstalls = addonList.querySelector( "message-bar-stack.pending-uninstall" ); const addonPendingUninstall = addonList.getPendingUninstallBar(addon); ok( addonPendingUninstall, "Got expected message-bar for the pending uninstall test extension" ); is( addonPendingUninstall.parentNode, pendingUninstalls, "pending uninstall bar should be part of the message-bar-stack" ); is( addonPendingUninstall.getAttribute("addon-id"), addon.id, "Got expected addon-id attribute on the pending uninstall message-bar" ); } async function testUndoPendingUninstall(addonList, addon) { const addonPendingUninstall = addonList.getPendingUninstallBar(addon); const undoButton = addonPendingUninstall.querySelector("button[action=undo]"); ok(undoButton, "Got undo action button in the pending uninstall message-bar"); info( "Clicking the pending uninstall undo button and wait for addon card rendered" ); const updated = BrowserTestUtils.waitForEvent(addonList, "add"); undoButton.click(); await updated; ok( addon && !(addon.pendingOperations & AddonManager.PENDING_UNINSTALL), "The addon pending uninstall cancelled" ); } function loadTestSubscript(filePath) { Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this); } function cleanupPendingNotifications() { const { ExtensionsUI } = ChromeUtils.import( "resource:///modules/ExtensionsUI.jsm" ); info("Cleanup any pending notification before exiting the test"); const keys = ChromeUtils.nondeterministicGetWeakSetKeys( ExtensionsUI.pendingNotifications ); if (keys) { keys.forEach(key => ExtensionsUI.pendingNotifications.delete(key)); } } function promisePermissionPrompt(addonId) { return BrowserUtils.promiseObserved( "webextension-permission-prompt", subject => { const { info } = subject.wrappedJSObject || {}; return !addonId || (info.addon && info.addon.id === addonId); } ).then(({ subject }) => { return subject.wrappedJSObject.info; }); } async function handlePermissionPrompt({ addonId, reject = false, assertIcon = true, } = {}) { const info = await promisePermissionPrompt(addonId); // Assert that info.addon and info.icon are defined as expected. is( info.addon && info.addon.id, addonId, "Got the AddonWrapper in the permission prompt info" ); if (assertIcon) { ok(info.icon != null, "Got an addon icon in the permission prompt info"); } if (reject) { info.reject(); } else { info.resolve(); } } async function switchToDetailView({ id, win }) { let card = getAddonCard(win, id); ok(card, `Addon card found for ${id}`); ok(!card.querySelector("addon-details"), "The card doesn't have details"); let loaded = waitForViewLoad(win); EventUtils.synthesizeMouseAtCenter(card, { clickCount: 1 }, win); await loaded; card = getAddonCard(win, id); ok(card.querySelector("addon-details"), "The card does have details"); return card; }