Bug 1169179 - Run mozscreenshots as a mochitest-browser-chrome test. r=felipe,glandium
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Tue, 19 Jan 2016 22:40:34 -0800
changeset 317707 162b1fb7424a6e92463a3de30fa56b696f13ddc1
parent 317706 92016fe68730f7f2257fb9a12b242fb85c541fa7
child 317708 41efc3cbf8d6a498cf0f3661c88cf7eed379c42d
push id1079
push userjlund@mozilla.com
push dateFri, 15 Apr 2016 21:02:33 +0000
treeherdermozilla-release@575fbf6786d5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfelipe, glandium
bugs1169179
milestone46.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1169179 - Run mozscreenshots as a mochitest-browser-chrome test. r=felipe,glandium
browser/moz.build
browser/tools/mozscreenshots/browser.ini
browser/tools/mozscreenshots/browser_screenshots.js
browser/tools/mozscreenshots/head.js
browser/tools/mozscreenshots/moz.build
browser/tools/mozscreenshots/mozscreenshots/extension/Makefile.in
browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.jsm
browser/tools/mozscreenshots/mozscreenshots/extension/TestRunner.jsm
browser/tools/mozscreenshots/mozscreenshots/extension/bootstrap.js
browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.jsm
browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.jsm
browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.jsm
browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevEdition.jsm
browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.jsm
browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.jsm
browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.jsm
browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.jsm
browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.jsm
browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.jsm
browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.jsm
browser/tools/mozscreenshots/mozscreenshots/extension/install.rdf
browser/tools/mozscreenshots/mozscreenshots/extension/jar.mn
browser/tools/mozscreenshots/mozscreenshots/extension/lib/black_theme.png
browser/tools/mozscreenshots/mozscreenshots/extension/lib/white_theme.png
browser/tools/mozscreenshots/mozscreenshots/extension/moz.build
--- a/browser/moz.build
+++ b/browser/moz.build
@@ -21,10 +21,14 @@ DIRS += [
 
 DIRS += [
     'app',
 ]
 
 if CONFIG['MAKENSISU']:
     DIRS += ['installer/windows']
 
+TEST_DIRS += [
+    'tools/mozscreenshots',
+]
+
 DIST_SUBDIR = 'browser'
 export('DIST_SUBDIR')
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+subsuite = screenshots
+support-files =
+  head.js
+
+[browser_screenshots.js]
+tags = screenshots
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/browser_screenshots.js
@@ -0,0 +1,18 @@
+/* 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";
+
+const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+
+add_task(function* test() {
+  let { TestRunner } = Cu.import("chrome://mozscreenshots/content/TestRunner.jsm", {});
+  let sets = ["TabsInTitlebar", "Tabs", "WindowSize", "Toolbars", "LightweightThemes"];
+  let setsEnv = env.get("MOZSCREENSHOTS_SETS");
+  if (setsEnv) {
+    sets = setsEnv.trim().split(",");
+  }
+
+  yield TestRunner.start(sets);
+});
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/head.js
@@ -0,0 +1,20 @@
+/* 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";
+
+const {AddonWatcher} = Cu.import("resource://gre/modules/AddonWatcher.jsm", {});
+let TestRunner;
+
+function setup() {
+  requestLongerTimeout(10);
+
+  info("Checking for mozscreenshots extension");
+  AddonManager.getAddonByID("mozscreenshots@mozilla.org", function(aAddon) {
+    isnot(aAddon, null, "The mozscreenshots extension should be installed");
+    AddonWatcher.ignoreAddonPermanently(aAddon.id);
+  });
+}
+
+add_task(setup);
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['browser.ini']
+
+TEST_DIRS += [
+    'mozscreenshots/extension',
+]
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/Makefile.in
@@ -0,0 +1,12 @@
+# 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/.
+
+TEST_EXTENSIONS_DIR = $(DEPTH)/_tests/testing/mochitest/extensions
+GENERATED_DIRS =  $(TEST_EXTENSIONS_DIR)
+XPI_PKGNAME = mozscreenshots@mozilla.org
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+	(cd $(DIST)/xpi-stage && tar $(TAR_CREATE_FLAGS) - $(XPI_NAME)) | (cd $(TEST_EXTENSIONS_DIR) && tar -xf -)
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.jsm
@@ -0,0 +1,175 @@
+/* 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 = ["Screenshot"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+
+// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
+// See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
+const PREF_LOG_LEVEL = "extensions.mozscreenshots@mozilla.org.loglevel";
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
+  let consoleOptions = {
+    maxLogLevel: "info",
+    maxLogLevelPref: PREF_LOG_LEVEL,
+    prefix: "mozscreenshots",
+  };
+  return new ConsoleAPI(consoleOptions);
+});
+
+let Screenshot = {
+  _extensionPath: null,
+  _path: null,
+  _imagePrefix: "",
+  _imageExtension: ".png",
+  _screenshotFunction: null,
+
+  init(path, extensionPath, imagePrefix = "") {
+    this._path = path;
+
+    let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+    dir.initWithPath(this._path);
+    if (!dir.exists()) {
+      dir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+    }
+
+    this._extensionPath = extensionPath;
+    this._imagePrefix = imagePrefix;
+    switch (Services.appinfo.OS) {
+      case "WINNT":
+        this._screenshotFunction = this._screenshotWindows;
+        break;
+      case "Darwin":
+        this._screenshotFunction = this._screenshotOSX;
+        break;
+      case "Linux":
+        this._screenshotFunction = this._screenshotLinux;
+        break;
+      default:
+        throw new Error("Unsupported operating system");
+        break;
+    }
+  },
+
+  _buildImagePath(baseName) {
+    return OS.Path.join(this._path, this._imagePrefix + baseName + this._imageExtension);
+  },
+
+  // Capture the whole screen using an external application.
+  captureExternal(filename) {
+    let imagePath = this._buildImagePath(filename);
+    return this._screenshotFunction(imagePath).then(() => {
+      log.debug("saved screenshot: " + filename);
+    });
+  },
+
+  ///// helpers /////
+
+  _screenshotWindows(filename) {
+    return new Promise((resolve, reject) => {
+      let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+      exe.append("screenshot.exe");
+      if (!exe.exists()) {
+        exe = this._extensionPath.QueryInterface(Ci.nsIFileURL).file;
+        exe.append("lib");
+        exe.append("screenshot.exe");
+      }
+      let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+      process.init(exe);
+
+      let args = [filename];
+      process.runAsync(args, args.length, this._processObserver(resolve, reject));
+    });
+  },
+
+  _screenshotOSX: Task.async(function*(filename) {
+    let screencapture = (windowID = null) => {
+      return new Promise((resolve, reject) => {
+        // Get the screencapture executable
+        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+        file.initWithPath("/usr/sbin/screencapture");
+
+        let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+        process.init(file);
+
+        // Run the process.
+        let args = ["-x", "-t", "png"];
+        // Darwin version number for OS X 10.6 is 10.x
+        if (windowID && Services.sysinfo.getProperty("version").indexOf("10.") !== 0) {
+          // Capture only that window on 10.7+
+          args.push("-l");
+          args.push(windowID);
+        }
+        args.push(filename);
+        process.runAsync(args, args.length, this._processObserver(resolve, reject));
+      });
+    };
+
+    function readWindowID() {
+      let decoder = new TextDecoder();
+      let promise = OS.File.read("/tmp/mozscreenshots-windowid");
+      return promise.then(function onSuccess(array) {
+          return decoder.decode(array);
+      });
+    }
+
+    let promiseWindowID = () => {
+      return new Promise((resolve, reject) => {
+        // Get the window ID of the application (assuming its front-most)
+        let osascript = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+        osascript.initWithPath("/bin/bash");
+
+        let osascriptP = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+        osascriptP.init(osascript);
+        let osaArgs = ["-c", "/usr/bin/osascript -e 'tell application (path to frontmost application as text) to set winID to id of window 1' > /tmp/mozscreenshots-windowid"];
+        osascriptP.runAsync(osaArgs, osaArgs.length, this._processObserver(resolve, reject));
+      });
+    };
+
+    yield promiseWindowID();
+    let windowID = yield readWindowID();
+    yield screencapture(windowID);
+  }),
+
+  _screenshotLinux(filename) {
+    return new Promise((resolve, reject) => {
+      let file = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+      file.append("screentopng");
+      let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+      process.init(file);
+
+      let args = [filename];
+      process.runAsync(args, args.length, this._processObserver(resolve, reject));
+    });
+  },
+
+  _processObserver(resolve, reject) {
+    return {
+      observe(subject, topic, data) {
+        switch (topic) {
+          case "process-finished":
+            try {
+              // Wait 1s after process to resolve
+              setTimeout(resolve, 1000);
+            } catch (ex) {
+              reject(ex);
+            }
+            break;
+          default:
+            reject(topic);
+            break;
+        };
+      },
+    };
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/TestRunner.jsm
@@ -0,0 +1,256 @@
+/* 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 = ["TestRunner"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const defaultSetNames = ["TabsInTitlebar", "Tabs", "WindowSize", "Toolbars", "LightweightThemes"];
+const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("chrome://mozscreenshots/content/Screenshot.jsm");
+
+// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
+// See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
+const PREF_LOG_LEVEL = "extensions.mozscreenshots@mozilla.org.loglevel";
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
+  let consoleOptions = {
+    maxLogLevel: "info",
+    maxLogLevelPref: PREF_LOG_LEVEL,
+    prefix: "mozscreenshots",
+  };
+  return new ConsoleAPI(consoleOptions);
+});
+
+this.TestRunner = {
+  combos: null,
+  completedCombos: 0,
+  currentComboIndex: 0,
+  _lastCombo: null,
+  _libDir: null,
+
+  init(extensionPath) {
+    let subDirs = ["mozscreenshots",
+                   (new Date()).toISOString().replace(/:/g, "-") + "_" + Services.appinfo.OS];
+    let screenshotPath = FileUtils.getFile("TmpD", subDirs).path;
+
+    const MOZ_UPLOAD_DIR = env.get("MOZ_UPLOAD_DIR");
+    if (MOZ_UPLOAD_DIR) {
+      screenshotPath = MOZ_UPLOAD_DIR;
+    }
+
+    log.info("Saving screenshots to:", screenshotPath);
+    log.debug("TestRunner.init");
+
+    let screenshotPrefix = Services.appinfo.appBuildID + "_";
+    Screenshot.init(screenshotPath, extensionPath, screenshotPrefix);
+    this._libDir = extensionPath.QueryInterface(Ci.nsIFileURL).file.clone();
+    this._libDir.append("chrome");
+    this._libDir.append("mozscreenshots");
+    this._libDir.append("lib");
+
+    // Setup some prefs
+    Services.prefs.setCharPref("browser.aboutHomeSnippets.updateUrl", "data:");
+    Services.prefs.setCharPref("extensions.ui.lastCategory", "addons://list/extension");
+    // Don't let the caret blink since it causes false positives for image diffs
+    Services.prefs.setIntPref("ui.caretBlinkTime", -1);
+  },
+
+  /**
+   * Load specified sets, execute all combinations of them, and capture screenshots.
+   */
+  start(setNames = null) {
+    setNames = setNames || defaultSetNames;
+    let sets = this.loadSets(setNames);
+
+    log.info(sets.length + " sets:", setNames);
+    this.combos = new LazyProduct(sets);
+    log.info(this.combos.length + " combinations");
+
+    this.currentComboIndex = this.completedCombos = 0;
+    this._lastCombo = null;
+
+    return Task.spawn(function* doStart() {
+      for (let i = 0; i < this.combos.length;
+           i++){
+        this.currentComboIndex = i;
+        yield* this._performCombo(this.combos.item(this.currentComboIndex));
+      }
+
+      log.info("Done: Completed " + this.completedCombos + " out of " +
+               this.combos.length + " configurations.");
+      this.cleanup();
+    }.bind(this));
+  },
+
+  /**
+   * Load sets of configurations from JSMs.
+   * @param {String[]} setNames - array of set names (e.g. ["Tabs", "WindowSize"].
+   * @return {Object[]} Array of sets containing `name` and `configurations` properties.
+   */
+  loadSets(setNames) {
+    let sets = [];
+    for (let setName of setNames) {
+      try {
+        let imported = {};
+        Cu.import("chrome://mozscreenshots/content/configurations/" + setName + ".jsm",
+                  imported);
+        imported[setName].init(this._libDir);
+        let configurationNames = Object.keys(imported[setName].configurations);
+        if (!configurationNames.length) {
+          throw new Error(setName + " has no configurations for this environment");
+        }
+        for (let config of configurationNames) {
+          // Automatically set the name property of the configuration object to
+          // its name from the configuration object.
+          imported[setName].configurations[config].name = config;
+        }
+        sets.push(imported[setName].configurations);
+      } catch (ex) {
+        log.error("Error loading set: " + setName);
+        log.error(ex);
+        throw ex;
+      }
+    }
+    return sets;
+  },
+
+  cleanup() {
+    let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+    let gBrowser = browserWindow.gBrowser;
+    while (gBrowser.tabs.length > 1) {
+      gBrowser.removeTab(gBrowser.selectedTab, {animate: false});
+    }
+    gBrowser.unpinTab(gBrowser.selectedTab);
+    gBrowser.selectedBrowser.loadURI("data:text/html;charset=utf-8,<h1>Done!");
+    browserWindow.restore();
+  },
+
+  ///// helpers /////
+
+  _performCombo: function*(combo) {
+    let paddedComboIndex = padLeft(this.currentComboIndex + 1, String(this.combos.length).length);
+    log.info("Combination " + paddedComboIndex + "/" + this.combos.length + ": " +
+             this._comboName(combo).substring(1));
+
+    function changeConfig(config) {
+      log.debug("calling " + config.name);
+      let promise = config.applyConfig();
+      log.debug("called " + config.name);
+      return promise;
+    }
+
+    try {
+      // First go through and actually apply all of the configs
+      for (let i = 0; i < combo.length; i++) {
+        let config = combo[i];
+        if (!this._lastCombo || config !== this._lastCombo[i]) {
+          log.debug("promising", config.name);
+          yield changeConfig(config);
+        }
+      }
+
+      // Update the lastCombo since it's now been applied regardless of whether it's accepted below.
+      log.debug("fulfilled all applyConfig so setting lastCombo.");
+      this._lastCombo = combo;
+
+      // Then ask configs if the current setup is valid. We can't can do this in
+      // the applyConfig methods of the config since it doesn't know what configs
+      // later in the loop will do that may invalidate the combo.
+      for (let i = 0; i < combo.length; i++) {
+        let config = combo[i];
+        // A configuration can specify an optional verifyConfig method to indicate
+        // if the current config is valid for a screenshot. This gets called even
+        // if the this config was used in the lastCombo since another config may
+        // have invalidated it.
+        if (config.verifyConfig) {
+          log.debug("checking if the combo is valid with", config.name);
+          yield config.verifyConfig();
+        }
+      }
+    } catch (ex) {
+      log.warn("\tskipped configuration: " + ex);
+      // Don't set lastCombo here so that we properly know which configurations
+      // need to be applied since the last screenshot
+
+      // Return so we don't take a screenshot.
+      return;
+    }
+
+    yield this._onConfigurationReady(combo);
+  },
+
+  _onConfigurationReady(combo) {
+    let delayedScreenshot = () => {
+      let filename = padLeft(this.currentComboIndex + 1,
+                             String(this.combos.length).length) + this._comboName(combo);
+      return Screenshot.captureExternal(filename)
+        .then(() => {
+          this.completedCombos++;
+        });
+    };
+
+    log.debug("_onConfigurationReady");
+    return Task.spawn(delayedScreenshot);
+  },
+
+  _comboName(combo) {
+    return combo.reduce(function(a, b) {
+      return a + "_" + b.name;
+    }, "");
+  },
+};
+
+/**
+ * Helper to lazily compute the Cartesian product of all of the sets of configurations.
+ **/
+function LazyProduct(sets) {
+  /**
+   * An entry for each set with the value being:
+   * [the number of permutations of the sets with lower index,
+   *  the number of items in the set at the index]
+   */
+  this.sets = sets;
+  this.lookupTable = [];
+  let combinations = 1;
+  for (let i = this.sets.length - 1; i >= 0; i--) {
+    let set = this.sets[i];
+    let setLength = Object.keys(set).length;
+    this.lookupTable[i] = [combinations, setLength];
+    combinations *= setLength;
+  }
+}
+LazyProduct.prototype = {
+  get length() {
+    let last = this.lookupTable[0];
+    if (!last)
+      return 0;
+    return last[0] * last[1];
+  },
+
+  item(n) {
+    // For set i, get the item from the set with the floored value of
+    // (n / the number of permutations of the sets already chosen from) modulo the length of set i
+    let result = [];
+    for (let i = this.sets.length - 1; i >= 0; i--) {
+      let priorCombinations = this.lookupTable[i][0];
+      let setLength = this.lookupTable[i][1];
+      let keyIndex = Math.floor(n / priorCombinations) % setLength;
+      let keys = Object.keys(this.sets[i]);
+      result[i] = this.sets[i][keys[keyIndex]];
+    }
+    return result;
+  },
+};
+
+function padLeft(number, width, padding = "0") {
+  return padding.repeat(Math.max(0, width - String(number).length)) + number;
+}
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/bootstrap.js
@@ -0,0 +1,72 @@
+/* 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/. */
+/*
+#if 0
+Workaround a build system bug where this file doesn't get packaged if not pre-processed.
+#endif
+*/
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "TestRunner",
+                                  "chrome://mozscreenshots/content/TestRunner.jsm");
+
+function install(data, reason) {
+  if (!isAppSupported()) {
+    uninstallExtension(data);
+    return;
+  }
+
+  AddonManager.getAddonByID(data.id, function(addon) {
+    // Enable on install in case the user disabled a prior version
+    if (addon) {
+      addon.userDisabled = false;
+    }
+  });
+}
+
+function startup(data, reason) {
+  if (!isAppSupported()) {
+    uninstallExtension(data);
+    return;
+  }
+
+  AddonManager.getAddonByID(data.id, function(addon) {
+    let extensionPath = addon.getResourceURI();
+    TestRunner.init(extensionPath);
+  });
+}
+
+function shutdown(data, reason) { }
+
+function uninstall(data, reason) { }
+
+/**
+ * @return boolean whether the test suite applies to the application.
+ */
+function isAppSupported() {
+  return true;
+}
+
+function uninstallExtension(data) {
+  AddonManager.getAddonByID(data.id, function(addon) {
+    addon.uninstall();
+  });
+}
+
+function startRun() {
+  let env = Cc["@mozilla.org/process/environment;1"]
+              .getService(Ci.nsIEnvironment);
+  let setsEnv = env.get("MOZSCREENSHOTS_SETS");
+  let sets = setsEnv ? setsEnv.split(",") : null;
+  TestRunner.start(sets);
+}
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.jsm
@@ -0,0 +1,84 @@
+/* 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 = ["AppMenu"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+this.AppMenu = {
+
+  init(libDir) {},
+
+  configurations: {
+    appMenuClosed: {
+      applyConfig: Task.async(function*() {
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        browserWindow.PanelUI.hide();
+      }),
+    },
+
+    appMenuMainView: {
+      applyConfig() {
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        let promise = browserWindow.PanelUI.show();
+        browserWindow.PanelUI.showMainView();
+        return promise;
+      },
+    },
+
+    appMenuHistorySubview: {
+      applyConfig() {
+        // History has a footer
+        if (isCustomizing()) {
+          return Promise.reject("Can't show subviews while customizing");
+        }
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        let promise = browserWindow.PanelUI.show();
+        return promise.then(() => {
+          browserWindow.PanelUI.showMainView();
+          browserWindow.document.getElementById("history-panelmenu").click();
+        });
+      },
+
+      verifyConfig: verifyConfigHelper,
+    },
+
+    appMenuHelpSubview: {
+      applyConfig() {
+        if (isCustomizing()) {
+          return Promise.reject("Can't show subviews while customizing");
+        }
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        let promise = browserWindow.PanelUI.show();
+        return promise.then(() => {
+          browserWindow.PanelUI.showMainView();
+          browserWindow.document.getElementById("PanelUI-help").click();
+        });
+      },
+
+      verifyConfig: verifyConfigHelper,
+    },
+
+  },
+};
+
+function verifyConfigHelper() {
+  if (isCustomizing()) {
+    return Promise.reject("AppMenu verifyConfigHelper");
+  }
+  return Promise.resolve("AppMenu verifyConfigHelper");
+}
+
+function isCustomizing() {
+  let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+  if (browserWindow.document.documentElement.hasAttribute("customizing")) {
+    return true;
+  }
+  return false;
+}
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.jsm
@@ -0,0 +1,87 @@
+/* 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 = ["Buttons"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource:///modules/CustomizableUI.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+this.Buttons = {
+
+  init(libDir) {
+    createWidget();
+  },
+
+  configurations: {
+    navBarButtons: {
+      applyConfig: Task.async(() =>{
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        CustomizableUI.addWidgetToArea("screenshot-widget", CustomizableUI.AREA_NAVBAR);
+      }),
+    },
+
+    tabsToolbarButtons: {
+      applyConfig: Task.async(() => {
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        CustomizableUI.addWidgetToArea("screenshot-widget", CustomizableUI.AREA_TABSTRIP);
+      }),
+    },
+
+    menuPanelButtons: {
+      applyConfig: Task.async(() => {
+        CustomizableUI.addWidgetToArea("screenshot-widget", CustomizableUI.AREA_PANEL);
+      }),
+
+      verifyConfig() {
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        if (browserWindow.PanelUI.panel.state == "closed") {
+          return Promise.reject("The button isn't shown when the panel isn't open.");
+        }
+        return Promise.resolve("menuPanelButtons.verifyConfig");
+      },
+    },
+
+    custPaletteButtons: {
+      applyConfig: Task.async(() => {
+        CustomizableUI.removeWidgetFromArea("screenshot-widget");
+      }),
+
+      verifyConfig() {
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        if (browserWindow.document.documentElement.getAttribute("customizing") != "true") {
+          return Promise.reject("The button isn't shown when we're not in customize mode.");
+        }
+        return Promise.resolve("custPaletteButtons.verifyConfig");
+      },
+    },
+  },
+};
+
+function createWidget() {
+  let id = "screenshot-widget";
+  let spec = {
+    id: id,
+    label: "My Button",
+    removable: true,
+    tooltiptext: "",
+    type: "button",
+  };
+  CustomizableUI.createWidget(spec);
+
+  // Append a <style> for the image
+  let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+  let st = browserWindow.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
+  let styles = "" +
+        "#screenshot-widget > .toolbarbutton-icon {" +
+        "  list-style-image: url(chrome://browser/skin/Toolbar.png);" +
+        "  -moz-image-region: rect(0px, 18px, 18px, 0px);" +
+        "}";
+  st.appendChild(browserWindow.document.createTextNode(styles));
+  browserWindow.document.documentElement.appendChild(st);
+}
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.jsm
@@ -0,0 +1,61 @@
+/* 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 = ["CustomizeMode"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+this.CustomizeMode = {
+
+  init(libDir) {},
+
+  configurations: {
+    notCustomizing: {
+      applyConfig() {
+        return new Promise((resolve) => {
+          let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+          if (!browserWindow.document.documentElement.hasAttribute("customizing")) {
+            resolve("notCustomizing: already not customizing");
+            return;
+          }
+          function onCustomizationEnds() {
+            browserWindow.gNavToolbox.removeEventListener("aftercustomization",
+                                                          onCustomizationEnds);
+            // Wait for final changes
+            setTimeout(() => resolve("notCustomizing: onCustomizationEnds"), 500);
+          }
+          browserWindow.gNavToolbox.addEventListener("aftercustomization",
+                                                     onCustomizationEnds);
+          browserWindow.gCustomizeMode.exit();
+        });
+      },
+    },
+
+    customizing: {
+      applyConfig() {
+        return new Promise((resolve) => {
+          let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+          if (browserWindow.document.documentElement.hasAttribute("customizing")) {
+            resolve("customizing: already customizing");
+            return;
+          }
+          function onCustomizing() {
+            browserWindow.gNavToolbox.removeEventListener("customizationready",
+                                                          onCustomizing);
+            // Wait for final changes
+            setTimeout(() => resolve("customizing: onCustomizing"), 500);
+          }
+          browserWindow.gNavToolbox.addEventListener("customizationready",
+                                                     onCustomizing);
+          browserWindow.gCustomizeMode.enter();
+        });
+      },
+    },
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevEdition.jsm
@@ -0,0 +1,42 @@
+/* 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 = ["DevEdition"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const THEME_ID = "firefox-devedition@mozilla.org";
+
+Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+this.DevEdition = {
+  init(libDir) {},
+
+  configurations: {
+    devEditionLight: {
+      applyConfig: Task.async(() => {
+        Services.prefs.setCharPref("devtools.theme", "light");
+        LightweightThemeManager.currentTheme = LightweightThemeManager.getUsedTheme(THEME_ID);
+        Services.prefs.setBoolPref("browser.devedition.theme.showCustomizeButton", true);
+      }),
+    },
+    devEditionDark: {
+      applyConfig: Task.async(() => {
+        Services.prefs.setCharPref("devtools.theme", "dark");
+        LightweightThemeManager.currentTheme = LightweightThemeManager.getUsedTheme(THEME_ID);
+        Services.prefs.setBoolPref("browser.devedition.theme.showCustomizeButton", true);
+      }),
+    },
+    devEditionOff: {
+      applyConfig: Task.async(() => {
+        Services.prefs.clearUserPref("devtools.theme");
+        LightweightThemeManager.currentTheme = null;
+        Services.prefs.clearUserPref("browser.devedition.theme.showCustomizeButton");
+      }),
+    },
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.jsm
@@ -0,0 +1,50 @@
+/* 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 = ["DevTools"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://devtools/client/framework/gDevTools.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let TargetFactory = devtools.TargetFactory;
+
+function getTargetForSelectedTab() {
+  let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+  let target = TargetFactory.forTab(browserWindow.gBrowser.selectedTab);
+  return target;
+}
+
+this.DevTools = {
+  init(libDir) {
+    let panels = ["options", "webconsole", "inspector", "jsdebugger", "netmonitor"];
+    panels.forEach(panel => {
+      this.configurations[panel] = {};
+      this.configurations[panel].applyConfig = () => {
+        return gDevTools.showToolbox(getTargetForSelectedTab(), panel, "bottom");
+      };
+    });
+  },
+
+  configurations: {
+    bottomToolbox: {
+      applyConfig() {
+        return gDevTools.showToolbox(getTargetForSelectedTab(), "inspector", "bottom");
+      },
+    },
+    sideToolbox: {
+      applyConfig() {
+        return gDevTools.showToolbox(getTargetForSelectedTab(), "inspector", "side");
+      },
+    },
+    undockedToolbox: {
+      applyConfig() {
+        return gDevTools.showToolbox(getTargetForSelectedTab(), "inspector", "window");
+      },
+    }
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.jsm
@@ -0,0 +1,92 @@
+/* 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 = ["LightweightThemes"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+this.LightweightThemes = {
+  init(libDir) {
+    // convert -size 3000x200 canvas:black black_theme.png
+    let blackImage = libDir.clone();
+    blackImage.append("black_theme.png");
+    this._blackImageURL = Services.io.newFileURI(blackImage).spec;
+
+    // convert -size 3000x200 canvas:white white_theme.png
+    let whiteImage = libDir.clone();
+    whiteImage.append("white_theme.png");
+    this._whiteImageURL = Services.io.newFileURI(whiteImage).spec;
+  },
+
+  configurations: {
+    noLWT: {
+      applyConfig: Task.async(function*() {
+        LightweightThemeManager.currentTheme = null;
+      }),
+    },
+
+    darkLWT: {
+      applyConfig() {
+        LightweightThemeManager.setLocalTheme({
+          id:          "black",
+          name:        "black",
+          headerURL:   LightweightThemes._blackImageURL,
+          footerURL:   LightweightThemes._blackImageURL,
+          textcolor:   "#ffffff",
+          accentcolor: "#111111",
+        });
+
+        // Wait for LWT listener
+        return new Promise(resolve => {
+          setTimeout(() => {
+            resolve("darkLWT");
+          }, 500);
+        });
+      },
+
+      verifyConfig: verifyConfigHelper,
+    },
+
+    lightLWT: {
+      applyConfig() {
+        LightweightThemeManager.setLocalTheme({
+          id:          "white",
+          name:        "white",
+          headerURL:   LightweightThemes._whiteImageURL,
+          footerURL:   LightweightThemes._whiteImageURL,
+          textcolor:   "#000000",
+          accentcolor: "#eeeeee",
+        });
+        // Wait for LWT listener
+        return new Promise(resolve => {
+          setTimeout(() => {
+            resolve("lightLWT");
+          }, 500);
+        });
+      },
+
+      verifyConfig: verifyConfigHelper,
+    },
+
+  },
+};
+
+
+function verifyConfigHelper() {
+  return new Promise((resolve, reject) => {
+    let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+    if (browserWindow.document.documentElement.hasAttribute("lwtheme")) {
+      resolve("verifyConfigHelper");
+    } else {
+      reject("The @lwtheme attribute wasn't present so themes may not be available");
+    }
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.jsm
@@ -0,0 +1,53 @@
+/* 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 = ["Preferences"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+this.Preferences = {
+
+  init(libDir) {
+    Services.prefs.setBoolPref("browser.preferences.inContent", true);
+
+    let panes = [
+      ["paneGeneral", null],
+      ["paneSearch", null],
+      ["paneContent", null],
+      ["paneApplications", null],
+      ["panePrivacy", null],
+      ["paneSecurity", null],
+      ["paneSync", null],
+      ["paneAdvanced", "generalTab"],
+      ["paneAdvanced", "dataChoicesTab"],
+      ["paneAdvanced", "networkTab"],
+      ["paneAdvanced", "updateTab"],
+      ["paneAdvanced", "encryptionTab"],
+    ];
+    for (let [primary, advanced] of panes) {
+      let configName = primary + ("-" + advanced || "");
+      this.configurations[configName] = {};
+      this.configurations[configName].applyConfig = prefHelper.bind(null, primary, advanced);
+    }
+  },
+
+  configurations: {},
+};
+
+function prefHelper(primary, advanced) {
+  return new Promise((resolve) => {
+    let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+    if (primary == "paneAdvanced") {
+      browserWindow.openAdvancedPreferences(advanced);
+    } else {
+      browserWindow.openPreferences(primary);
+    }
+    setTimeout(resolve, 50);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.jsm
@@ -0,0 +1,143 @@
+/* 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 = ["Tabs"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+const CUST_TAB = "chrome://browser/skin/customizableui/customizeFavicon.ico";
+const PREFS_TAB = "chrome://browser/skin/preferences/in-content/favicon.ico";
+const DEFAULT_FAVICON_TAB = `data:text/html,<meta charset="utf-8">
+<title>No favicon</title>`;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+this.Tabs = {
+  init(libDir) {},
+
+  configurations: {
+    fiveTabs: {
+      applyConfig: Task.async(function*() {
+        fiveTabsHelper();
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        hoverTab(browserWindow.gBrowser.tabs[3]);
+      }),
+    },
+
+    fourPinned: {
+      applyConfig: Task.async(function*() {
+        fiveTabsHelper();
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        let tab = browserWindow.gBrowser.addTab(PREFS_TAB);
+        browserWindow.gBrowser.pinTab(tab);
+        tab = browserWindow.gBrowser.addTab(CUST_TAB);
+        browserWindow.gBrowser.pinTab(tab);
+        tab = browserWindow.gBrowser.addTab("about:privatebrowsing");
+        browserWindow.gBrowser.pinTab(tab);
+        tab = browserWindow.gBrowser.addTab("about:home");
+        browserWindow.gBrowser.pinTab(tab);
+        browserWindow.gBrowser.selectTabAtIndex(5);
+        hoverTab(browserWindow.gBrowser.tabs[2]);
+        // also hover the new tab button
+        let newTabButton = browserWindow.document.getAnonymousElementByAttribute(browserWindow.
+                           gBrowser.tabContainer, "class", "tabs-newtab-button");
+        hoverTab(newTabButton);
+        browserWindow.gBrowser.tabs[browserWindow.gBrowser.tabs.length - 1].
+                      setAttribute("beforehovered", true);
+      }),
+    },
+
+    twoPinnedWithOverflow: {
+      applyConfig: Task.async(function*() {
+        fiveTabsHelper();
+
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        browserWindow.gBrowser.loadTabs([
+          "about:addons",
+          "about:home",
+          DEFAULT_FAVICON_TAB,
+          "about:newtab",
+          "about:addons",
+          "about:home",
+          DEFAULT_FAVICON_TAB,
+          "about:newtab",
+          "about:addons",
+          "about:home",
+          DEFAULT_FAVICON_TAB,
+          "about:newtab",
+          "about:addons",
+          "about:home",
+          DEFAULT_FAVICON_TAB,
+          "about:newtab",
+          "about:addons",
+          "about:home",
+          DEFAULT_FAVICON_TAB,
+          "about:newtab",
+          "about:addons",
+          "about:home",
+          DEFAULT_FAVICON_TAB,
+          "about:newtab",
+          "about:addons",
+          "about:home",
+          DEFAULT_FAVICON_TAB,
+          "about:newtab",
+        ], true, true);
+        let tab = browserWindow.gBrowser.addTab(PREFS_TAB);
+        browserWindow.gBrowser.pinTab(tab);
+        tab = browserWindow.gBrowser.addTab(CUST_TAB);
+        browserWindow.gBrowser.pinTab(tab);
+        browserWindow.gBrowser.selectTabAtIndex(4);
+        hoverTab(browserWindow.gBrowser.tabs[6]);
+      }),
+    },
+  },
+};
+
+
+/* helpers */
+
+function fiveTabsHelper() {
+  // some with no favicon and some with. Selected tab in middle.
+  closeAllButOneTab("about:addons");
+
+  let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+  browserWindow.gBrowser.loadTabs([
+    "about:addons",
+    "about:home",
+    DEFAULT_FAVICON_TAB,
+    "about:newtab",
+    CUST_TAB,
+  ], true, true);
+  browserWindow.gBrowser.selectTabAtIndex(1);
+}
+
+function closeAllButOneTab(url = "about:blank") {
+  let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+  let gBrowser = browserWindow.gBrowser;
+  // Close all tabs except the last so we don't quit the browser.
+  while (gBrowser.tabs.length > 1)
+    gBrowser.removeTab(gBrowser.selectedTab, {animate: false});
+  gBrowser.selectedBrowser.loadURI(url);
+  if (gBrowser.selectedTab.pinned)
+    gBrowser.unpinTab(gBrowser.selectedTab);
+  let newTabButton = browserWindow.document.getAnonymousElementByAttribute(browserWindow.gBrowser.tabContainer, "class", "tabs-newtab-button");
+  hoverTab(newTabButton, false);
+}
+
+function hoverTab(tab, hover = true) {
+  const inIDOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+  if (hover) {
+    inIDOMUtils.addPseudoClassLock(tab, ":hover");
+  } else {
+    inIDOMUtils.clearPseudoClassLocks(tab);
+  }
+  // XXX TODO: this isn't necessarily testing what we ship
+  if (tab.nextElementSibling)
+    tab.nextElementSibling.setAttribute("afterhovered", hover || null);
+  if (tab.previousElementSibling)
+    tab.previousElementSibling.setAttribute("beforehovered", hover || null);
+}
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.jsm
@@ -0,0 +1,37 @@
+/* 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 = ["TabsInTitlebar"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+const PREF_TABS_IN_TITLEBAR = "browser.tabs.drawInTitlebar";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+this.TabsInTitlebar = {
+
+  init(libDir) {},
+
+  configurations: {
+    tabsInTitlebar: {
+      applyConfig: Task.async(function*() {
+        if (Services.appinfo.OS == "Linux") {
+          return Promise.reject("TabsInTitlebar isn't supported on Linux");
+        }
+        Services.prefs.setBoolPref(PREF_TABS_IN_TITLEBAR, true);
+      }),
+    },
+
+    tabsOutsideTitlebar: {
+      applyConfig: Task.async(function*() {
+        Services.prefs.setBoolPref(PREF_TABS_IN_TITLEBAR, false);
+      }),
+    },
+
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.jsm
@@ -0,0 +1,56 @@
+/* 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 = ["Toolbars"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+this.Toolbars = {
+  init(libDir) {},
+
+  configurations: {
+    onlyNavBar: {
+      applyConfig: Task.async(function*() {
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        let personalToolbar = browserWindow.document.getElementById("PersonalToolbar");
+        browserWindow.setToolbarVisibility(personalToolbar, false);
+        toggleMenubarIfNecessary(false);
+      }),
+    },
+
+    allToolbars: {
+      applyConfig: Task.async(function*() { // Boookmarks and menubar
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        let personalToolbar = browserWindow.document.getElementById("PersonalToolbar");
+        browserWindow.setToolbarVisibility(personalToolbar, true);
+        toggleMenubarIfNecessary(true);
+      }),
+
+      verifyConfig: Task.async(function*() {
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        if (browserWindow.fullScreen) {
+          return Promise.reject("The bookmark toolbar and menubar are not shown in fullscreen.");
+        }
+      }),
+    },
+
+  },
+};
+
+
+///// helpers /////
+
+function toggleMenubarIfNecessary(visible) {
+  let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+  // The menubar is not shown on OS X or while in fullScreen
+  if (Services.appinfo.OS != "Darwin" /*&& !browserWindow.fullScreen*/) {
+    let menubar = browserWindow.document.getElementById("toolbar-menubar");
+    browserWindow.setToolbarVisibility(menubar, visible);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.jsm
@@ -0,0 +1,60 @@
+/* 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 = ["WindowSize"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+this.WindowSize = {
+
+  init(libDir) {
+    Services.prefs.setBoolPref("browser.fullscreen.autohide", false);
+  },
+
+  configurations: {
+    maximized: {
+      applyConfig: Task.async(function*() {
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        browserWindow.fullScreen = false;
+
+        // Wait for the Lion fullscreen transition to end as there doesn't seem to be an event
+        // and trying to maximize while still leaving fullscreen doesn't work.
+        yield new Promise((resolve, reject) => {
+          setTimeout(function waitToLeaveFS() {
+            browserWindow.maximize();
+            resolve();
+          }, Services.appinfo.OS == "Darwin" ? 1500 : 0);
+        });
+      }),
+    },
+
+    normal: {
+      applyConfig: Task.async(() => {
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        browserWindow.fullScreen = false;
+        browserWindow.restore();
+      }),
+    },
+
+    fullScreen: {
+      applyConfig: Task.async(function*() {
+        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        browserWindow.fullScreen = true;
+        // OS X Lion fullscreen transition takes a while
+        yield new Promise((resolve, reject) => {
+          setTimeout(function waitAfterEnteringFS() {
+            resolve();
+          }, Services.appinfo.OS == "Darwin" ? 1500 : 0);
+        });
+      }),
+    },
+
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/install.rdf
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<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>mozscreenshots@mozilla.org</em:id>
+#expand <em:version>__MOZILLA_VERSION_U__</em:version>
+    <em:bootstrap>true</em:bootstrap>
+
+    <!-- for running custom screenshot binaries -->
+    <em:unpack>true</em:unpack>
+
+    <!-- Front End MetaData -->
+    <em:name>mozscreenshots</em:name>
+    <em:description>Take screenshots of Mozilla applications in various UI configurations.</em:description>
+    <em:creator>Mozilla</em:creator>
+
+    <em:targetApplication>
+      <Description>
+        <!-- Firefox -->
+        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+#expand <em:minVersion>__MOZILLA_VERSION_U__</em:minVersion>
+        <em:maxVersion>*</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/jar.mn
@@ -0,0 +1,6 @@
+mozscreenshots.jar:
+% content mozscreenshots chrome/mozscreenshots/
+  Screenshot.jsm
+  TestRunner.jsm
+  configurations/ (configurations/*.jsm)
+  lib/            (lib/*.png)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..810420d967a6aa6211494f67cab112b161e300d2
GIT binary patch
literal 343
zc%17D@N?(olHy`uVBq!ia0y~y;NAgbpI`(M3<2H~-vcR@bVpxD28NCO+<y{TfqaEz
zk04(LhAK4%hK3dfhF?ITh8GMBr3MTPuM!v-tY$DUh!@P+6=(yLU@!6Xb!ETLDlV!d
z(km{03Mj;s<n8Xlz<9SycnOdf@9E+gA|d(qpdrZ0Lkho_$MMJ~Fi;5`@Krs}!07F#
zGa1NHEpd$~Nl7e8wMs5Z1yT$~28O1(2Bx}3#vw+AR>me)Mh3bD=2iv<;aAwWP&DM`
Zr(~v8;?|(nxo;cDK2KLamvv4FO#t)_Lrnkx
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ee0ef460df4c9b82a548628d756ac93ba4e3f5f0
GIT binary patch
literal 522
zc%17D@N?(olHy`uVBq!ia0y~y;NAgbpI`(M3<2H~-vcR@bVpxD28NCO+<y{TfqaEz
zk04(LhAK4%hK3dfhF?ITh8GMBr3MTPuM!v-tY$DUh!@P+6=(yLU@!6Xb!ETLDlV$b
zmcBLoFi?mo$=lt9f$?sa@Dc_FMj=lZ$B>F!Z!Z`Et>ihp;FtN_lQ)=!B5GzZy}$SI
zoh+QVYkhC;V>nTLe0TXCB;tEt`TGhufk)%xyF_aQ68JQ(J3uXyP&9_H$JRU%TU(m>
z78q8lC9V-ADTyViR>?)FK#IZ0z|d6Jz*N`BIK;@%%GlJ(z)aV`+{(aU=HvKc6b-rg
aDVb@NxHY`k_xwC4l09AhT-G@yGywq3srZrr
new file mode 100644
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPI_NAME = 'mozscreenshots'
+
+JAR_MANIFESTS += ['jar.mn']
+
+USE_EXTENSION_MANIFEST = True
+NO_JS_MANIFEST = True
+
+FINAL_TARGET_PP_FILES += [
+    'bootstrap.js',
+    'install.rdf',
+]