summaryrefslogtreecommitdiffstats
path: root/toolkit/components/webextensions/ExtensionTestCommon.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/webextensions/ExtensionTestCommon.jsm')
-rw-r--r--toolkit/components/webextensions/ExtensionTestCommon.jsm343
1 files changed, 343 insertions, 0 deletions
diff --git a/toolkit/components/webextensions/ExtensionTestCommon.jsm b/toolkit/components/webextensions/ExtensionTestCommon.jsm
new file mode 100644
index 000000000..02453ddfd
--- /dev/null
+++ b/toolkit/components/webextensions/ExtensionTestCommon.jsm
@@ -0,0 +1,343 @@
+/* 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 module contains extension testing helper logic which is common
+ * between all test suites.
+ */
+
+/* exported ExtensionTestCommon, MockExtension */
+
+this.EXPORTED_SYMBOLS = ["ExtensionTestCommon", "MockExtension"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.importGlobalProperties(["TextEncoder"]);
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+ "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
+ "resource://gre/modules/ExtensionParent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "apiManager",
+ () => ExtensionParent.apiManager);
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+const {
+ flushJarCache,
+ instanceOf,
+} = ExtensionUtils;
+
+XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
+
+
+/**
+ * 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
+ */
+class MockExtension {
+ constructor(file, rootURI, installType) {
+ this.id = null;
+ this.file = file;
+ this.rootURI = rootURI;
+ this.installType = installType;
+ this.addon = null;
+
+ let promiseEvent = eventName => new Promise(resolve => {
+ let onstartup = (msg, extension) => {
+ if (this.addon && extension.id == this.addon.id) {
+ apiManager.off(eventName, onstartup);
+
+ this.id = extension.id;
+ this._extension = extension;
+ resolve(extension);
+ }
+ };
+ apiManager.on(eventName, onstartup);
+ });
+
+ this._extension = null;
+ this._extensionPromise = promiseEvent("startup");
+ this._readyPromise = promiseEvent("ready");
+ }
+
+ testMessage(...args) {
+ return this._extension.testMessage(...args);
+ }
+
+ on(...args) {
+ this._extensionPromise.then(extension => {
+ extension.on(...args);
+ });
+ }
+
+ off(...args) {
+ this._extensionPromise.then(extension => {
+ extension.off(...args);
+ });
+ }
+
+ startup() {
+ if (this.installType == "temporary") {
+ return AddonManager.installTemporaryAddon(this.file).then(addon => {
+ this.addon = addon;
+ return this._readyPromise;
+ });
+ } else if (this.installType == "permanent") {
+ return new Promise((resolve, reject) => {
+ AddonManager.getInstallForFile(this.file, install => {
+ let listener = {
+ onInstallFailed: reject,
+ onInstallEnded: (install, newAddon) => {
+ this.addon = 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() {
+ flushJarCache(this.file);
+ return OS.File.remove(this.file.path);
+ }
+}
+
+class ExtensionTestCommon {
+ /**
+ * This code is designed to make it easy to test a WebExtension
+ * without creating a bunch of files. Everything is contained in a
+ * single JSON blob.
+ *
+ * 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.
+ *
+ * The generated extension is stored in the system temporary directory,
+ * and an nsIFile object pointing to it is returned.
+ *
+ * @param {object} data
+ * @returns {nsIFile}
+ */
+ static generateXPI(data) {
+ let manifest = data.manifest;
+ if (!manifest) {
+ manifest = {};
+ }
+
+ let files = data.files;
+ if (!files) {
+ files = {};
+ }
+
+ 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);
+ }
+ }
+
+ provide(manifest, ["name"], "Generated extension");
+ provide(manifest, ["manifest_version"], 2);
+ provide(manifest, ["version"], "1.0");
+
+ if (data.background) {
+ let bgScript = uuidGen.generateUUID().number + ".js";
+
+ provide(manifest, ["background", "scripts"], [bgScript], true);
+ files[bgScript] = data.background;
+ }
+
+ provide(files, ["manifest.json"], manifest);
+
+ if (data.embedded) {
+ // Package this as a webextension embedded inside a legacy
+ // extension.
+
+ let xpiFiles = {
+ "install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
+ <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest"
+ em:id="${manifest.applications.gecko.id}"
+ em:name="${manifest.name}"
+ em:type="2"
+ em:version="${manifest.version}"
+ em:description=""
+ em:hasEmbeddedWebExtension="true"
+ em:bootstrap="true">
+
+ <!-- Firefox -->
+ <em:targetApplication>
+ <Description
+ em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
+ em:minVersion="51.0a1"
+ em:maxVersion="*"/>
+ </em:targetApplication>
+ </Description>
+ </RDF>
+ `,
+
+ "bootstrap.js": `
+ function install() {}
+ function uninstall() {}
+ function shutdown() {}
+
+ function startup(data) {
+ data.webExtension.startup();
+ }
+ `,
+ };
+
+ for (let [path, data] of Object.entries(files)) {
+ xpiFiles[`webextension/${path}`] = data;
+ }
+
+ files = xpiFiles;
+ }
+
+ 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 = FileUtils.getFile("TmpD", [baseName]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 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 (typeof(script) == "function") {
+ script = "(" + script.toString() + ")()";
+ } else if (instanceOf(script, "Object") || instanceOf(script, "Array")) {
+ script = JSON.stringify(script);
+ }
+
+ if (!instanceOf(script, "ArrayBuffer")) {
+ script = new TextEncoder("utf-8").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;
+ }
+
+ /**
+ * Generates a new extension using |Extension.generateXPI|, and initializes a
+ * new |Extension| instance which will execute it.
+ *
+ * @param {object} data
+ * @returns {Extension}
+ */
+ static generate(data) {
+ let file = this.generateXPI(data);
+
+ flushJarCache(file);
+ Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
+
+ let fileURI = Services.io.newFileURI(file);
+ let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null);
+
+ // This may be "temporary" or "permanent".
+ if (data.useAddonManager) {
+ return new MockExtension(file, jarURI, data.useAddonManager);
+ }
+
+ 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 = uuidGen.generateUUID().number;
+ }
+
+ return new Extension({
+ id,
+ resourceURI: jarURI,
+ cleanupFile: file,
+ });
+ }
+}