summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/ExtensionXPCShellUtils.jsm')
-rw-r--r--toolkit/components/extensions/ExtensionXPCShellUtils.jsm306
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);
+ },
+};