Bug 1288885: Support testing WebExtensions from xpcshell tests. r?aswan
Most of the test helper code is derived from the SpecialPowers/ExtensionTestUtils
code that does the same. Eventually, the two implementations should probably
be unified, but I don't think it's worth the trouble for now.
MozReview-Commit-ID: 7Yy9jWkGsMM
--- a/browser/components/extensions/test/xpcshell/.eslintrc
+++ b/browser/components/extensions/test/xpcshell/.eslintrc
@@ -1,3 +1,7 @@
{
"extends": "../../../../../testing/xpcshell/xpcshell.eslintrc",
+
+ "globals": {
+ "browser": false,
+ },
}
--- a/browser/components/extensions/test/xpcshell/head.js
+++ b/browser/components/extensions/test/xpcshell/head.js
@@ -1,55 +1,56 @@
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+/* exported createHttpServer */
+
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
"resource://gre/modules/Extension.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
"resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
+ "resource://gre/modules/ExtensionManagement.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils",
+ "resource://testing-common/ExtensionXPCShellUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
+ "resource://testing-common/httpd.js");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/ExtensionManagement.jsm");
-
-/* exported normalizeManifest */
-
-let BASE_MANIFEST = {
- "applications": {"gecko": {"id": "test@web.ext"}},
-
- "manifest_version": 2,
-
- "name": "name",
- "version": "0",
-};
+ExtensionTestUtils.init(this);
ExtensionManagement.registerSchema("chrome://browser/content/schemas/commands.json");
-function* normalizeManifest(manifest, baseManifest = BASE_MANIFEST) {
- const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
-
- yield Management.lazyInit();
-
- let errors = [];
- let context = {
- url: null,
+/**
+ * Creates a new HttpServer for testing, and begins listening on the
+ * specified port. Automatically shuts down the server when the test
+ * unit ends.
+ *
+ * @param {integer} [port]
+ * The port to listen on. If omitted, listen on a random
+ * port. The latter is the preferred behavior.
+ *
+ * @returns {HttpServer}
+ */
+function createHttpServer(port = -1) {
+ let server = new HttpServer();
+ server.start(port);
- logError: error => {
- errors.push(error);
- },
-
- preprocessors: {},
- };
+ do_register_cleanup(() => {
+ return new Promise(resolve => {
+ server.stop(resolve);
+ });
+ });
- manifest = Object.assign({}, baseManifest, manifest);
-
- let normalized = Schemas.normalize(manifest, "manifest.WebExtensionManifest", context);
- normalized.errors = errors;
-
- return normalized;
+ return server;
}
--- a/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js
@@ -1,15 +1,15 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_manifest_commands() {
- let normalized = yield normalizeManifest({
+ let normalized = yield ExtensionTestUtils.normalizeManifest({
"commands": {
"toggle-feature": {
"suggested_key": {"default": "Shifty+Y"},
"description": "Send a 'toggle-feature' event to the extension",
},
},
});
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -50,16 +50,17 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/ExtensionContent.jsm");
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
// Register built-in parts of the API. Other parts may be registered
// in browser/, mobile/, or b2g/.
ExtensionManagement.registerScript("chrome://extensions/content/ext-alarms.js");
ExtensionManagement.registerScript("chrome://extensions/content/ext-backgroundPage.js");
ExtensionManagement.registerScript("chrome://extensions/content/ext-cookies.js");
ExtensionManagement.registerScript("chrome://extensions/content/ext-downloads.js");
@@ -347,17 +348,17 @@ class ProxyContext extends ExtensionCont
get externallyVisible() {
return false;
}
}
function findPathInObject(obj, path) {
for (let elt of path) {
- obj = obj[elt];
+ obj = obj[elt] || undefined;
}
return obj;
}
let ParentAPIManager = {
proxyContexts: new Map(),
init() {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -0,0 +1,286 @@
+/* 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.defineLazyServiceGetter(this, "uuidGenerator",
+ "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
+
+/* 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.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");
+ }
+ });
+
+ /* eslint-disable mozilla/balanced-listeners */
+ extension.on("test-eq", (kind, pass, msg, expected, actual) => {
+ this.testScope.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`);
+ });
+ extension.on("test-log", (kind, pass, msg) => {
+ this.testScope.do_print(msg);
+ });
+ extension.on("test-result", (kind, pass, msg) => {
+ this.testScope.ok(pass, msg);
+ });
+ extension.on("test-done", (kind, pass, msg, expected, actual) => {
+ this.testScope.ok(pass, msg);
+ this.testResolve(msg);
+ });
+
+ 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 */
+
+ this.testScope.do_register_cleanup(() => {
+ if (this.state == "pending" || this.state == "running") {
+ this.testScope.ok(false, "Extension left running at test shutdown");
+ return this.unload();
+ } else if (extension.state == "unloading") {
+ this.testScope.ok(false, "Extension not fully unloaded at test shutdown");
+ }
+ });
+
+ this.testScope.do_print(`Extension loaded`);
+ }
+
+ 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();
+ }
+
+ 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) {
+ const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
+
+ 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,
+
+ init(scope) {
+ this.currentScope = scope;
+
+ // 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,null", true);
+
+ scope.do_register_cleanup(() => {
+ this.currentScope = null;
+ });
+ },
+
+ addonManagerStarted: false,
+
+ startAddonManager() {
+ if (this.addonManagerStarted) {
+ return;
+ }
+ this.addonManagerStarted = true;
+
+ let tmpD = this.currentScope.do_get_profile().clone();
+ tmpD.append("tmp");
+ tmpD.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+
+ let dirProvider = {
+ getFile: function(prop, persistent) {
+ persistent.value = false;
+ if (prop == "TmpD") {
+ return tmpD.clone();
+ }
+ return null;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider]),
+ };
+ Services.dirsvc.registerProvider(dirProvider);
+
+
+ this.currentScope.do_register_cleanup(() => {
+ tmpD.remove(true);
+ Services.dirsvc.unregisterProvider(dirProvider);
+
+ this.currentScope = null;
+ });
+
+
+ let appInfo = {};
+ Cu.import("resource://testing-common/AppInfo.jsm", appInfo);
+
+ appInfo.updateAppInfo({
+ ID: "xpcshell@tests.mozilla.org",
+ name: "XPCShell",
+ version: "48",
+ platformVersion: "48",
+ });
+
+
+ let manager = Cc["@mozilla.org/addons/integration;1"].getService(Ci.nsIObserver)
+ .QueryInterface(Ci.nsITimerCallback);
+ manager.observe(null, "addons-startup", null);
+ },
+
+ loadExtension(data, id = uuidGenerator.generateUUID().number) {
+ let extension = Extension.generate(id, data);
+
+ return new ExtensionWrapper(extension, this.currentScope);
+ },
+};
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -1403,17 +1403,17 @@ this.Schemas = {
return new NumberType(type);
} else if (type.type == "integer") {
checkTypeProperties("minimum", "maximum");
return new IntegerType(type, type.minimum || -Infinity, type.maximum || Infinity);
} else if (type.type == "boolean") {
checkTypeProperties();
return new BooleanType(type);
} else if (type.type == "function") {
- let isAsync = Boolean(type.async);
+ let isAsync = Boolean(type.async || false);
let parameters = null;
if ("parameters" in type) {
parameters = [];
for (let param of type.parameters) {
// Callbacks default to optional for now, because of promise
// handling.
let isCallback = isAsync && param.name == type.async;
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -10,15 +10,19 @@ EXTRA_JS_MODULES += [
'ExtensionManagement.jsm',
'ExtensionStorage.jsm',
'ExtensionUtils.jsm',
'MessageChannel.jsm',
'NativeMessaging.jsm',
'Schemas.jsm',
]
+TESTING_JS_MODULES += [
+ 'ExtensionXPCShellUtils.jsm',
+]
+
DIRS += ['schemas']
JAR_MANIFESTS += ['jar.mn']
MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
--- a/toolkit/components/extensions/test/xpcshell/.eslintrc
+++ b/toolkit/components/extensions/test/xpcshell/.eslintrc
@@ -1,3 +1,7 @@
{
"extends": "../../../../../testing/xpcshell/xpcshell.eslintrc",
+
+ "globals": {
+ "browser": false,
+ },
}
--- a/toolkit/components/extensions/test/xpcshell/head.js
+++ b/toolkit/components/extensions/test/xpcshell/head.js
@@ -3,49 +3,18 @@
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
"resource://gre/modules/Extension.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
"resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils",
+ "resource://testing-common/ExtensionXPCShellUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
-/* exported normalizeManifest */
-
-let BASE_MANIFEST = {
- "applications": {"gecko": {"id": "test@web.ext"}},
-
- "manifest_version": 2,
-
- "name": "name",
- "version": "0",
-};
-
-function* normalizeManifest(manifest, baseManifest = BASE_MANIFEST) {
- const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
-
- 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;
-}
+ExtensionTestUtils.init(this);
--- a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js
@@ -1,26 +1,26 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_manifest_csp() {
- let normalized = yield normalizeManifest({
+ let normalized = yield ExtensionTestUtils.normalizeManifest({
"content_security_policy": "script-src 'self'; object-src 'none'",
});
equal(normalized.error, undefined, "Should not have an error");
equal(normalized.errors.length, 0, "Should not have warnings");
equal(normalized.value.content_security_policy,
"script-src 'self'; object-src 'none'",
"Should have the expected poilcy string");
- normalized = yield normalizeManifest({
+ normalized = yield ExtensionTestUtils.normalizeManifest({
"content_security_policy": "object-src 'none'",
});
equal(normalized.error, undefined, "Should not have an error");
Assert.deepEqual(normalized.errors,
["Error processing content_security_policy: SyntaxError: Policy is missing a required \u2018script-src\u2019 directive"],
"Should have the expected warning");
--- a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js
@@ -1,25 +1,25 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* test_manifest_incognito() {
- let normalized = yield normalizeManifest({
+ let normalized = yield ExtensionTestUtils.normalizeManifest({
"incognito": "spanning",
});
equal(normalized.error, undefined, "Should not have an error");
equal(normalized.errors.length, 0, "Should not have warnings");
equal(normalized.value.incognito,
"spanning",
"Should have the expected incognito string");
- normalized = yield normalizeManifest({
+ normalized = yield ExtensionTestUtils.normalizeManifest({
"incognito": "split",
});
equal(normalized.error, undefined, "Should not have an error");
Assert.deepEqual(normalized.errors,
['Error processing incognito: Invalid enumeration value "split"'],
"Should have the expected warning");
equal(normalized.value.incognito, null,