forked from mirrors/gecko-dev
Data and preferences associated with generated extensions used to not be cleaned up properly upon extension shutdown. This patch changes the implementation to force the unload and cleanup of generated extensions in mochitests in these cases, and enables us to remove the temporary exceptions in ignorePrefs.json from bug 1825953. While the same lack-of-cleanup issue exists in xpcshell tests, this patch does not change the behavior for xpcshell tests yet to minimize the odds of side effects (e.g. longer-running tests and observed differences due to cleanup tasks being run). Differential Revision: https://phabricator.services.mozilla.com/D174758
803 lines
25 KiB
JavaScript
803 lines
25 KiB
JavaScript
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set sts=2 sw=2 et tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/**
|
|
* This module contains extension testing helper logic which is common
|
|
* between all test suites.
|
|
*/
|
|
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
|
Assert: "resource://testing-common/Assert.sys.mjs",
|
|
Extension: "resource://gre/modules/Extension.sys.mjs",
|
|
ExtensionAddonObserver: "resource://gre/modules/Extension.sys.mjs",
|
|
ExtensionData: "resource://gre/modules/Extension.sys.mjs",
|
|
ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
|
|
ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
|
|
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
|
|
clearInterval: "resource://gre/modules/Timer.sys.mjs",
|
|
setInterval: "resource://gre/modules/Timer.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(
|
|
lazy,
|
|
"apiManager",
|
|
() => lazy.ExtensionParent.apiManager
|
|
);
|
|
|
|
import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
|
|
import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
|
|
|
|
const { flushJarCache } = ExtensionUtils;
|
|
|
|
const { instanceOf } = ExtensionCommon;
|
|
|
|
// The tasks received here have already been registered as shutdown blockers
|
|
// (with AsyncShutdown). That means that in reality, if these tasks take
|
|
// long, that they affect Firefox's ability to quit, with a warning after 10
|
|
// seconds and a forced quit without waiting for shutdown after 60 seconds
|
|
// (or whatever is in the toolkit.asyncshutdown.crash_timeout pref).
|
|
//
|
|
// To help with detecting unreasonable slowness in tests, we log when these
|
|
// tasks are taking too long at shutdown.
|
|
const MS_SLOW_TASK_DURATION = 2000;
|
|
|
|
/**
|
|
* ExtensionUninstallTracker should be instantiated before extension shutdown,
|
|
* and can be used to await the completion of the uninstall and post-uninstall
|
|
* cleanup logic. Log messages are printed to aid debugging if the cleanup is
|
|
* observed to be slow (i.e. taking longer than MS_SLOW_TASK_DURATION).
|
|
*
|
|
* // Usage:
|
|
* let uninstallTracker = new ExtensionUninstallTracker(extension.id);
|
|
* await extension.shutdown();
|
|
* await uninstallTracker.waitForUninstallCleanupDone();
|
|
*/
|
|
class ExtensionUninstallTracker {
|
|
#resolveOnCleanupDone;
|
|
constructor(addonId) {
|
|
this.id = addonId;
|
|
|
|
this.remainingTasks = new Set();
|
|
// The uninstall/cleanup observer needs to be registered early, to not miss
|
|
// the notifications.
|
|
this._uninstallPromise = this.#promiseUninstallComplete();
|
|
this._cleanupPromise = this.#promiseCleanupAfterUninstall();
|
|
}
|
|
|
|
async waitForUninstallCleanupDone() {
|
|
// Call #addTask() now instead of in the constructor, so that we only track
|
|
// the time after extension.shutdown() has completed.
|
|
this.#addTask("Awaiting uninstall-complete", this._uninstallPromise);
|
|
this.#addTask("Awaiting cleanupAfterUninstall", this._cleanupPromise);
|
|
// For debugging purposes, if shutdown is slow, regularly print a message
|
|
// with the remaining tasks.
|
|
let timer = lazy.setInterval(
|
|
() => this.#checkRemainingTasks(),
|
|
2 * MS_SLOW_TASK_DURATION
|
|
);
|
|
await new Promise(resolve => {
|
|
this.#resolveOnCleanupDone = resolve;
|
|
this.#checkRemainingTasks();
|
|
});
|
|
lazy.clearInterval(timer);
|
|
}
|
|
|
|
#addTask(name, promise) {
|
|
const task = { name, promise, timeStart: Date.now() };
|
|
this.remainingTasks.add(task);
|
|
promise.finally(() => {
|
|
this.remainingTasks.delete(task);
|
|
this.#checkRemainingTasks();
|
|
});
|
|
}
|
|
|
|
#checkRemainingTasks() {
|
|
for (let task of this.remainingTasks) {
|
|
const timeSinceStart = Date.now() - task.timeStart;
|
|
if (timeSinceStart > MS_SLOW_TASK_DURATION) {
|
|
dump(
|
|
`WARNING: Detected slow post-uninstall task: ${timeSinceStart}ms for extension ${this.id}: ${task.name}\n`
|
|
);
|
|
}
|
|
}
|
|
if (this.remainingTasks.size === 0) {
|
|
this.#resolveOnCleanupDone?.();
|
|
}
|
|
}
|
|
|
|
#promiseUninstallComplete() {
|
|
return new Promise(resolve => {
|
|
const onUninstallComplete = (eventName, { id }) => {
|
|
if (id === this.id) {
|
|
lazy.apiManager.off("uninstall-complete", onUninstallComplete);
|
|
resolve();
|
|
}
|
|
};
|
|
lazy.apiManager.on("uninstall-complete", onUninstallComplete);
|
|
});
|
|
}
|
|
|
|
#promiseCleanupAfterUninstall() {
|
|
return new Promise(resolve => {
|
|
const onCleanupAfterUninstall = (eventName, id, tasks) => {
|
|
if (id === this.id) {
|
|
lazy.apiManager.off("cleanupAfterUninstall", onCleanupAfterUninstall);
|
|
for (const task of tasks) {
|
|
if (task.promise) {
|
|
this.#addTask(task.name, task.promise);
|
|
}
|
|
}
|
|
resolve();
|
|
}
|
|
};
|
|
lazy.apiManager.on("cleanupAfterUninstall", onCleanupAfterUninstall);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A skeleton Extension-like object, used for testing, which installs an
|
|
* add-on via the add-on manager when startup() is called, and
|
|
* uninstalles it on shutdown().
|
|
*
|
|
* @param {string} id
|
|
* @param {nsIFile} file
|
|
* @param {nsIURI} rootURI
|
|
* @param {string} installType
|
|
*/
|
|
export class MockExtension {
|
|
constructor(file, rootURI, addonData) {
|
|
this.id = null;
|
|
this.file = file;
|
|
this.rootURI = rootURI;
|
|
this.installType = addonData.useAddonManager;
|
|
this.addonData = addonData;
|
|
this.addon = null;
|
|
|
|
let promiseEvent = eventName =>
|
|
new Promise(resolve => {
|
|
let onstartup = async (msg, extension) => {
|
|
this.maybeSetID(extension.rootURI, extension.id);
|
|
if (!this.id && this.addonPromise) {
|
|
await this.addonPromise;
|
|
}
|
|
|
|
if (extension.id == this.id) {
|
|
lazy.apiManager.off(eventName, onstartup);
|
|
this._extension = extension;
|
|
resolve(extension);
|
|
}
|
|
};
|
|
lazy.apiManager.on(eventName, onstartup);
|
|
});
|
|
|
|
this._extension = null;
|
|
this._extensionPromise = promiseEvent("startup");
|
|
this._readyPromise = promiseEvent("ready");
|
|
}
|
|
|
|
maybeSetID(uri, id) {
|
|
if (
|
|
!this.id &&
|
|
uri instanceof Ci.nsIJARURI &&
|
|
uri.JARFile.QueryInterface(Ci.nsIFileURL).file.equals(this.file)
|
|
) {
|
|
this.id = id;
|
|
}
|
|
}
|
|
|
|
testMessage(...args) {
|
|
return this._extension.testMessage(...args);
|
|
}
|
|
|
|
get tabManager() {
|
|
return this._extension.tabManager;
|
|
}
|
|
|
|
on(...args) {
|
|
this._extensionPromise.then(extension => {
|
|
extension.on(...args);
|
|
});
|
|
// Extension.sys.mjs emits a "startup" event on |extension| before emitting the
|
|
// "startup" event on |apiManager|. Trigger the "startup" event anyway, to
|
|
// make sure that the MockExtension behaves like an Extension with regards
|
|
// to the startup event.
|
|
if (args[0] === "startup" && !this._extension) {
|
|
this._extensionPromise.then(extension => {
|
|
args[1]("startup", extension);
|
|
});
|
|
}
|
|
}
|
|
|
|
off(...args) {
|
|
this._extensionPromise.then(extension => {
|
|
extension.off(...args);
|
|
});
|
|
}
|
|
|
|
_setIncognitoOverride() {
|
|
let { addonData } = this;
|
|
if (addonData && addonData.incognitoOverride) {
|
|
try {
|
|
let { id } = addonData.manifest.browser_specific_settings.gecko;
|
|
if (id) {
|
|
return ExtensionTestCommon.setIncognitoOverride({ id, addonData });
|
|
}
|
|
} catch (e) {}
|
|
throw new Error(
|
|
"Extension ID is required for setting incognito permission."
|
|
);
|
|
}
|
|
}
|
|
|
|
async startup() {
|
|
await this._setIncognitoOverride();
|
|
|
|
if (this.installType == "temporary") {
|
|
return lazy.AddonManager.installTemporaryAddon(this.file).then(
|
|
async addon => {
|
|
this.addon = addon;
|
|
this.id = addon.id;
|
|
return this._readyPromise;
|
|
}
|
|
);
|
|
} else if (this.installType == "permanent") {
|
|
this.addonPromise = new Promise(resolve => {
|
|
this.resolveAddon = resolve;
|
|
});
|
|
let install = await lazy.AddonManager.getInstallForFile(this.file);
|
|
return new Promise((resolve, reject) => {
|
|
let listener = {
|
|
onInstallFailed: reject,
|
|
onInstallEnded: async (install, newAddon) => {
|
|
this.addon = newAddon;
|
|
this.id = newAddon.id;
|
|
this.resolveAddon(newAddon);
|
|
resolve(this._readyPromise);
|
|
},
|
|
};
|
|
|
|
install.addListener(listener);
|
|
install.install();
|
|
});
|
|
}
|
|
throw new Error("installType must be one of: temporary, permanent");
|
|
}
|
|
|
|
shutdown() {
|
|
this.addon.uninstall();
|
|
return this.cleanupGeneratedFile();
|
|
}
|
|
|
|
cleanupGeneratedFile() {
|
|
return this._extensionPromise
|
|
.then(extension => {
|
|
return extension.broadcast("Extension:FlushJarCache", {
|
|
path: this.file.path,
|
|
});
|
|
})
|
|
.then(() => {
|
|
return IOUtils.remove(this.file.path, { retryReadonly: true });
|
|
});
|
|
}
|
|
|
|
terminateBackground(...args) {
|
|
return this._extensionPromise.then(extension => {
|
|
return extension.terminateBackground(...args);
|
|
});
|
|
}
|
|
|
|
wakeupBackground() {
|
|
return this._extensionPromise.then(extension => {
|
|
return extension.wakeupBackground();
|
|
});
|
|
}
|
|
}
|
|
|
|
function provide(obj, keys, value, override = false) {
|
|
if (keys.length == 1) {
|
|
if (!(keys[0] in obj) || override) {
|
|
obj[keys[0]] = value;
|
|
}
|
|
} else {
|
|
if (!(keys[0] in obj)) {
|
|
obj[keys[0]] = {};
|
|
}
|
|
provide(obj[keys[0]], keys.slice(1), value, override);
|
|
}
|
|
}
|
|
|
|
// Some test assertions to work in both mochitest and xpcshell. This
|
|
// will be revisited later.
|
|
const ExtensionTestAssertions = {
|
|
getPersistentListeners(extWrapper, apiNs, apiEvent) {
|
|
let policy = WebExtensionPolicy.getByID(extWrapper.id);
|
|
const extension = policy?.extension || extWrapper.extension;
|
|
|
|
if (!extension || !(extension instanceof lazy.Extension)) {
|
|
throw new Error(
|
|
`Unable to retrieve the Extension class instance for ${extWrapper.id}`
|
|
);
|
|
}
|
|
|
|
const { persistentListeners } = extension;
|
|
if (
|
|
!persistentListeners?.size ||
|
|
!persistentListeners.get(apiNs)?.has(apiEvent)
|
|
) {
|
|
return [];
|
|
}
|
|
|
|
return Array.from(persistentListeners.get(apiNs).get(apiEvent).values());
|
|
},
|
|
|
|
assertPersistentListeners(
|
|
extWrapper,
|
|
apiNs,
|
|
apiEvent,
|
|
{ primed, persisted = true, primedListenersCount }
|
|
) {
|
|
if (primed && !persisted) {
|
|
throw new Error(
|
|
"Inconsistent assertion, can't assert a primed listener if it is not persisted"
|
|
);
|
|
}
|
|
|
|
let listenersInfo = ExtensionTestAssertions.getPersistentListeners(
|
|
extWrapper,
|
|
apiNs,
|
|
apiEvent
|
|
);
|
|
lazy.Assert.equal(
|
|
persisted,
|
|
!!listenersInfo?.length,
|
|
`Got a persistent listener for ${apiNs}.${apiEvent}`
|
|
);
|
|
for (const info of listenersInfo) {
|
|
if (primed) {
|
|
lazy.Assert.ok(
|
|
info.listeners.some(listener => listener.primed),
|
|
`${apiNs}.${apiEvent} listener expected to be primed`
|
|
);
|
|
} else {
|
|
lazy.Assert.ok(
|
|
!info.listeners.some(listener => listener.primed),
|
|
`${apiNs}.${apiEvent} listener expected to not be primed`
|
|
);
|
|
}
|
|
}
|
|
if (primed && primedListenersCount > 0) {
|
|
lazy.Assert.equal(
|
|
listenersInfo.reduce((acc, info) => {
|
|
acc += info.listeners.length;
|
|
return acc;
|
|
}, 0),
|
|
primedListenersCount,
|
|
`Got the expected number of ${apiNs}.${apiEvent} listeners to be primed`
|
|
);
|
|
}
|
|
},
|
|
};
|
|
|
|
export var ExtensionTestCommon = class ExtensionTestCommon {
|
|
static get testAssertions() {
|
|
return ExtensionTestAssertions;
|
|
}
|
|
|
|
// Called by AddonTestUtils.promiseShutdownManager to reset startup promises
|
|
static resetStartupPromises() {
|
|
lazy.ExtensionParent._resetStartupPromises();
|
|
}
|
|
|
|
// Called to notify "browser-delayed-startup-finished", which resolves
|
|
// ExtensionParent.browserPaintedPromise. Thus must be resolved for
|
|
// primed listeners to be able to wake the extension.
|
|
static notifyEarlyStartup() {
|
|
Services.obs.notifyObservers(null, "browser-delayed-startup-finished");
|
|
return lazy.ExtensionParent.browserPaintedPromise;
|
|
}
|
|
|
|
// Called to notify "extensions-late-startup", which resolves
|
|
// ExtensionParent.browserStartupPromise. Normally, in Firefox, the
|
|
// notification would be "sessionstore-windows-restored", however
|
|
// mobile listens for "extensions-late-startup" so that is more useful
|
|
// in testing.
|
|
static notifyLateStartup() {
|
|
Services.obs.notifyObservers(null, "extensions-late-startup");
|
|
return lazy.ExtensionParent.browserStartupPromise;
|
|
}
|
|
|
|
/**
|
|
* Shortcut to more easily access WebExtensionPolicy.backgroundServiceWorkerEnabled
|
|
* from mochitest-plain tests.
|
|
*
|
|
* @returns {boolean} true if the background service worker are enabled.
|
|
*/
|
|
static getBackgroundServiceWorkerEnabled() {
|
|
return WebExtensionPolicy.backgroundServiceWorkerEnabled;
|
|
}
|
|
|
|
/**
|
|
* A test helper mainly used to skip test tasks if running in "backgroundServiceWorker" test mode
|
|
* (e.g. while running test files shared across multiple test modes: e.g. in-process-webextensions,
|
|
* remote-webextensions, sw-webextensions etc.).
|
|
*
|
|
* The underlying pref "extension.backgroundServiceWorker.forceInTestExtension":
|
|
* - is set to true in the xpcshell-serviceworker.ini and mochitest-serviceworker.ini manifests
|
|
* (and so it is going to be set to true while running the test files listed in those manifests)
|
|
* - when set to true, all test extension using a background script without explicitly listing it
|
|
* in the test extension manifest will be automatically executed as background service workers
|
|
* (instead of background scripts loaded in a background page)
|
|
*
|
|
* @returns {boolean} true if the test is running in "background service worker mode"
|
|
*/
|
|
static isInBackgroundServiceWorkerTests() {
|
|
return Services.prefs.getBoolPref(
|
|
"extensions.backgroundServiceWorker.forceInTestExtension",
|
|
false
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This code is designed to make it easy to test a WebExtension
|
|
* without creating a bunch of files. Everything is contained in a
|
|
* single JS object.
|
|
*
|
|
* Properties:
|
|
* "background": "<JS code>"
|
|
* A script to be loaded as the background script.
|
|
* The "background" section of the "manifest" property is overwritten
|
|
* if this is provided.
|
|
* "manifest": {...}
|
|
* Contents of manifest.json
|
|
* "files": {"filename1": "contents1", ...}
|
|
* Data to be included as files. Can be referenced from the manifest.
|
|
* If a manifest file is provided here, it takes precedence over
|
|
* a generated one. Always use "/" as a directory separator.
|
|
* Directories should appear here only implicitly (as a prefix
|
|
* to file names)
|
|
*
|
|
* To make things easier, the value of "background" and "files"[] can
|
|
* be a function, which is converted to source that is run.
|
|
*
|
|
* @param {object} data
|
|
* @returns {object}
|
|
*/
|
|
static generateFiles(data) {
|
|
let files = {};
|
|
|
|
Object.assign(files, data.files);
|
|
|
|
let manifest = data.manifest;
|
|
if (!manifest) {
|
|
manifest = {};
|
|
}
|
|
|
|
provide(manifest, ["name"], "Generated extension");
|
|
provide(manifest, ["manifest_version"], 2);
|
|
provide(manifest, ["version"], "1.0");
|
|
|
|
// Make it easier to test same manifest in both MV2 and MV3 configurations.
|
|
if (manifest.manifest_version === 2 && manifest.host_permissions) {
|
|
manifest.permissions = [].concat(
|
|
manifest.permissions || [],
|
|
manifest.host_permissions
|
|
);
|
|
delete manifest.host_permissions;
|
|
}
|
|
|
|
if (data.useServiceWorker === undefined) {
|
|
// If we're force-testing service workers we will turn the background
|
|
// script part of ExtensionTestUtils test extensions into a background
|
|
// service worker.
|
|
data.useServiceWorker =
|
|
ExtensionTestCommon.isInBackgroundServiceWorkerTests();
|
|
}
|
|
|
|
// allowInsecureRequests is a shortcut to removing upgrade-insecure-requests from default csp.
|
|
if (data.allowInsecureRequests) {
|
|
// upgrade-insecure-requests is only added automatically to MV3.
|
|
// This flag is therefore not needed in MV2.
|
|
if (manifest.manifest_version < 3) {
|
|
throw new Error("allowInsecureRequests requires manifest_version 3");
|
|
}
|
|
if (manifest.content_security_policy) {
|
|
throw new Error(
|
|
"allowInsecureRequests cannot be used with manifest.content_security_policy"
|
|
);
|
|
}
|
|
manifest.content_security_policy = {
|
|
extension_pages: `script-src 'self'`,
|
|
};
|
|
}
|
|
|
|
if (data.background) {
|
|
let bgScript = Services.uuid.generateUUID().number + ".js";
|
|
|
|
// If persistent is set keep the flag.
|
|
let persistent = manifest.background?.persistent;
|
|
let scriptKey = data.useServiceWorker
|
|
? ["background", "service_worker"]
|
|
: ["background", "scripts"];
|
|
let scriptVal = data.useServiceWorker ? bgScript : [bgScript];
|
|
provide(manifest, scriptKey, scriptVal, true);
|
|
provide(manifest, ["background", "persistent"], persistent);
|
|
|
|
files[bgScript] = data.background;
|
|
}
|
|
|
|
provide(files, ["manifest.json"], JSON.stringify(manifest));
|
|
|
|
for (let filename in files) {
|
|
let contents = files[filename];
|
|
if (typeof contents == "function") {
|
|
files[filename] = this.serializeScript(contents);
|
|
} else if (
|
|
typeof contents != "string" &&
|
|
!instanceOf(contents, "ArrayBuffer")
|
|
) {
|
|
files[filename] = JSON.stringify(contents);
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
/**
|
|
* Write an xpi file to disk for a webextension.
|
|
* The generated extension is stored in the system temporary directory,
|
|
* and an nsIFile object pointing to it is returned.
|
|
*
|
|
* @param {object} data In the format handled by generateFiles.
|
|
* @returns {nsIFile}
|
|
*/
|
|
static generateXPI(data) {
|
|
let files = this.generateFiles(data);
|
|
return this.generateZipFile(files);
|
|
}
|
|
|
|
static generateZipFile(files, baseName = "generated-extension.xpi") {
|
|
let ZipWriter = Components.Constructor(
|
|
"@mozilla.org/zipwriter;1",
|
|
"nsIZipWriter"
|
|
);
|
|
let zipW = new ZipWriter();
|
|
|
|
let file = new lazy.FileUtils.File(
|
|
PathUtils.join(PathUtils.tempDir, baseName)
|
|
);
|
|
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, lazy.FileUtils.PERMS_FILE);
|
|
|
|
const MODE_WRONLY = 0x02;
|
|
const MODE_TRUNCATE = 0x20;
|
|
zipW.open(file, MODE_WRONLY | MODE_TRUNCATE);
|
|
|
|
// Needs to be in microseconds for some reason.
|
|
let time = Date.now() * 1000;
|
|
|
|
function generateFile(filename) {
|
|
let components = filename.split("/");
|
|
let path = "";
|
|
for (let component of components.slice(0, -1)) {
|
|
path += component + "/";
|
|
if (!zipW.hasEntry(path)) {
|
|
zipW.addEntryDirectory(path, time, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let filename in files) {
|
|
let script = files[filename];
|
|
if (!instanceOf(script, "ArrayBuffer")) {
|
|
script = new TextEncoder().encode(script).buffer;
|
|
}
|
|
|
|
let stream = Cc[
|
|
"@mozilla.org/io/arraybuffer-input-stream;1"
|
|
].createInstance(Ci.nsIArrayBufferInputStream);
|
|
stream.setData(script, 0, script.byteLength);
|
|
|
|
generateFile(filename);
|
|
zipW.addEntryStream(filename, time, 0, stream, false);
|
|
}
|
|
|
|
zipW.close();
|
|
|
|
return file;
|
|
}
|
|
|
|
/**
|
|
* Properly serialize a function into eval-able code string.
|
|
*
|
|
* @param {Function} script
|
|
* @returns {string}
|
|
*/
|
|
static serializeFunction(script) {
|
|
// Serialization of object methods doesn't include `function` anymore.
|
|
const method = /^(async )?(?:(\w+)|"(\w+)\.js")\(/;
|
|
|
|
let code = script.toString();
|
|
let match = code.match(method);
|
|
if (match && match[2] !== "function") {
|
|
code = code.replace(method, "$1function $2$3(");
|
|
}
|
|
return code;
|
|
}
|
|
|
|
/**
|
|
* Properly serialize a script into eval-able code string.
|
|
*
|
|
* @param {string | Function | Array} script
|
|
* @returns {string}
|
|
*/
|
|
static serializeScript(script) {
|
|
if (Array.isArray(script)) {
|
|
return Array.from(script, this.serializeScript, this).join(";");
|
|
}
|
|
if (typeof script !== "function") {
|
|
return script;
|
|
}
|
|
return `(${this.serializeFunction(script)})();`;
|
|
}
|
|
|
|
static setIncognitoOverride(extension) {
|
|
let { id, addonData } = extension;
|
|
if (!addonData || !addonData.incognitoOverride) {
|
|
return;
|
|
}
|
|
if (addonData.incognitoOverride == "not_allowed") {
|
|
return lazy.ExtensionPermissions.remove(id, {
|
|
permissions: ["internal:privateBrowsingAllowed"],
|
|
origins: [],
|
|
});
|
|
}
|
|
return lazy.ExtensionPermissions.add(id, {
|
|
permissions: ["internal:privateBrowsingAllowed"],
|
|
origins: [],
|
|
});
|
|
}
|
|
|
|
static setExtensionID(data) {
|
|
try {
|
|
if (data.manifest.browser_specific_settings.gecko.id) {
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
// No ID is set.
|
|
}
|
|
provide(
|
|
data,
|
|
["manifest", "browser_specific_settings", "gecko", "id"],
|
|
Services.uuid.generateUUID().number
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generates a new extension using |Extension.generateXPI|, and initializes a
|
|
* new |Extension| instance which will execute it.
|
|
*
|
|
* @param {object} data
|
|
* @returns {Partial<Extension>}
|
|
*/
|
|
static generate(data) {
|
|
if (data.useAddonManager === "android-only") {
|
|
// Some extension APIs are partially implemented in Java, and the
|
|
// interface between the JS and Java side (GeckoViewWebExtension)
|
|
// expects extensions to be registered with the AddonManager.
|
|
// This is at least necessary for tests that use the following APIs:
|
|
// - browserAction/pageAction.
|
|
// - tabs.create, tabs.update, tabs.remove (uses GeckoViewTabBridge).
|
|
// - downloads API
|
|
// - browsingData API (via ExtensionBrowsingData.sys.mjs).
|
|
//
|
|
// In xpcshell tests, the AddonManager is optional, so the AddonManager
|
|
// cannot unconditionally be enabled.
|
|
// In mochitests, tests are run in an actual browser, so the AddonManager
|
|
// is always enabled and hence useAddonManager is always set by default.
|
|
if (AppConstants.platform === "android") {
|
|
// Many MV3 tests set temporarilyInstalled for granted_host_permissions.
|
|
// The granted_host_permissions flag is only effective for temporarily
|
|
// installed extensions, so make sure to use "temporary" in this case.
|
|
if (data.temporarilyInstalled) {
|
|
data.useAddonManager = "temporary";
|
|
} else {
|
|
data.useAddonManager = "permanent";
|
|
}
|
|
// MockExtension requires data.manifest.applications.gecko.id to be set.
|
|
// The AddonManager requires an ID in the manifest for unsigned XPIs.
|
|
this.setExtensionID(data);
|
|
} else {
|
|
// On non-Android, default to not using the AddonManager.
|
|
data.useAddonManager = null;
|
|
}
|
|
}
|
|
|
|
let file = this.generateXPI(data);
|
|
|
|
flushJarCache(file.path);
|
|
Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {
|
|
path: file.path,
|
|
});
|
|
|
|
let fileURI = Services.io.newFileURI(file);
|
|
let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/");
|
|
|
|
// This may be "temporary" or "permanent".
|
|
if (data.useAddonManager) {
|
|
return new MockExtension(file, jarURI, data);
|
|
}
|
|
|
|
let id;
|
|
if (data.manifest) {
|
|
if (data.manifest.applications && data.manifest.applications.gecko) {
|
|
id = data.manifest.applications.gecko.id;
|
|
} else if (
|
|
data.manifest.browser_specific_settings &&
|
|
data.manifest.browser_specific_settings.gecko
|
|
) {
|
|
id = data.manifest.browser_specific_settings.gecko.id;
|
|
}
|
|
}
|
|
if (!id) {
|
|
id = Services.uuid.generateUUID().number;
|
|
}
|
|
|
|
let signedState = lazy.AddonManager.SIGNEDSTATE_SIGNED;
|
|
if (data.isPrivileged) {
|
|
signedState = lazy.AddonManager.SIGNEDSTATE_PRIVILEGED;
|
|
}
|
|
if (data.isSystem) {
|
|
signedState = lazy.AddonManager.SIGNEDSTATE_SYSTEM;
|
|
}
|
|
|
|
let isPrivileged = lazy.ExtensionData.getIsPrivileged({
|
|
signedState,
|
|
builtIn: false,
|
|
temporarilyInstalled: !!data.temporarilyInstalled,
|
|
});
|
|
|
|
return new lazy.Extension(
|
|
{
|
|
id,
|
|
resourceURI: jarURI,
|
|
cleanupFile: file,
|
|
signedState,
|
|
incognitoOverride: data.incognitoOverride,
|
|
temporarilyInstalled: !!data.temporarilyInstalled,
|
|
isPrivileged,
|
|
TEST_NO_ADDON_MANAGER: true,
|
|
// By default we set TEST_NO_DELAYED_STARTUP to true
|
|
TEST_NO_DELAYED_STARTUP: !data.delayedStartup,
|
|
},
|
|
data.startupReason ?? "ADDON_INSTALL"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Unload an extension and await completion of post-uninstall cleanup tasks.
|
|
*
|
|
* @param {Extension|MockExtension} extension
|
|
*/
|
|
static async unloadTestExtension(extension) {
|
|
const { id } = extension;
|
|
const uninstallTracker = new ExtensionUninstallTracker(id);
|
|
await extension.shutdown();
|
|
if (extension instanceof lazy.Extension) {
|
|
// AddonManager-managed add-ons run additional (cleanup) tasks after
|
|
// shutting down an extension. Do the same even without useAddonManager.
|
|
lazy.Extension.getBootstrapScope().uninstall({ id });
|
|
|
|
// Data removal by ExtensionAddonObserver.onUninstalled:
|
|
lazy.ExtensionAddonObserver.clearOnUninstall(id);
|
|
}
|
|
await uninstallTracker.waitForUninstallCleanupDone();
|
|
}
|
|
};
|