diff options
Diffstat (limited to 'toolkit/components/extensions/ExtensionXPCShellUtils.jsm')
-rw-r--r-- | toolkit/components/extensions/ExtensionXPCShellUtils.jsm | 1096 |
1 files changed, 1096 insertions, 0 deletions
diff --git a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm new file mode 100644 index 0000000000..7fee082a0b --- /dev/null +++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm @@ -0,0 +1,1096 @@ +/* -*- 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/. */ +"use strict"; + +var EXPORTED_SYMBOLS = ["ExtensionTestUtils"]; + +// Need to import ActorManagerParent.jsm so that the actors are initialized before +// running extension XPCShell tests. +ChromeUtils.import("resource://gre/modules/ActorManagerParent.jsm"); + +const { ExtensionUtils } = ChromeUtils.import( + "resource://gre/modules/ExtensionUtils.jsm" +); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +// Windowless browsers can create documents that rely on XUL Custom Elements: +ChromeUtils.import("resource://gre/modules/CustomElementsListener.jsm", null); + +ChromeUtils.defineModuleGetter( + this, + "AddonManager", + "resource://gre/modules/AddonManager.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "AddonTestUtils", + "resource://testing-common/AddonTestUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "ContentTask", + "resource://testing-common/ContentTask.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "ExtensionTestCommon", + "resource://testing-common/ExtensionTestCommon.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "FileUtils", + "resource://gre/modules/FileUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "MessageChannel", + "resource://gre/modules/MessageChannel.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "Schemas", + "resource://gre/modules/Schemas.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "Services", + "resource://gre/modules/Services.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "TestUtils", + "resource://testing-common/TestUtils.jsm" +); + +XPCOMUtils.defineLazyGetter(this, "Management", () => { + const { Management } = ChromeUtils.import( + "resource://gre/modules/Extension.jsm", + null + ); + return Management; +}); + +/* exported ExtensionTestUtils */ + +const { promiseDocumentLoaded, promiseEvent, promiseObserved } = ExtensionUtils; + +var REMOTE_CONTENT_SCRIPTS = Services.appinfo.browserTabsRemoteAutostart; +const REMOTE_CONTENT_SUBFRAMES = Services.appinfo.fissionAutostart; + +let BASE_MANIFEST = Object.freeze({ + applications: Object.freeze({ + gecko: Object.freeze({ + id: "test@web.ext", + }), + }), + + manifest_version: 2, + + name: "name", + version: "0", +}); + +function frameScript() { + const { MessageChannel } = ChromeUtils.import( + "resource://gre/modules/MessageChannel.jsm" + ); + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + + // We need to make sure that the ExtensionPolicy service has been initialized + // as it sets up the observers that inject extension content scripts. + Cc["@mozilla.org/addons/policy-service;1"].getService(); + + Services.obs.notifyObservers(this, "tab-content-frameloader-created"); + + const messageListener = { + async receiveMessage({ target, messageName, recipient, data, name }) { + /* globals content */ + let resp = await content.fetch(data.url, data.options); + return resp.text(); + }, + }; + MessageChannel.addListener(this, "Test:Fetch", messageListener); + + // eslint-disable-next-line mozilla/balanced-listeners, no-undef + addEventListener( + "MozHeapMinimize", + () => { + Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize"); + }, + true, + true + ); +} + +let kungFuDeathGrip = new Set(); +function promiseBrowserLoaded(browser, url, redirectUrl) { + url = url && Services.io.newURI(url); + redirectUrl = redirectUrl && Services.io.newURI(redirectUrl); + + return new Promise(resolve => { + const listener = { + QueryInterface: ChromeUtils.generateQI([ + "nsISupportsWeakReference", + "nsIWebProgressListener", + ]), + + onStateChange(webProgress, request, stateFlags, statusCode) { + request.QueryInterface(Ci.nsIChannel); + + let requestURI = + request.originalURI || + webProgress.DOMWindow.document.documentURIObject; + if ( + webProgress.isTopLevel && + (url?.equals(requestURI) || redirectUrl?.equals(requestURI)) && + stateFlags & Ci.nsIWebProgressListener.STATE_STOP + ) { + resolve(); + kungFuDeathGrip.delete(listener); + browser.removeProgressListener(listener); + } + }, + }; + + // addProgressListener only supports weak references, so we need to + // use one. But we also need to make sure it stays alive until we're + // done with it, so thunk away a strong reference to keep it alive. + kungFuDeathGrip.add(listener); + browser.addProgressListener( + listener, + Ci.nsIWebProgress.NOTIFY_STATE_WINDOW + ); + }); +} + +class ContentPage { + constructor( + remote = REMOTE_CONTENT_SCRIPTS, + remoteSubframes = REMOTE_CONTENT_SUBFRAMES, + extension = null, + privateBrowsing = false, + userContextId = undefined + ) { + this.remote = remote; + + // If an extension has been passed, overwrite remote + // with extension.remote to be sure that the ContentPage + // will have the same remoteness of the extension. + if (extension) { + this.remote = extension.remote; + } + + this.remoteSubframes = this.remote && remoteSubframes; + this.extension = extension; + this.privateBrowsing = privateBrowsing; + this.userContextId = userContextId; + + this.browserReady = this._initBrowser(); + } + + async _initBrowser() { + let chromeFlags = 0; + if (this.remote) { + chromeFlags |= Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW; + } + if (this.remoteSubframes) { + chromeFlags |= Ci.nsIWebBrowserChrome.CHROME_FISSION_WINDOW; + } + if (this.privateBrowsing) { + chromeFlags |= Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW; + } + this.windowlessBrowser = Services.appShell.createWindowlessBrowser( + true, + chromeFlags + ); + + let system = Services.scriptSecurityManager.getSystemPrincipal(); + + let chromeShell = this.windowlessBrowser.docShell.QueryInterface( + Ci.nsIWebNavigation + ); + + chromeShell.createAboutBlankContentViewer(system, system); + this.windowlessBrowser.browsingContext.useGlobalHistory = false; + let loadURIOptions = { + triggeringPrincipal: system, + }; + chromeShell.loadURI( + "chrome://extensions/content/dummy.xhtml", + loadURIOptions + ); + + await promiseObserved( + "chrome-document-global-created", + win => win.document == chromeShell.document + ); + + let chromeDoc = await promiseDocumentLoaded(chromeShell.document); + + let browser = chromeDoc.createXULElement("browser"); + browser.setAttribute("type", "content"); + browser.setAttribute("disableglobalhistory", "true"); + browser.setAttribute("messagemanagergroup", "webext-browsers"); + if (this.userContextId) { + browser.setAttribute("usercontextid", this.userContextId); + } + + if (this.extension?.remote) { + browser.setAttribute("remote", "true"); + browser.setAttribute("remoteType", "extension"); + } + + // Ensure that the extension is loaded into the correct + // BrowsingContextGroupID by default. + if (this.extension) { + browser.setAttribute( + "initialBrowsingContextGroupId", + this.extension.browsingContextGroupId + ); + } + + let awaitFrameLoader = Promise.resolve(); + if (this.remote) { + awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated"); + browser.setAttribute("remote", "true"); + + browser.setAttribute("maychangeremoteness", "true"); + browser.addEventListener( + "DidChangeBrowserRemoteness", + this.didChangeBrowserRemoteness.bind(this) + ); + } + + chromeDoc.documentElement.appendChild(browser); + + // Forcibly flush layout so that we get a pres shell soon enough, see + // bug 1274775. + browser.getBoundingClientRect(); + + await awaitFrameLoader; + + this.browser = browser; + + this.loadFrameScript(frameScript); + + return browser; + } + + sendMessage(msg, data) { + return MessageChannel.sendMessage(this.browser.messageManager, msg, data); + } + + loadFrameScript(func) { + let frameScript = `data:text/javascript,(${encodeURI(func)}).call(this)`; + this.browser.messageManager.loadFrameScript(frameScript, true, true); + } + + addFrameScriptHelper(func) { + let frameScript = `data:text/javascript,${encodeURI(func)}`; + this.browser.messageManager.loadFrameScript(frameScript, false, true); + } + + didChangeBrowserRemoteness(event) { + // XXX: Tests can load their own additional frame scripts, so we may need to + // track all scripts that have been loaded, and reload them here? + this.loadFrameScript(frameScript); + } + + async loadURL(url, redirectUrl = undefined) { + await this.browserReady; + + this.browser.loadURI(url, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + return promiseBrowserLoaded(this.browser, url, redirectUrl); + } + + async fetch(url, options) { + return this.sendMessage("Test:Fetch", { url, options }); + } + + spawn(params, task) { + return ContentTask.spawn(this.browser, params, task); + } + + async close() { + await this.browserReady; + + let { messageManager } = this.browser; + + this.browser.removeEventListener( + "DidChangeBrowserRemoteness", + this.didChangeBrowserRemoteness.bind(this) + ); + this.browser = null; + + this.windowlessBrowser.close(); + this.windowlessBrowser = null; + + await TestUtils.topicObserved( + "message-manager-disconnect", + subject => subject === messageManager + ); + } +} + +class ExtensionWrapper { + constructor(testScope, extension = null) { + this.testScope = testScope; + + this.extension = null; + + this.handleResult = this.handleResult.bind(this); + this.handleMessage = this.handleMessage.bind(this); + + this.state = "uninitialized"; + + this.testResolve = null; + this.testDone = new Promise(resolve => { + this.testResolve = resolve; + }); + + this.messageHandler = new Map(); + this.messageAwaiter = new Map(); + + this.messageQueue = new Set(); + + this.testScope.registerCleanupFunction(() => { + this.clearMessageQueues(); + + if (this.state == "pending" || this.state == "running") { + this.testScope.equal( + this.state, + "unloaded", + "Extension left running at test shutdown" + ); + return this.unload(); + } else if (this.state == "unloading") { + this.testScope.equal( + this.state, + "unloaded", + "Extension not fully unloaded at test shutdown" + ); + } + this.destroy(); + }); + + if (extension) { + this.id = extension.id; + this.attachExtension(extension); + } + } + + destroy() { + // This method should be implemented in subclasses which need to + // perform cleanup when destroyed. + } + + attachExtension(extension) { + if (extension === this.extension) { + return; + } + + if (this.extension) { + this.extension.off("test-eq", this.handleResult); + this.extension.off("test-log", this.handleResult); + this.extension.off("test-result", this.handleResult); + this.extension.off("test-done", this.handleResult); + this.extension.off("test-message", this.handleMessage); + this.clearMessageQueues(); + } + this.uuid = extension.uuid; + this.extension = extension; + + extension.on("test-eq", this.handleResult); + extension.on("test-log", this.handleResult); + extension.on("test-result", this.handleResult); + extension.on("test-done", this.handleResult); + extension.on("test-message", this.handleMessage); + + this.testScope.info(`Extension attached`); + } + + clearMessageQueues() { + if (this.messageQueue.size) { + let names = Array.from(this.messageQueue, ([msg]) => msg); + this.testScope.equal( + JSON.stringify(names), + "[]", + "message queue is empty" + ); + this.messageQueue.clear(); + } + if (this.messageAwaiter.size) { + let names = Array.from(this.messageAwaiter.keys()); + this.testScope.equal( + JSON.stringify(names), + "[]", + "no tasks awaiting on messages" + ); + for (let promise of this.messageAwaiter.values()) { + promise.reject(); + } + this.messageAwaiter.clear(); + } + } + + handleResult(kind, pass, msg, expected, actual) { + switch (kind) { + case "test-eq": + this.testScope.ok( + pass, + `${msg} - Expected: ${expected}, Actual: ${actual}` + ); + break; + + case "test-log": + this.testScope.info(msg); + break; + + case "test-result": + this.testScope.ok(pass, msg); + break; + + case "test-done": + this.testScope.ok(pass, msg); + this.testResolve(msg); + break; + } + } + + handleMessage(kind, msg, ...args) { + let handler = this.messageHandler.get(msg); + if (handler) { + handler(...args); + } else { + this.messageQueue.add([msg, ...args]); + this.checkMessages(); + } + } + + awaitStartup() { + return this.startupPromise; + } + + async startup() { + if (this.state != "uninitialized") { + throw new Error("Extension already started"); + } + this.state = "pending"; + + await ExtensionTestCommon.setIncognitoOverride(this.extension); + + this.startupPromise = this.extension.startup().then( + result => { + this.state = "running"; + + return result; + }, + error => { + this.state = "failed"; + + return Promise.reject(error); + } + ); + + return this.startupPromise; + } + + async unload() { + if (this.state != "running") { + throw new Error("Extension not running"); + } + this.state = "unloading"; + + if (this.addonPromise) { + // If addonPromise is still pending resolution, wait for it to make sure + // that add-ons that are installed through the AddonManager are properly + // uninstalled. + await this.addonPromise; + } + + if (this.addon) { + await this.addon.uninstall(); + } else { + await this.extension.shutdown(); + } + + if (AppConstants.platform === "android") { + // We need a way to notify the embedding layer that an extension has been + // uninstalled, so that the java layer can be updated too. + Services.obs.notifyObservers( + null, + "testing-uninstalled-addon", + this.addon ? this.addon.id : this.extension.id + ); + } + + this.state = "unloaded"; + } + + /* + * This method marks the extension unloading without actually calling + * shutdown, since shutting down a MockExtension causes it to be uninstalled. + * + * Normally you shouldn't need to use this unless you need to test something + * that requires a restart, such as updates. + */ + markUnloaded() { + if (this.state != "running") { + throw new Error("Extension not running"); + } + this.state = "unloaded"; + + return Promise.resolve(); + } + + sendMessage(...args) { + this.extension.testMessage(...args); + } + + awaitFinish(msg) { + return this.testDone.then(actual => { + if (msg) { + this.testScope.equal(actual, msg, "test result correct"); + } + return actual; + }); + } + + checkMessages() { + for (let message of this.messageQueue) { + let [msg, ...args] = message; + + let listener = this.messageAwaiter.get(msg); + if (listener) { + this.messageQueue.delete(message); + this.messageAwaiter.delete(msg); + + listener.resolve(...args); + return; + } + } + } + + checkDuplicateListeners(msg) { + if (this.messageHandler.has(msg) || this.messageAwaiter.has(msg)) { + throw new Error("only one message handler allowed"); + } + } + + awaitMessage(msg) { + return new Promise((resolve, reject) => { + this.checkDuplicateListeners(msg); + + this.messageAwaiter.set(msg, { resolve, reject }); + this.checkMessages(); + }); + } + + onMessage(msg, callback) { + this.checkDuplicateListeners(msg); + this.messageHandler.set(msg, callback); + } +} + +class AOMExtensionWrapper extends ExtensionWrapper { + constructor(testScope) { + super(testScope); + + this.onEvent = this.onEvent.bind(this); + + Management.on("ready", this.onEvent); + Management.on("shutdown", this.onEvent); + Management.on("startup", this.onEvent); + + AddonTestUtils.on("addon-manager-shutdown", this.onEvent); + AddonTestUtils.on("addon-manager-started", this.onEvent); + + AddonManager.addAddonListener(this); + } + + destroy() { + this.id = null; + this.addon = null; + + Management.off("ready", this.onEvent); + Management.off("shutdown", this.onEvent); + Management.off("startup", this.onEvent); + + AddonTestUtils.off("addon-manager-shutdown", this.onEvent); + AddonTestUtils.off("addon-manager-started", this.onEvent); + + AddonManager.removeAddonListener(this); + } + + setRestarting() { + if (this.state !== "restarting") { + this.startupPromise = new Promise(resolve => { + this.resolveStartup = resolve; + }).then(async result => { + await this.addonPromise; + return result; + }); + } + this.state = "restarting"; + } + + onEnabling(addon) { + if (addon.id === this.id) { + this.setRestarting(); + } + } + + onInstalling(addon) { + if (addon.id === this.id) { + this.setRestarting(); + } + } + + onInstalled(addon) { + if (addon.id === this.id) { + this.addon = addon; + } + } + + onUninstalled(addon) { + if (addon.id === this.id) { + this.destroy(); + } + } + + onEvent(kind, ...args) { + switch (kind) { + case "addon-manager-started": + if (this.state === "uninitialized") { + // startup() not called yet, ignore AddonManager startup notification. + return; + } + this.addonPromise = AddonManager.getAddonByID(this.id).then(addon => { + this.addon = addon; + this.addonPromise = null; + }); + // FALLTHROUGH + case "addon-manager-shutdown": + if (this.state === "uninitialized") { + return; + } + this.addon = null; + + this.setRestarting(); + break; + + case "startup": { + let [extension] = args; + + this.maybeSetID(extension.rootURI, extension.id); + + if (extension.id === this.id) { + this.attachExtension(extension); + this.state = "pending"; + } + break; + } + + case "shutdown": { + let [extension] = args; + if (extension.id === this.id && this.state !== "restarting") { + this.state = "unloaded"; + } + break; + } + + case "ready": { + let [extension] = args; + if (extension.id === this.id) { + this.state = "running"; + if (AppConstants.platform === "android") { + // We need a way to notify the embedding layer that a new extension + // has been installed, so that the java layer can be updated too. + Services.obs.notifyObservers( + null, + "testing-installed-addon", + extension.id + ); + } + this.resolveStartup(extension); + } + break; + } + } + } + + async _flushCache() { + if (this.extension && this.extension.rootURI instanceof Ci.nsIJARURI) { + let file = this.extension.rootURI.JARFile.QueryInterface(Ci.nsIFileURL) + .file; + await Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", { + path: file.path, + }); + } + } + + get version() { + return this.addon && this.addon.version; + } + + async unload() { + await this._flushCache(); + return super.unload(); + } + + async upgrade(data) { + this.startupPromise = new Promise(resolve => { + this.resolveStartup = resolve; + }); + this.state = "restarting"; + + await this._flushCache(); + + let xpiFile = ExtensionTestCommon.generateXPI(data); + + this.cleanupFiles.push(xpiFile); + + return this._install(xpiFile); + } +} + +class InstallableWrapper extends AOMExtensionWrapper { + constructor(testScope, xpiFile, addonData = {}) { + super(testScope); + + this.file = xpiFile; + this.addonData = addonData; + this.installType = addonData.useAddonManager || "temporary"; + this.installTelemetryInfo = addonData.amInstallTelemetryInfo; + + this.cleanupFiles = [xpiFile]; + } + + destroy() { + super.destroy(); + + for (let file of this.cleanupFiles.splice(0)) { + try { + Services.obs.notifyObservers(file, "flush-cache-entry"); + file.remove(false); + } catch (e) { + Cu.reportError(e); + } + } + } + + maybeSetID(uri, id) { + if ( + !this.id && + uri instanceof Ci.nsIJARURI && + uri.JARFile.QueryInterface(Ci.nsIFileURL).file.equals(this.file) + ) { + this.id = id; + } + } + + _setIncognitoOverride() { + // this.id is not set yet so grab it from the manifest data to set + // the incognito permission. + let { addonData } = this; + if (addonData && addonData.incognitoOverride) { + try { + let { id } = addonData.manifest.applications.gecko; + if (id) { + return ExtensionTestCommon.setIncognitoOverride({ id, addonData }); + } + } catch (e) {} + throw new Error( + "Extension ID is required for setting incognito permission." + ); + } + } + + async _install(xpiFile) { + await this._setIncognitoOverride(); + + if (this.installType === "temporary") { + return AddonManager.installTemporaryAddon(xpiFile) + .then(addon => { + this.id = addon.id; + this.addon = addon; + + return this.startupPromise; + }) + .catch(e => { + this.state = "unloaded"; + return Promise.reject(e); + }); + } else if (this.installType === "permanent") { + return AddonManager.getInstallForFile( + xpiFile, + null, + this.installTelemetryInfo + ).then(install => { + let listener = { + onInstallFailed: () => { + this.state = "unloaded"; + this.resolveStartup(Promise.reject(new Error("Install failed"))); + }, + onInstallEnded: (install, newAddon) => { + this.id = newAddon.id; + this.addon = newAddon; + }, + }; + + install.addListener(listener); + install.install(); + + return this.startupPromise; + }); + } + } + + startup() { + if (this.state != "uninitialized") { + throw new Error("Extension already started"); + } + + this.state = "pending"; + this.startupPromise = new Promise(resolve => { + this.resolveStartup = resolve; + }); + + return this._install(this.file); + } +} + +class ExternallyInstalledWrapper extends AOMExtensionWrapper { + constructor(testScope, id) { + super(testScope); + + this.id = id; + this.startupPromise = new Promise(resolve => { + this.resolveStartup = resolve; + }); + + this.state = "restarting"; + } + + maybeSetID(uri, id) {} +} + +var ExtensionTestUtils = { + BASE_MANIFEST, + + async normalizeManifest( + manifest, + manifestType = "manifest.WebExtensionManifest", + baseManifest = BASE_MANIFEST + ) { + await Management.lazyInit(); + + let errors = []; + let context = { + url: null, + + logError: error => { + errors.push(error); + }, + + preprocessors: {}, + }; + + manifest = Object.assign({}, baseManifest, manifest); + + let normalized = Schemas.normalize(manifest, manifestType, context); + normalized.errors = errors; + + return normalized; + }, + + currentScope: null, + + profileDir: null, + + init(scope) { + this.currentScope = scope; + + this.profileDir = scope.do_get_profile(); + + this.fetchScopes = new Map(); + + // We need to load at least one frame script into every message + // manager to ensure that the scriptable wrapper for its global gets + // created before we try to access it externally. If we don't, we + // fail sanity checks on debug builds the first time we try to + // create a wrapper, because we should never have a global without a + // cached wrapper. + Services.mm.loadFrameScript("data:text/javascript,//", true, true); + + let tmpD = this.profileDir.clone(); + tmpD.append("tmp"); + tmpD.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + + let dirProvider = { + getFile(prop, persistent) { + persistent.value = false; + if (prop == "TmpD") { + return tmpD.clone(); + } + return null; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]), + }; + Services.dirsvc.registerProvider(dirProvider); + + scope.registerCleanupFunction(() => { + try { + tmpD.remove(true); + } catch (e) { + Cu.reportError(e); + } + Services.dirsvc.unregisterProvider(dirProvider); + + this.currentScope = null; + + return Promise.all( + Array.from(this.fetchScopes.values(), promise => + promise.then(scope => scope.close()) + ) + ); + }); + }, + + addonManagerStarted: false, + + mockAppInfo() { + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "48", + "48" + ); + }, + + startAddonManager() { + if (this.addonManagerStarted) { + return; + } + this.addonManagerStarted = true; + this.mockAppInfo(); + + return AddonTestUtils.promiseStartupManager(); + }, + + loadExtension(data) { + if (data.useAddonManager) { + // If we're using incognitoOverride, we'll need to ensure + // an ID is available before generating the XPI. + if (data.incognitoOverride) { + ExtensionTestCommon.setExtensionID(data); + } + let xpiFile = ExtensionTestCommon.generateXPI(data); + + return this.loadExtensionXPI(xpiFile, data); + } + + let extension = ExtensionTestCommon.generate(data); + + return new ExtensionWrapper(this.currentScope, extension); + }, + + loadExtensionXPI(xpiFile, data) { + return new InstallableWrapper(this.currentScope, xpiFile, data); + }, + + // Create a wrapper for a webextension that will be installed + // by some external process (e.g., Normandy) + expectExtension(id) { + return new ExternallyInstalledWrapper(this.currentScope, id); + }, + + failOnSchemaWarnings(warningsAsErrors = true) { + let prefName = "extensions.webextensions.warnings-as-errors"; + Services.prefs.setBoolPref(prefName, warningsAsErrors); + if (!warningsAsErrors) { + this.currentScope.registerCleanupFunction(() => { + Services.prefs.setBoolPref(prefName, true); + }); + } + }, + + get remoteContentScripts() { + return REMOTE_CONTENT_SCRIPTS; + }, + + set remoteContentScripts(val) { + REMOTE_CONTENT_SCRIPTS = !!val; + }, + + async fetch(origin, url, options) { + let fetchScopePromise = this.fetchScopes.get(origin); + if (!fetchScopePromise) { + fetchScopePromise = this.loadContentPage(origin); + this.fetchScopes.set(origin, fetchScopePromise); + } + + let fetchScope = await fetchScopePromise; + return fetchScope.sendMessage("Test:Fetch", { url, options }); + }, + + /** + * Loads a content page into a hidden docShell. + * + * @param {string} url + * The URL to load. + * @param {object} [options = {}] + * @param {ExtensionWrapper} [options.extension] + * If passed, load the URL as an extension page for the given + * extension. + * @param {boolean} [options.remote] + * If true, load the URL in a content process. If false, load + * it in the parent process. + * @param {boolean} [options.remoteSubframes] + * If true, load cross-origin frames in separate content processes. + * This is ignored if |options.remote| is false. + * @param {string} [options.redirectUrl] + * An optional URL that the initial page is expected to + * redirect to. + * + * @returns {ContentPage} + */ + loadContentPage( + url, + { + extension = undefined, + remote = undefined, + remoteSubframes = undefined, + redirectUrl = undefined, + privateBrowsing = false, + userContextId = undefined, + } = {} + ) { + ContentTask.setTestScope(this.currentScope); + + let contentPage = new ContentPage( + remote, + remoteSubframes, + extension && extension.extension, + privateBrowsing, + userContextId + ); + + return contentPage.loadURL(url, redirectUrl).then(() => { + return contentPage; + }); + }, +}; |