forked from mirrors/gecko-dev
		
	MozReview-Commit-ID: 2fjL5eCL2xP --HG-- extra : amend_source : 1fef859835028292ca163d840a61077bf05fcc06
		
			
				
	
	
		
			794 lines
		
	
	
	
		
			21 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			794 lines
		
	
	
	
		
			21 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* -*- 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"];
 | 
						|
 | 
						|
ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 | 
						|
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 | 
						|
 | 
						|
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, "Extension",
 | 
						|
                               "resource://gre/modules/Extension.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", {});
 | 
						|
  return Management;
 | 
						|
});
 | 
						|
 | 
						|
/* exported ExtensionTestUtils */
 | 
						|
 | 
						|
const {
 | 
						|
  promiseDocumentLoaded,
 | 
						|
  promiseEvent,
 | 
						|
  promiseObserved,
 | 
						|
} = ExtensionUtils;
 | 
						|
 | 
						|
var REMOTE_CONTENT_SCRIPTS = false;
 | 
						|
 | 
						|
let BASE_MANIFEST = Object.freeze({
 | 
						|
  "applications": Object.freeze({
 | 
						|
    "gecko": Object.freeze({
 | 
						|
      "id": "test@web.ext",
 | 
						|
    }),
 | 
						|
  }),
 | 
						|
 | 
						|
  "manifest_version": 2,
 | 
						|
 | 
						|
  "name": "name",
 | 
						|
  "version": "0",
 | 
						|
});
 | 
						|
 | 
						|
 | 
						|
function frameScript() {
 | 
						|
  ChromeUtils.import("resource://gre/modules/MessageChannel.jsm");
 | 
						|
  ChromeUtils.import("resource://gre/modules/Services.jsm");
 | 
						|
 | 
						|
  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) {
 | 
						|
  return new Promise(resolve => {
 | 
						|
    const listener = {
 | 
						|
      QueryInterface: ChromeUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIWebProgressListener]),
 | 
						|
 | 
						|
      onStateChange(webProgress, request, stateFlags, statusCode) {
 | 
						|
        let requestUrl = request.originalURI ? request.originalURI.spec : webProgress.DOMWindow.location.href;
 | 
						|
        if (webProgress.isTopLevel &&
 | 
						|
            (requestUrl === url || requestUrl === redirectUrl) &&
 | 
						|
            (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, extension = null) {
 | 
						|
    this.remote = remote;
 | 
						|
    this.extension = extension;
 | 
						|
 | 
						|
    this.browserReady = this._initBrowser();
 | 
						|
  }
 | 
						|
 | 
						|
  async _initBrowser() {
 | 
						|
    this.windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
 | 
						|
 | 
						|
    let system = Services.scriptSecurityManager.getSystemPrincipal();
 | 
						|
 | 
						|
    let chromeShell = this.windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor)
 | 
						|
                                            .getInterface(Ci.nsIDocShell)
 | 
						|
                                            .QueryInterface(Ci.nsIWebNavigation);
 | 
						|
 | 
						|
    chromeShell.createAboutBlankContentViewer(system);
 | 
						|
    chromeShell.useGlobalHistory = false;
 | 
						|
    chromeShell.loadURI("chrome://extensions/content/dummy.xul", 0, null, null, null);
 | 
						|
 | 
						|
    await promiseObserved("chrome-document-global-created",
 | 
						|
                          win => win.document == chromeShell.document);
 | 
						|
 | 
						|
    let chromeDoc = await promiseDocumentLoaded(chromeShell.document);
 | 
						|
 | 
						|
    let browser = chromeDoc.createElement("browser");
 | 
						|
    browser.setAttribute("type", "content");
 | 
						|
    browser.setAttribute("disableglobalhistory", "true");
 | 
						|
 | 
						|
    if (this.extension && this.extension.remote) {
 | 
						|
      this.remote = true;
 | 
						|
      browser.setAttribute("remote", "true");
 | 
						|
      browser.setAttribute("remoteType", "extension");
 | 
						|
      browser.sameProcessAsFrameLoader = this.extension.groupFrameLoader;
 | 
						|
    }
 | 
						|
 | 
						|
    let awaitFrameLoader = Promise.resolve();
 | 
						|
    if (this.remote) {
 | 
						|
      awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
 | 
						|
      browser.setAttribute("remote", "true");
 | 
						|
    }
 | 
						|
 | 
						|
    chromeDoc.documentElement.appendChild(browser);
 | 
						|
 | 
						|
    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);
 | 
						|
  }
 | 
						|
 | 
						|
  async loadURL(url, redirectUrl = undefined) {
 | 
						|
    await this.browserReady;
 | 
						|
 | 
						|
    this.browser.loadURI(url);
 | 
						|
    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 = 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.uuid = extension.uuid;
 | 
						|
      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.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;
 | 
						|
  }
 | 
						|
 | 
						|
  startup() {
 | 
						|
    if (this.state != "uninitialized") {
 | 
						|
      throw new Error("Extension already started");
 | 
						|
    }
 | 
						|
    this.state = "pending";
 | 
						|
 | 
						|
    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.addon) {
 | 
						|
      await this.addon.uninstall();
 | 
						|
    } else {
 | 
						|
      await this.extension.shutdown();
 | 
						|
    }
 | 
						|
 | 
						|
    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, xpiFile, installType) {
 | 
						|
    super(testScope);
 | 
						|
 | 
						|
    this.onEvent = this.onEvent.bind(this);
 | 
						|
 | 
						|
    this.file = xpiFile;
 | 
						|
    this.installType = installType;
 | 
						|
 | 
						|
    this.cleanupFiles = [xpiFile];
 | 
						|
 | 
						|
    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);
 | 
						|
 | 
						|
    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;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  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":
 | 
						|
        this.addonPromise = AddonManager.getAddonByID(this.id).then(addon => {
 | 
						|
          this.addon = addon;
 | 
						|
        });
 | 
						|
        // FALLTHROUGH
 | 
						|
      case "addon-manager-shutdown":
 | 
						|
        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";
 | 
						|
          this.resolveStartup(extension);
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  _install(xpiFile) {
 | 
						|
    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).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;
 | 
						|
      });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  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;
 | 
						|
  }
 | 
						|
 | 
						|
  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);
 | 
						|
  }
 | 
						|
 | 
						|
  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 = Extension.generateXPI(data);
 | 
						|
 | 
						|
    this.cleanupFiles.push(xpiFile);
 | 
						|
 | 
						|
    return this._install(xpiFile);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
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);
 | 
						|
 | 
						|
 | 
						|
    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([Ci.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() {
 | 
						|
    const {updateAppInfo} = ChromeUtils.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) {
 | 
						|
    if (data.useAddonManager) {
 | 
						|
      let xpiFile = Extension.generateXPI(data);
 | 
						|
 | 
						|
      return this.loadExtensionXPI(xpiFile, data.useAddonManager);
 | 
						|
    }
 | 
						|
 | 
						|
    let extension = Extension.generate(data);
 | 
						|
 | 
						|
    return new ExtensionWrapper(this.currentScope, extension);
 | 
						|
  },
 | 
						|
 | 
						|
  loadExtensionXPI(xpiFile, useAddonManager = "temporary") {
 | 
						|
    return new AOMExtensionWrapper(this.currentScope, xpiFile, useAddonManager);
 | 
						|
  },
 | 
						|
 | 
						|
  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 {string} [options.redirectUrl]
 | 
						|
   *        An optional URL that the initial page is expected to
 | 
						|
   *        redirect to.
 | 
						|
   *
 | 
						|
   * @returns {ContentPage}
 | 
						|
   */
 | 
						|
  loadContentPage(url, {extension = undefined, remote = undefined, redirectUrl = undefined} = {}) {
 | 
						|
    ContentTask.setTestScope(this.currentScope);
 | 
						|
 | 
						|
    let contentPage = new ContentPage(remote, extension && extension.extension);
 | 
						|
 | 
						|
    return contentPage.loadURL(url, redirectUrl).then(() => {
 | 
						|
      return contentPage;
 | 
						|
    });
 | 
						|
  },
 | 
						|
};
 |