1770 lines
48 KiB
JavaScript
1770 lines
48 KiB
JavaScript
/* 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) {
|
|
// 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 => {
|
|
async function setup_manager(aManagerWindow) {
|
|
if (aLoadCallback) {
|
|
log_exceptions(aLoadCallback, aManagerWindow);
|
|
}
|
|
|
|
if (aView) {
|
|
aManagerWindow.loadView(aView);
|
|
}
|
|
|
|
Assert.notEqual(
|
|
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) {
|
|
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);
|
|
|
|
Assert.notEqual(
|
|
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() {},
|
|
});
|
|
});
|
|
|
|
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 => {
|
|
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,
|
|
{ supportsOperationsRequiringRestart = false } = {}
|
|
) {
|
|
this.addons = [];
|
|
this.installs = [];
|
|
this.addonTypes = addonTypes ?? ["extension"];
|
|
// NOTE: operationsRequiringRestart is an historical feature of the
|
|
// XPIProvider, which is not supported anymore, there may still be
|
|
// tests making assumptions about MockProvider behaviors related to
|
|
// it, and so this is a temporary measure to gradually remove the
|
|
// remaining bits of the deprecated feature from the MockProvider
|
|
// test helpers.
|
|
//
|
|
// TODO: (Bug 1921875) Remove operationsRequiringRestart-related
|
|
// behaviors from MockProvider.
|
|
this.supportsOperationsRequiringRestart = supportsOperationsRequiringRestart;
|
|
|
|
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 = this.supportsOperationsRequiringRestart
|
|
? (aAddon.operationsRequiringRestart &
|
|
AddonManager.OP_NEEDS_RESTART_INSTALL) !=
|
|
0
|
|
: false;
|
|
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) {
|
|
if (!this.supportsOperationsRequiringRestart) {
|
|
if (addonProp.operationsRequiringRestart !== undefined) {
|
|
throw new Error(
|
|
`Unexpected operationsRequiringRestart set on MockAddon ${addonProp.id}. MockProvider instance does not support operationsRequiringRestart.`
|
|
);
|
|
}
|
|
addonProp.operationsRequiringRestart = 0;
|
|
}
|
|
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 if (prop == "softDisabled") {
|
|
addon.setSoftDisabled(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() {
|
|
// 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() {
|
|
// 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() {
|
|
// 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() {
|
|
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() {
|
|
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._softDisabled &&
|
|
!(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() {
|
|
// NOTE: the logic here should reseamble the logic
|
|
// from the AddonInstall getter with the same name.
|
|
return this._softDisabled || this._userDisabled;
|
|
},
|
|
|
|
set userDisabled(val) {
|
|
throw new Error("No. Bad.");
|
|
},
|
|
|
|
get softDisabled() {
|
|
return this._softDisabled;
|
|
},
|
|
|
|
set softDisabled(val) {
|
|
throw new Error("No. Bad.");
|
|
},
|
|
|
|
setUserDisabled(val) {
|
|
// NOTE: the logic here should reseamble the logic
|
|
// from the AddonInstall method with the same name.
|
|
if (val == (this._userDisabled || this._softDisabled)) {
|
|
return;
|
|
}
|
|
|
|
var currentActive = this.shouldBeActive;
|
|
this._userDisabled = val;
|
|
var newActive = this.shouldBeActive;
|
|
this._updateActiveState(currentActive, newActive);
|
|
},
|
|
|
|
setSoftDisabled(val) {
|
|
// NOTE: the logic here should reseamble the logic
|
|
// from the AddonInstall method with the same name.
|
|
if (val == this._softDisabled) {
|
|
return;
|
|
}
|
|
|
|
var currentActive = this.shouldBeActive;
|
|
if (!this._userDisabled) {
|
|
this._softDisabled = val;
|
|
}
|
|
var newActive = this.shouldBeActive;
|
|
this._updateActiveState(currentActive, newActive);
|
|
},
|
|
|
|
async enable() {
|
|
await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
|
|
|
|
this.setUserDisabled(false);
|
|
this.setSoftDisabled(false);
|
|
},
|
|
async disable() {
|
|
await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
|
|
|
|
this.setUserDisabled(true);
|
|
},
|
|
|
|
get permissions() {
|
|
let permissions = this._permissions;
|
|
if (this.appDisabled) {
|
|
permissions &= ~AddonManager.PERM_CAN_ENABLE;
|
|
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() {
|
|
return true;
|
|
},
|
|
|
|
findUpdates() {
|
|
// 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;
|
|
},
|
|
|
|
updateBlocklistState() {
|
|
// NOTE: this is currently a no-op meant to just prevent MockProvider
|
|
// addons to trigger an unexpected "addon.updateBlockistState is not a function"
|
|
// error in tests covering the blocklist (while there are also MockProvider
|
|
// installed addons).
|
|
},
|
|
|
|
_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._provider?.supportsOperationsRequiringRestart
|
|
? this.operationsRequiringRestart &
|
|
AddonManager.OP_NEEDS_RESTART_INSTALL
|
|
: false;
|
|
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 popupnotificationID = PanelUI._getPopupId(notification);
|
|
let popupnotification = document.getElementById(popupnotificationID);
|
|
|
|
if (accept) {
|
|
popupnotification.button.click();
|
|
}
|
|
|
|
resolve(popupnotification);
|
|
}
|
|
// 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.importESModule(
|
|
"resource:///modules/ExtensionsUI.sys.mjs"
|
|
);
|
|
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) {
|
|
Assert.notEqual(
|
|
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.querySelector(".addon-name-link"),
|
|
{ clickCount: 1 },
|
|
win
|
|
);
|
|
await loaded;
|
|
card = getAddonCard(win, id);
|
|
ok(card.querySelector("addon-details"), "The card does have details");
|
|
return card;
|
|
}
|