diff options
Diffstat (limited to 'toolkit/mozapps/webextensions/test/browser/head.js')
-rw-r--r-- | toolkit/mozapps/webextensions/test/browser/head.js | 1468 |
1 files changed, 1468 insertions, 0 deletions
diff --git a/toolkit/mozapps/webextensions/test/browser/head.js b/toolkit/mozapps/webextensions/test/browser/head.js new file mode 100644 index 000000000..5a749099d --- /dev/null +++ b/toolkit/mozapps/webextensions/test/browser/head.js @@ -0,0 +1,1468 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +/* globals end_test*/ + +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +var tmp = {}; +Components.utils.import("resource://gre/modules/AddonManager.jsm", tmp); +Components.utils.import("resource://gre/modules/Log.jsm", tmp); +var AddonManager = tmp.AddonManager; +var AddonManagerPrivate = tmp.AddonManagerPrivate; +var Log = tmp.Log; + +var pathParts = gTestPath.split("/"); +// Drop the test filename +pathParts.splice(pathParts.length - 1, pathParts.length); + +var gTestInWindow = /-window$/.test(pathParts[pathParts.length - 1]); + +// Drop the UI type +if (gTestInWindow) { + 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_DISCOVERURL = "extensions.webservice.discoverURL"; +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_CUSTOM_XPINSTALL_CONFIRMATION_UI = "xpinstall.customConfirmationUI"; +const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory"; + +const MANAGER_URI = "about:addons"; +const INSTALL_URI = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul"; +const PREF_LOGGING_ENABLED = "extensions.logging.enabled"; +const PREF_SEARCH_MAXRESULTS = "extensions.getAddons.maxResults"; +const PREF_STRICT_COMPAT = "extensions.strictCompatibility"; + +var PREF_CHECK_COMPATIBILITY; +(function() { + var channel = "default"; + try { + channel = Services.prefs.getCharPref("app.update.channel"); + } catch (e) { } + 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 gUseInContentUI = !gTestInWindow && ("switchToTabHavingURI" in window); + +var gRestorePrefs = [{name: PREF_LOGGING_ENABLED}, + {name: PREF_CUSTOM_XPINSTALL_CONFIRMATION_UI}, + {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.search.browseURL"}, + {name: "extensions.getAddons.search.url"}, + {name: "extensions.getAddons.cache.enabled"}, + {name: "devtools.chrome.enabled"}, + {name: PREF_SEARCH_MAXRESULTS}, + {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); + +Services.prefs.setBoolPref(PREF_CUSTOM_XPINSTALL_CONFIRMATION_UI, false); + +// Helper to register test failures and close windows if any are left open +function checkOpenWindows(aWindowID) { + let windows = Services.wm.getEnumerator(aWindowID); + let found = false; + while (windows.hasMoreElements()) { + let win = windows.getNext().QueryInterface(Ci.nsIDOMWindow); + if (!win.closed) { + found = true; + win.close(); + } + } + if (found) + ok(false, "Found unexpected " + aWindowID + " window still open"); +} + +// Tools to disable and re-enable the background update and blocklist timers +// so that tests can protect themselves from unwanted timer events. +var gCatMan = Components.classes["@mozilla.org/categorymanager;1"] + .getService(Components.interfaces.nsICategoryManager); +// Default values 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 blocklistUpdateConfig = "@mozilla.org/extensions/blocklist;1,getService,blocklist-background-update-timer,extensions.blocklist.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); +} + +function disableBlocklistUpdateTimer() { + info("Disabling " + UTIMER + " " + BLOCKLIST); + blocklistUpdateConfig = gCatMan.getCategoryEntry(UTIMER, BLOCKLIST); + gCatMan.deleteCategoryEntry(UTIMER, BLOCKLIST, true); +} + +function enableBlocklistUpdateTimer() { + info("Enabling " + UTIMER + " " + BLOCKLIST); + gCatMan.addCategoryEntry(UTIMER, BLOCKLIST, blocklistUpdateConfig, 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); + } + + // Throw an error if the add-ons manager window is open anywhere + checkOpenWindows("Addons:Manager"); + checkOpenWindows("Addons:Compatibility"); + checkOpenWindows("Addons:Install"); + + return new Promise((resolve, reject) => AddonManager.getAllInstalls(resolve)) + .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) + .then(null, 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 == 0) { + 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 = Task.async(function*(addon) { + let managerWindow = addon.ownerDocument.defaultView; + + // The popup code uses a triggering event's target to set the + // document.tooltipNode property. + let nameNode = addon.ownerDocument.getAnonymousElementByAttribute(addon, "anonid", "name"); + let event = new managerWindow.CustomEvent("TriggerEvent"); + nameNode.dispatchEvent(event); + + let tooltip = managerWindow.document.getElementById("addonitem-tooltip"); + + let promise = BrowserTestUtils.waitForEvent(tooltip, "popupshown"); + tooltip.openPopup(nameNode, "after_start", 0, 0, false, false, event); + yield promise; + + let tiptext = tooltip.label; + + promise = BrowserTestUtils.waitForEvent(tooltip, "popuphidden"); + tooltip.hidePopup(); + yield promise; + + let expectedName = addon.getAttribute("name"); + ok(tiptext.substring(0, expectedName.length), expectedName, + "Tooltip should always start with the expected name"); + + if (expectedName.length == tiptext.length) { + return { + name: tiptext, + version: undefined + }; + } + return { + name: tiptext.substring(0, expectedName.length), + version: tiptext.substring(expectedName.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 get_current_view(aManager) { + let view = aManager.document.getElementById("view-port").selectedPanel; + if (view.id == "headered-views") { + view = aManager.document.getElementById("headered-views-content").selectedPanel; + } + is(view, aManager.gViewController.displayedView, "view controller is tracking the displayed view correctly"); + return view; +} + +function get_test_items_in_list(aManager) { + var tests = "@tests.mozilla.org"; + + let view = get_current_view(aManager); + let listid = view.id == "search-view" ? "search-list" : "addon-list"; + let item = aManager.document.getElementById(listid).firstChild; + let items = []; + + while (item) { + if (item.localName != "richlistitem") { + item = item.nextSibling; + continue; + } + + if (!item.mAddon || item.mAddon.id.substring(item.mAddon.id.length - tests.length) == tests) + items.push(item); + item = item.nextSibling; + } + + return items; +} + +function check_all_in_list(aManager, aIds, aIgnoreExtras) { + var doc = aManager.document; + var view = get_current_view(aManager); + var listid = view.id == "search-view" ? "search-list" : "addon-list"; + var list = doc.getElementById(listid); + + 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.indexOf(id) == -1) + ok(false, "Should find " + id + " in the list"); + } + + if (aIgnoreExtras) + return; + + for (let inlistItem of inlist) { + if (aIds.indexOf(inlistItem) == -1) + ok(false, "Shouldn't have seen " + inlistItem + " in the list"); + } +} + +function get_addon_element(aManager, aId) { + var doc = aManager.document; + var view = get_current_view(aManager); + var listid = "addon-list"; + if (view.id == "search-view") + listid = "search-list"; + else if (view.id == "updates-view") + listid = "updates-list"; + var list = doc.getElementById(listid); + + var node = list.firstChild; + while (node) { + if (node.value == aId) + return node; + node = node.nextSibling; + } + return null; +} + +function wait_for_view_load(aManagerWindow, aCallback, aForceWait, aLongerTimeout) { + requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2); + + if (!aForceWait && !aManagerWindow.gViewController.isLoading) { + log_exceptions(aCallback, aManagerWindow); + return; + } + + aManagerWindow.document.addEventListener("ViewChanged", function() { + aManagerWindow.document.removeEventListener("ViewChanged", arguments.callee, false); + log_exceptions(aCallback, aManagerWindow); + }, false); +} + +function wait_for_manager_load(aManagerWindow, aCallback) { + if (!aManagerWindow.gIsInitializing) { + log_exceptions(aCallback, aManagerWindow); + return; + } + + info("Waiting for initialization"); + aManagerWindow.document.addEventListener("Initialized", function() { + aManagerWindow.document.removeEventListener("Initialized", arguments.callee, false); + log_exceptions(aCallback, aManagerWindow); + }, false); +} + +function open_manager(aView, aCallback, aLoadCallback, aLongerTimeout) { + let p = new Promise((resolve, reject) => { + + 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, MANAGER_URI, "Should be displaying the correct UI"); + + waitForFocus(function() { + info("window has focus, waiting for manager load"); + wait_for_manager_load(aManagerWindow, function() { + info("Manager waiting for view load"); + wait_for_view_load(aManagerWindow, function() { + resolve(aManagerWindow); + }, null, aLongerTimeout); + }); + }, aManagerWindow); + } + + if (gUseInContentUI) { + info("Loading manager window in tab"); + Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.removeObserver(arguments.callee, aTopic); + if (aSubject.location.href != MANAGER_URI) { + info("Ignoring load event for " + aSubject.location.href); + return; + } + setup_manager(aSubject); + }, "EM-loaded", false); + + gBrowser.selectedTab = gBrowser.addTab(); + switchToTabHavingURI(MANAGER_URI, true); + } else { + info("Loading manager window in dialog"); + Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.removeObserver(arguments.callee, aTopic); + setup_manager(aSubject); + }, "EM-loaded", false); + + openDialog(MANAGER_URI); + } + }); + + // 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, MANAGER_URI, "Should be closing window with correct URI"); + + aManagerWindow.addEventListener("unload", function() { + try { + dump("Manager window unload handler\n"); + this.removeEventListener("unload", arguments.callee, false); + resolve(); + } catch (e) { + reject(e); + } + }, false); + }); + + 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) { + Services.wm.addListener({ + onOpenWindow: function(aWindow) { + Services.wm.removeListener(this); + + let domwindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + domwindow.addEventListener("load", function() { + domwindow.removeEventListener("load", arguments.callee, false); + executeSoon(function() { + aCallback(domwindow); + }); + }, false); + }, + + onCloseWindow: function(aWindow) { + }, + + onWindowTitleChange: function(aWindow, aTitle) { + } + }); +} + +function get_string(aName, ...aArgs) { + var bundle = Services.strings.createBundle("chrome://mozapps/locale/extensions/extensions.properties"); + if (aArgs.length == 0) + return bundle.GetStringFromName(aName); + return bundle.formatStringFromName(aName, aArgs, aArgs.length); +} + +function formatDate(aDate) { + const locale = Cc["@mozilla.org/chrome/chrome-registry;1"] + .getService(Ci.nsIXULChromeRegistry) + .getSelectedLocale("global", true); + const dtOptions = { year: 'numeric', month: 'long', day: 'numeric' }; + return aDate.toLocaleDateString(locale, dtOptions); +} + +function is_hidden(aElement) { + var style = aElement.ownerDocument.defaultView.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 new Promise(resolve => { + AddonManager.getAddonByID(aId, resolve); + }); +} + +function promiseAddonsByIDs(aIDs) { + return new Promise(resolve => { + AddonManager.getAddonsByIDs(aIDs, resolve); + }); +} +/** + * Install an add-on and call a callback when complete. + * + * The callback will receive the Addon for the installed add-on. + */ +function install_addon(path, cb, pathPrefix=TESTROOT) { + let p = new Promise((resolve, reject) => { + AddonManager.getInstallForURL(pathPrefix + path, (install) => { + install.addListener({ + onInstallEnded: () => resolve(install.addon), + }); + + install.install(); + }, "application/x-xpinstall"); + }); + + return log_callback(p, cb); +} + +function CategoryUtilities(aManagerWindow) { + this.window = aManagerWindow; + + var self = this; + this.window.addEventListener("unload", function() { + self.window.removeEventListener("unload", arguments.callee, false); + self.window = null; + }, false); +} + +CategoryUtilities.prototype = { + window: null, + + get selectedCategory() { + isnot(this.window, null, "Should not get selected category when manager window is not loaded"); + var selectedItem = this.window.document.getElementById("categories").selectedItem; + isnot(selectedItem, null, "A category should be selected"); + var view = this.window.gViewController.parseViewId(selectedItem.value); + return (view.type == "list") ? view.param : view.type; + }, + + get: function(aCategoryType, aAllowMissing) { + isnot(this.window, null, "Should not get category when manager window is not loaded"); + var categories = this.window.document.getElementById("categories"); + + var viewId = "addons://list/" + aCategoryType; + var items = categories.getElementsByAttribute("value", viewId); + if (items.length) + return items[0]; + + viewId = "addons://" + aCategoryType + "/"; + items = categories.getElementsByAttribute("value", viewId); + if (items.length) + return items[0]; + + if (!aAllowMissing) + ok(false, "Should have found a category with type " + aCategoryType); + return null; + }, + + getViewId: function(aCategoryType) { + isnot(this.window, null, "Should not get view id when manager window is not loaded"); + return this.get(aCategoryType).value; + }, + + isVisible: function(aCategory) { + isnot(this.window, null, "Should not check visible state when manager window is not loaded"); + if (aCategory.hasAttribute("disabled") && + aCategory.getAttribute("disabled") == "true") + return false; + + return !is_hidden(aCategory); + }, + + isTypeVisible: function(aCategoryType) { + return this.isVisible(this.get(aCategoryType)); + }, + + open: function(aCategory, aCallback) { + + isnot(this.window, null, "Should not open category when manager window is not loaded"); + ok(this.isVisible(aCategory), "Category should be visible if attempting to open it"); + + EventUtils.synthesizeMouse(aCategory, 2, 2, { }, this.window); + let p = new Promise((resolve, reject) => wait_for_view_load(this.window, resolve)); + + return log_callback(p, aCallback); + }, + + openType: function(aCategoryType, aCallback) { + return this.open(this.get(aCategoryType), aCallback); + } +} + +function CertOverrideListener(host, bits) { + this.host = host; + this.bits = bits; +} + +CertOverrideListener.prototype = { + host: null, + bits: null, + + getInterface: function (aIID) { + return this.QueryInterface(aIID); + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIBadCertListener2) || + aIID.equals(Ci.nsIInterfaceRequestor) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Components.Exception("No interface", Components.results.NS_ERROR_NO_INTERFACE); + }, + + notifyCertProblem: function (socketInfo, sslStatus, targetHost) { + var cert = sslStatus.QueryInterface(Components.interfaces.nsISSLStatus) + .serverCert; + var cos = Cc["@mozilla.org/security/certoverride;1"]. + getService(Ci.nsICertOverrideService); + cos.rememberValidityOverride(this.host, -1, cert, this.bits, false); + return true; + } +} + +// Add overrides for the bad certificates +function addCertOverride(host, bits) { + var req = new XMLHttpRequest(); + try { + req.open("GET", "https://" + host + "/", false); + req.channel.notificationCallbacks = new CertOverrideListener(host, bits); + req.send(null); + } + catch (e) { + // This request will fail since the SSL server is not trusted yet + } +} + +/** *** Mock Provider *****/ + +function MockProvider(aUseAsyncCallbacks, aTypes) { + this.addons = []; + this.installs = []; + this.callbackTimers = []; + this.timerLocations = new Map(); + this.useAsyncCallbacks = (aUseAsyncCallbacks === undefined) ? true : aUseAsyncCallbacks; + this.types = (aTypes === undefined) ? [{ + id: "extension", + name: "Extensions", + uiPriority: 4000, + flags: AddonManager.TYPE_UI_VIEW_LIST | + AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL, + }] : aTypes; + + var self = this; + registerCleanupFunction(function() { + if (self.started) + self.unregister(); + }); + + this.register(); +} + +MockProvider.prototype = { + addons: null, + installs: null, + started: null, + apiDelay: 10, + callbackTimers: null, + timerLocations: null, + useAsyncCallbacks: null, + types: null, + + /** *** Utility functions *****/ + + /** + * Register this provider with the AddonManager + */ + register: function MP_register() { + info("Registering mock add-on provider"); + AddonManagerPrivate.registerProvider(this, this.types); + }, + + /** + * 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 > 0 ? 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]; + continue; + } + if (prop == "appDisabled") { + addon._appDisabled = addonProp[prop]; + continue; + } + 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() { + if (this.callbackTimers.length) { + info("MockProvider: pending callbacks at shutdown(): calling immediately"); + } + while (this.callbackTimers.length > 0) { + // When we notify the callback timer, it removes itself from our array + let timer = this.callbackTimers[0]; + try { + let setAt = this.timerLocations.get(timer); + info("Notifying timer set at " + (setAt || "unknown location")); + timer.callback.notify(timer); + timer.cancel(); + } catch (e) { + info("Timer notify failed: " + e); + } + } + this.callbackTimers = []; + this.timerLocations = null; + + this.started = false; + }, + + /** + * Called to get an Addon with a particular ID. + * + * @param aId + * The ID of the add-on to retrieve + * @param aCallback + * A callback to pass the Addon to + */ + getAddonByID: function MP_getAddon(aId, aCallback) { + for (let addon of this.addons) { + if (addon.id == aId) { + this._delayCallback(aCallback, addon); + return; + } + } + + aCallback(null); + }, + + /** + * Called to get Addons of a particular type. + * + * @param aTypes + * An array of types to fetch. Can be null to get all types. + * @param callback + * A callback to pass an array of Addons to + */ + getAddonsByTypes: function MP_getAddonsByTypes(aTypes, aCallback) { + var addons = this.addons.filter(function(aAddon) { + if (aTypes && aTypes.length > 0 && aTypes.indexOf(aAddon.type) == -1) + return false; + return true; + }); + this._delayCallback(aCallback, addons); + }, + + /** + * Called to get Addons that have pending operations. + * + * @param aTypes + * An array of types to fetch. Can be null to get all types + * @param aCallback + * A callback to pass an array of Addons to + */ + getAddonsWithOperationsByTypes: function MP_getAddonsWithOperationsByTypes(aTypes, aCallback) { + var addons = this.addons.filter(function(aAddon) { + if (aTypes && aTypes.length > 0 && aTypes.indexOf(aAddon.type) == -1) + return false; + return aAddon.pendingOperations != 0; + }); + this._delayCallback(aCallback, addons); + }, + + /** + * Called to get the current AddonInstalls, optionally restricting by type. + * + * @param aTypes + * An array of types or null to get all types + * @param aCallback + * A callback to pass the array of AddonInstalls to + */ + getInstallsByTypes: function MP_getInstallsByTypes(aTypes, aCallback) { + 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 > 0 && aTypes.indexOf(aInstall.type) == -1) + return false; + + return true; + }); + this._delayCallback(aCallback, 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 aUrl + * The URL to be installed + * @param aHash + * A hash for the install + * @param aName + * A name for the install + * @param aIconURL + * An icon URL for the install + * @param aVersion + * A version for the install + * @param aLoadGroup + * An nsILoadGroup to associate requests with + * @param aCallback + * A callback to pass the AddonInstall to + */ + getInstallForURL: function MP_getInstallForURL(aUrl, aHash, aName, aIconURL, + aVersion, aLoadGroup, aCallback) { + // Not yet implemented + }, + + /** + * Called to get an AddonInstall to install an add-on from a local file. + * + * @param aFile + * The file to be installed + * @param aCallback + * A callback to pass the AddonInstall to + */ + getInstallForFile: function MP_getInstallForFile(aFile, aCallback) { + // 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; + }, + + + /** *** Internal functions *****/ + + /** + * Delay calling a callback to fake a time-consuming async operation. + * The delay is specified by the apiDelay property, in milliseconds. + * Parameters to send to the callback should be specified as arguments after + * the aCallback argument. + * + * @param aCallback Callback to eventually call + */ + _delayCallback: function MP_delayCallback(aCallback, ...aArgs) { + if (!this.useAsyncCallbacks) { + aCallback(...aArgs); + return; + } + + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + // Need to keep a reference to the timer, so it doesn't get GC'ed + this.callbackTimers.push(timer); + // Capture a stack trace where the timer was set + // needs the 'new Error' hack until bug 1007656 + this.timerLocations.set(timer, Log.stackTrace(new Error("dummy"))); + timer.initWithCallback(() => { + let idx = this.callbackTimers.indexOf(timer); + if (idx == -1) { + dump("MockProvider._delayCallback lost track of timer set at " + + (this.timerLocations.get(timer) || "unknown location") + "\n"); + } else { + this.callbackTimers.splice(idx, 1); + } + this.timerLocations.delete(timer); + aCallback(...aArgs); + }, this.apiDelay, timer.TYPE_ONE_SHOT); + } +}; + +/** *** 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; + 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 val; + + AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["appDisabled"]); + + var currentActive = this.shouldBeActive; + this._appDisabled = val; + var newActive = this.shouldBeActive; + this._updateActiveState(currentActive, newActive); + + return val; + }, + + get userDisabled() { + return this._userDisabled; + }, + + set userDisabled(val) { + if (val == this._userDisabled) + return val; + + var currentActive = this.shouldBeActive; + this._userDisabled = val; + var newActive = this.shouldBeActive; + this._updateActiveState(currentActive, newActive); + + return val; + }, + + 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) { + return 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: function(aAppVersion, aPlatformVersion) { + return true; + }, + + findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) { + // Tests can implement this if they need to + }, + + uninstall: function(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: function() { + 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: function() { + this.seen = true; + }, + + _updateActiveState: function(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: function() { + 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"); + + 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: + case AddonManager.STATE_INSTALLING: + // Installation is already running + return; + default: + ok(false, "Cannot start installing when state = " + this.state); + } + }, + + cancel: function() { + 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: function(aListener) { + if (!this.listeners.some(i => i == aListener)) + this.listeners.push(aListener); + }, + + removeListener: function(aListener) { + this.listeners = this.listeners.filter(i => i != aListener); + }, + + addTestListener: function(aListener) { + if (!this.testListeners.some(i => i == aListener)) + this.testListeners.push(aListener); + }, + + removeTestListener: function(aListener) { + this.testListeners = this.testListeners.filter(i => i != aListener); + }, + + callListeners: function(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].call(listener, 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(); }; +} + +function getTestPluginTag() { + let ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + let tags = ph.getPluginTags(); + + // Find the test plugin + for (let i = 0; i < tags.length; i++) { + if (tags[i].name == "Test Plug-in") + return tags[i]; + } + ok(false, "Unable to find plugin"); + return null; +} |