diff options
Diffstat (limited to 'toolkit/components/extensions/ExtensionXPCShellUtils.jsm')
-rw-r--r-- | toolkit/components/extensions/ExtensionXPCShellUtils.jsm | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm new file mode 100644 index 000000000..339709a19 --- /dev/null +++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm @@ -0,0 +1,306 @@ +/* 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"; + +this.EXPORTED_SYMBOLS = ["ExtensionTestUtils"]; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Components.utils.import("resource://gre/modules/Task.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Extension", + "resource://gre/modules/Extension.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Schemas", + "resource://gre/modules/Schemas.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyGetter(this, "Management", () => { + const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {}); + return Management; +}); + +/* exported ExtensionTestUtils */ + +let BASE_MANIFEST = Object.freeze({ + "applications": Object.freeze({ + "gecko": Object.freeze({ + "id": "test@web.ext", + }), + }), + + "manifest_version": 2, + + "name": "name", + "version": "0", +}); + +class ExtensionWrapper { + constructor(extension, testScope) { + this.extension = extension; + this.testScope = testScope; + + 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.attachListeners(); + + this.testScope.do_register_cleanup(() => { + if (this.messageQueue.size) { + let names = Array.from(this.messageQueue, ([msg]) => msg); + this.testScope.equal(JSON.stringify(names), "[]", "message queue is empty"); + } + if (this.messageAwaiter.size) { + let names = Array.from(this.messageAwaiter.keys()); + this.testScope.equal(JSON.stringify(names), "[]", "no tasks awaiting on messages"); + } + }); + + this.testScope.do_register_cleanup(() => { + if (this.state == "pending" || this.state == "running") { + this.testScope.equal(this.state, "unloaded", "Extension left running at test shutdown"); + return this.unload(); + } else if (extension.state == "unloading") { + this.testScope.equal(this.state, "unloaded", "Extension not fully unloaded at test shutdown"); + } + }); + + this.testScope.do_print(`Extension loaded`); + } + + attachListeners() { + /* eslint-disable mozilla/balanced-listeners */ + this.extension.on("test-eq", (kind, pass, msg, expected, actual) => { + this.testScope.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`); + }); + this.extension.on("test-log", (kind, pass, msg) => { + this.testScope.do_print(msg); + }); + this.extension.on("test-result", (kind, pass, msg) => { + this.testScope.ok(pass, msg); + }); + this.extension.on("test-done", (kind, pass, msg, expected, actual) => { + this.testScope.ok(pass, msg); + this.testResolve(msg); + }); + + this.extension.on("test-message", (kind, msg, ...args) => { + let handler = this.messageHandler.get(msg); + if (handler) { + handler(...args); + } else { + this.messageQueue.add([msg, ...args]); + this.checkMessages(); + } + }); + /* eslint-enable mozilla/balanced-listeners */ + } + + startup() { + if (this.state != "uninitialized") { + throw new Error("Extension already started"); + } + this.state = "pending"; + + return this.extension.startup().then( + result => { + this.state = "running"; + + return result; + }, + error => { + this.state = "failed"; + + return Promise.reject(error); + }); + } + + unload() { + if (this.state != "running") { + throw new Error("Extension not running"); + } + this.state = "unloading"; + + this.extension.shutdown(); + + this.state = "unloaded"; + + return Promise.resolve(); + } + + /* + * 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 => { + this.checkDuplicateListeners(msg); + + this.messageAwaiter.set(msg, {resolve}); + this.checkMessages(); + }); + } + + onMessage(msg, callback) { + this.checkDuplicateListeners(msg); + this.messageHandler.set(msg, callback); + } +} + +var ExtensionTestUtils = { + BASE_MANIFEST, + + normalizeManifest: Task.async(function* (manifest, baseManifest = BASE_MANIFEST) { + yield Management.lazyInit(); + + let errors = []; + let context = { + url: null, + + logError: error => { + errors.push(error); + }, + + preprocessors: {}, + }; + + manifest = Object.assign({}, baseManifest, manifest); + + let normalized = Schemas.normalize(manifest, "manifest.WebExtensionManifest", context); + normalized.errors = errors; + + return normalized; + }), + + currentScope: null, + + profileDir: null, + + init(scope) { + this.currentScope = scope; + + this.profileDir = scope.do_get_profile(); + + // 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); + + + 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: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider]), + }; + Services.dirsvc.registerProvider(dirProvider); + + + scope.do_register_cleanup(() => { + tmpD.remove(true); + Services.dirsvc.unregisterProvider(dirProvider); + + this.currentScope = null; + }); + }, + + addonManagerStarted: false, + + mockAppInfo() { + const {updateAppInfo} = Cu.import("resource://testing-common/AppInfo.jsm", {}); + updateAppInfo({ + ID: "xpcshell@tests.mozilla.org", + name: "XPCShell", + version: "48", + platformVersion: "48", + }); + }, + + startAddonManager() { + if (this.addonManagerStarted) { + return; + } + this.addonManagerStarted = true; + this.mockAppInfo(); + + let manager = Cc["@mozilla.org/addons/integration;1"].getService(Ci.nsIObserver) + .QueryInterface(Ci.nsITimerCallback); + manager.observe(null, "addons-startup", null); + }, + + loadExtension(data) { + let extension = Extension.generate(data); + + return new ExtensionWrapper(extension, this.currentScope); + }, +}; |