summaryrefslogtreecommitdiffstats
path: root/browser/tools/mozscreenshots
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/tools/mozscreenshots/controlCenter/browser.ini8
-rw-r--r--browser/tools/mozscreenshots/controlCenter/browser_controlCenter.js16
-rw-r--r--browser/tools/mozscreenshots/devtools/browser.ini7
-rw-r--r--browser/tools/mozscreenshots/devtools/browser_devtools.js23
-rw-r--r--browser/tools/mozscreenshots/head.js65
-rw-r--r--browser/tools/mozscreenshots/moz.build37
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/Makefile.in12
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.sys.mjs196
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/TestRunner.sys.mjs650
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/api.js26
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.sys.mjs77
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.sys.mjs97
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.sys.mjs320
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.sys.mjs72
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.sys.mjs74
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.sys.mjs51
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.sys.mjs168
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.sys.mjs183
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.sys.mjs219
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.sys.mjs28
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.sys.mjs54
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/UIDensities.sys.mjs42
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.sys.mjs68
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/borderify.xpibin0 -> 1611 bytes
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed.html15
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed_active.html14
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed_passive.html14
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/password.html17
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/tracking.html17
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-script.js21
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-style.css28
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots.html83
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/permissionPrompts.html64
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot.pngbin0 -> 7599 bytes
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_center.pngbin0 -> 2319 bytes
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_cropped_diagonal.pngbin0 -> 1306 bytes
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_diagonal.pngbin0 -> 2100 bytes
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_uncropped.pngbin0 -> 6596 bytes
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_upperleft.pngbin0 -> 1594 bytes
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/manifest.json23
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/moz.build58
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/schema.json1
-rw-r--r--browser/tools/mozscreenshots/permissionPrompts/browser.ini10
-rw-r--r--browser/tools/mozscreenshots/permissionPrompts/browser_permissionPrompts.js16
-rw-r--r--browser/tools/mozscreenshots/preferences/browser.ini7
-rw-r--r--browser/tools/mozscreenshots/preferences/browser_preferences.js16
-rw-r--r--browser/tools/mozscreenshots/primaryUI/browser.ini7
-rw-r--r--browser/tools/mozscreenshots/primaryUI/browser_primaryUI.js23
-rw-r--r--browser/tools/mozscreenshots/tests/browser/browser.ini8
-rw-r--r--browser/tools/mozscreenshots/tests/browser/browser_boundingbox.js173
-rw-r--r--browser/tools/mozscreenshots/tests/browser/browser_screenshots.js21
-rw-r--r--browser/tools/mozscreenshots/tests/browser/browser_screenshots_cropping.js153
-rw-r--r--browser/tools/mozscreenshots/tests/xpcshell/test_testConfigurations.js64
-rw-r--r--browser/tools/mozscreenshots/tests/xpcshell/xpcshell.ini5
54 files changed, 3351 insertions, 0 deletions
diff --git a/browser/tools/mozscreenshots/controlCenter/browser.ini b/browser/tools/mozscreenshots/controlCenter/browser.ini
new file mode 100644
index 0000000000..d641364fa3
--- /dev/null
+++ b/browser/tools/mozscreenshots/controlCenter/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+subsuite = screenshots
+support-files =
+ ../head.js
+
+[browser_controlCenter.js]
+https_first_disabled = true
+skip-if = os == 'mac' # macosx1014 times out, see bug 1554821
diff --git a/browser/tools/mozscreenshots/controlCenter/browser_controlCenter.js b/browser/tools/mozscreenshots/controlCenter/browser_controlCenter.js
new file mode 100644
index 0000000000..e5692ddf0d
--- /dev/null
+++ b/browser/tools/mozscreenshots/controlCenter/browser_controlCenter.js
@@ -0,0 +1,16 @@
+/* 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/. */
+
+/* import-globals-from ../head.js */
+
+"use strict";
+
+add_task(async function capture() {
+ if (!shouldCapture()) {
+ return;
+ }
+ let sets = ["LightweightThemes", "ControlCenter"];
+
+ await TestRunner.start(sets, "controlCenter");
+});
diff --git a/browser/tools/mozscreenshots/devtools/browser.ini b/browser/tools/mozscreenshots/devtools/browser.ini
new file mode 100644
index 0000000000..551112ef26
--- /dev/null
+++ b/browser/tools/mozscreenshots/devtools/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+subsuite = screenshots
+support-files =
+ ../head.js
+
+[browser_devtools.js]
+skip-if = os == 'mac' # times out on macosx1014, see 1570100
diff --git a/browser/tools/mozscreenshots/devtools/browser_devtools.js b/browser/tools/mozscreenshots/devtools/browser_devtools.js
new file mode 100644
index 0000000000..7d0efe0319
--- /dev/null
+++ b/browser/tools/mozscreenshots/devtools/browser_devtools.js
@@ -0,0 +1,23 @@
+/* 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/. */
+
+/* import-globals-from ../head.js */
+
+"use strict";
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const { gDevTools } = require("devtools/client/framework/devtools");
+
+add_task(async function capture() {
+ if (!shouldCapture()) {
+ return;
+ }
+ let sets = ["DevTools"];
+
+ await TestRunner.start(sets, "devtools");
+
+ await gDevTools.closeToolboxForTab(gBrowser.selectedTab);
+});
diff --git a/browser/tools/mozscreenshots/head.js b/browser/tools/mozscreenshots/head.js
new file mode 100644
index 0000000000..5e21ca75aa
--- /dev/null
+++ b/browser/tools/mozscreenshots/head.js
@@ -0,0 +1,65 @@
+/* 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/. */
+
+/* exported TestRunner, shouldCapture */
+
+"use strict";
+
+const chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+);
+const EXTENSION_DIR =
+ "chrome://mochitests/content/browser/browser/tools/mozscreenshots/mozscreenshots/extension/mozscreenshots/browser/";
+
+let TestRunner;
+
+async function setup() {
+ // This timeout doesn't actually end the job even if it is hit - the buildbot timeout will
+ // handle things for us if the test actually hangs.
+ requestLongerTimeout(100);
+
+ // Generate output so mozprocess knows we're still alive for the long session.
+ SimpleTest.requestCompleteLog();
+
+ info("installing extension temporarily");
+ let chromeURL = Services.io.newURI(EXTENSION_DIR);
+ let dir = chromeRegistry
+ .convertChromeURL(chromeURL)
+ .QueryInterface(Ci.nsIFileURL).file;
+ await AddonManager.installTemporaryAddon(dir);
+
+ info("Checking for mozscreenshots extension");
+
+ let aAddon = await AddonManager.getAddonByID("mozscreenshots@mozilla.org");
+ isnot(aAddon, null, "The mozscreenshots extension should be installed");
+ TestRunner = ChromeUtils.importESModule(
+ "resource://mozscreenshots/TestRunner.sys.mjs"
+ ).TestRunner;
+ TestRunner.initTest(this);
+}
+
+/**
+ * Used by pre-defined sets of configurations to decide whether to run for a build.
+ * @note This is not used by browser_screenshots.js which handles when MOZSCREENSHOTS_SETS is set.
+ * @return {bool} whether to capture screenshots.
+ */
+function shouldCapture() {
+ if (Services.env.get("MOZSCREENSHOTS_SETS")) {
+ ok(
+ true,
+ "MOZSCREENSHOTS_SETS was specified so only capture what was " +
+ "requested (in browser_screenshots.js)"
+ );
+ return false;
+ }
+
+ if (AppConstants.MOZ_UPDATE_CHANNEL == "nightly") {
+ ok(true, "Screenshots aren't captured on Nightlies");
+ return false;
+ }
+
+ return true;
+}
+
+add_setup(setup);
diff --git a/browser/tools/mozscreenshots/moz.build b/browser/tools/mozscreenshots/moz.build
new file mode 100644
index 0000000000..6408b1187b
--- /dev/null
+++ b/browser/tools/mozscreenshots/moz.build
@@ -0,0 +1,37 @@
+# -*- Mode: python; 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "General")
+
+with Files("controlCenter/**"):
+ BUG_COMPONENT = ("Firefox", "Site Identity")
+
+with Files("devtools/**"):
+ BUG_COMPONENT = ("DevTools", "General")
+
+with Files("permissionPrompts/**"):
+ BUG_COMPONENT = ("Firefox", "Site Permissions")
+
+with Files("preferences/**"):
+ BUG_COMPONENT = ("Firefox", "Settings UI")
+
+BROWSER_CHROME_MANIFESTS += [
+ # Each test is in it's own directory so it gets run in a clean profile with
+ # run-by-dir.
+ "controlCenter/browser.ini",
+ "devtools/browser.ini",
+ "permissionPrompts/browser.ini",
+ "preferences/browser.ini",
+ "primaryUI/browser.ini",
+ "tests/browser/browser.ini",
+]
+
+TEST_DIRS += [
+ "mozscreenshots/extension",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"]
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/Makefile.in b/browser/tools/mozscreenshots/mozscreenshots/extension/Makefile.in
new file mode 100644
index 0000000000..aa496e27af
--- /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/.
+
+OUTPUT_DIR = $(DEPTH)/_tests/testing/mochitest/browser/browser/tools/mozscreenshots/mozscreenshots/extension
+GENERATED_DIRS = $(OUTPUT_DIR)
+XPI_PKGNAME = mozscreenshots@mozilla.org
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ (cd $(DIST)/xpi-stage && tar $(TAR_CREATE_FLAGS) - $(XPI_NAME)) | (cd $(OUTPUT_DIR) && tar -xf -)
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.sys.mjs
new file mode 100644
index 0000000000..3db9fe3e0c
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.sys.mjs
@@ -0,0 +1,196 @@
+/* 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/. */
+
+import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
+// See LOG_LEVELS in Console.sys.mjs. Common examples: "All", "Info", "Warn", &
+// "Error".
+const PREF_LOG_LEVEL = "extensions.mozscreenshots@mozilla.org.loglevel";
+const lazy = {};
+XPCOMUtils.defineLazyGetter(lazy, "log", () => {
+ let { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ let consoleOptions = {
+ maxLogLevel: "info",
+ maxLogLevelPref: PREF_LOG_LEVEL,
+ prefix: "mozscreenshots",
+ };
+ return new ConsoleAPI(consoleOptions);
+});
+
+export var 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, 0o755);
+ }
+
+ 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");
+ }
+ },
+
+ _buildImagePath(baseName) {
+ return PathUtils.join(
+ this._path,
+ this._imagePrefix + baseName + this._imageExtension
+ );
+ },
+
+ // Capture the whole screen using an external application.
+ async captureExternal(filename) {
+ let imagePath = this._buildImagePath(filename);
+ await this._screenshotFunction(imagePath);
+ lazy.log.debug("saved screenshot: " + filename);
+ return imagePath;
+ },
+
+ // helpers
+
+ _screenshotWindows(filename) {
+ return new Promise((resolve, reject) => {
+ let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+ exe.append("screenshot.exe");
+ if (!exe.exists()) {
+ exe = Services.dirsvc.get("CurWorkD", Ci.nsIFile).parent;
+ exe.append("bin");
+ 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)
+ );
+ });
+ },
+
+ async _screenshotOSX(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"];
+ args.push(filename);
+ process.runAsync(
+ args,
+ args.length,
+ this._processObserver(resolve, reject)
+ );
+ });
+ };
+
+ function readWindowID() {
+ return IOUtils.readUTF8("/tmp/mozscreenshots-windowid");
+ }
+
+ 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)
+ );
+ });
+ };
+
+ await promiseWindowID();
+ let windowID = await readWindowID();
+ await screencapture(windowID);
+ },
+
+ _screenshotLinux(filename) {
+ return new Promise((resolve, reject) => {
+ let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+ exe.append("screentopng");
+ if (!exe.exists()) {
+ exe = Services.dirsvc.get("CurWorkD", Ci.nsIFile).parent;
+ exe.append("bin");
+ exe.append("screentopng");
+ }
+ 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)
+ );
+ });
+ },
+
+ _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;
+ }
+ },
+ };
+ },
+};
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/TestRunner.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/TestRunner.sys.mjs
new file mode 100644
index 0000000000..c0623d6e72
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/TestRunner.sys.mjs
@@ -0,0 +1,650 @@
+/* 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/. */
+
+const APPLY_CONFIG_TIMEOUT_MS = 60 * 1000;
+const HOME_PAGE = "resource://mozscreenshots/lib/mozscreenshots.html";
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+import { Rect } from "resource://gre/modules/Geometry.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserTestUtils: "resource://testing-common/BrowserTestUtils.sys.mjs",
+ // Screenshot.sys.mjs must be imported this way for xpcshell tests to work
+ Screenshot: "resource://mozscreenshots/Screenshot.sys.mjs",
+});
+
+export var TestRunner = {
+ combos: null,
+ completedCombos: 0,
+ currentComboIndex: 0,
+ _lastCombo: null,
+ _libDir: null,
+ croppingPadding: 0,
+ mochitestScope: null,
+
+ init(extensionPath) {
+ this._extensionPath = extensionPath;
+ this.setupOS();
+ },
+
+ /**
+ * Initialize the mochitest interface. This allows TestRunner to integrate
+ * with mochitest functions like is(...) and ok(...). This must be called
+ * prior to invoking any of the TestRunner functions. Note that this should
+ * be properly setup in head.js, so you probably don't need to call it.
+ */
+ initTest(mochitestScope) {
+ this.mochitestScope = mochitestScope;
+ },
+
+ setupOS() {
+ switch (AppConstants.platform) {
+ case "macosx": {
+ this.disableNotificationCenter();
+ break;
+ }
+ }
+ },
+
+ disableNotificationCenter() {
+ let killall = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ killall.initWithPath("/bin/bash");
+
+ let killallP = Cc["@mozilla.org/process/util;1"].createInstance(
+ Ci.nsIProcess
+ );
+ killallP.init(killall);
+ let ncPlist =
+ "/System/Library/LaunchAgents/com.apple.notificationcenterui.plist";
+ let killallArgs = [
+ "-c",
+ `/bin/launchctl unload -w ${ncPlist} && ` +
+ "/usr/bin/killall -v NotificationCenter",
+ ];
+ killallP.run(true, killallArgs, killallArgs.length);
+ },
+
+ /**
+ * Load specified sets, execute all combinations of them, and capture screenshots.
+ */
+ async start(setNames, jobName = null) {
+ let subDirs = [
+ "mozscreenshots",
+ new Date().toISOString().replace(/:/g, "-") + "_" + Services.appinfo.OS,
+ ];
+ let screenshotPath = PathUtils.join(PathUtils.tempDir, ...subDirs);
+
+ const MOZ_UPLOAD_DIR = Services.env.get("MOZ_UPLOAD_DIR");
+ const GECKO_HEAD_REPOSITORY = Services.env.get("GECKO_HEAD_REPOSITORY");
+ // We don't want to upload images (from MOZ_UPLOAD_DIR) on integration
+ // branches in order to reduce bandwidth/storage.
+ if (MOZ_UPLOAD_DIR && !GECKO_HEAD_REPOSITORY.includes("/integration/")) {
+ screenshotPath = MOZ_UPLOAD_DIR;
+ }
+
+ this.mochitestScope.info(`Saving screenshots to: ${screenshotPath}`);
+
+ let screenshotPrefix = Services.appinfo.appBuildID;
+ if (jobName) {
+ screenshotPrefix += "-" + jobName;
+ }
+ screenshotPrefix += "_";
+ lazy.Screenshot.init(screenshotPath, this._extensionPath, screenshotPrefix);
+ this._libDir = this._extensionPath
+ .QueryInterface(Ci.nsIFileURL)
+ .file.clone();
+ this._libDir.append("chrome");
+ this._libDir.append("mozscreenshots");
+ this._libDir.append("lib");
+
+ let sets = this.loadSets(setNames);
+
+ this.mochitestScope.info(`${sets.length} sets: ${setNames}`);
+ this.combos = new LazyProduct(sets);
+ this.mochitestScope.info(this.combos.length + " combinations");
+
+ this.currentComboIndex = this.completedCombos = 0;
+ this._lastCombo = null;
+
+ // Setup some prefs
+ 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);
+ // Disable some animations that can cause false positives, such as the
+ // reload/stop button spinning animation.
+ Services.prefs.setIntPref("ui.prefersReducedMotion", 1);
+
+ let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+
+ // Prevent the mouse cursor from causing hover styles or tooltips to appear.
+ browserWindow.windowUtils.disableNonTestMouseEvents(true);
+
+ // When being automated through Marionette, Firefox shows a prominent indication
+ // in the urlbar and identity block. We don't want this to show when testing browser UI.
+ // Note that this doesn't prevent subsequently opened windows from showing the automation UI.
+ browserWindow.document
+ .getElementById("main-window")
+ .removeAttribute("remotecontrol");
+
+ let selectedBrowser = browserWindow.gBrowser.selectedBrowser;
+ lazy.BrowserTestUtils.loadURIString(selectedBrowser, HOME_PAGE);
+ await lazy.BrowserTestUtils.browserLoaded(selectedBrowser);
+
+ for (let i = 0; i < this.combos.length; i++) {
+ this.currentComboIndex = i;
+ await this._performCombo(this.combos.item(this.currentComboIndex));
+ }
+
+ this.mochitestScope.info(
+ "Done: Completed " +
+ this.completedCombos +
+ " out of " +
+ this.combos.length +
+ " configurations."
+ );
+ this.cleanup();
+ },
+
+ /**
+ * Helper function for loadSets. This filters out the restricted configs from setName.
+ * This was made a helper function to facilitate xpcshell unit testing.
+ * @param {String} setName - set name to be filtered e.g. "Toolbars[onlyNavBar,allToolbars]"
+ * @return {Object} Returns an object with two values: the filtered set name and a set of
+ * restricted configs.
+ */
+ filterRestrictions(setName) {
+ let match = /\[([^\]]+)\]$/.exec(setName);
+ if (!match) {
+ throw new Error(`Invalid restrictions in ${setName}`);
+ }
+ // Trim the restrictions from the set name.
+ setName = setName.slice(0, match.index);
+ let restrictions = match[1]
+ .split(",")
+ .reduce((set, name) => set.add(name.trim()), new Set());
+
+ return { trimmedSetName: setName, restrictions };
+ },
+
+ /**
+ * 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) {
+ let restrictions = null;
+ if (setName.includes("[")) {
+ let filteredData = this.filterRestrictions(setName);
+ setName = filteredData.trimmedSetName;
+ restrictions = filteredData.restrictions;
+ }
+ let imported = ChromeUtils.import(
+ `resource://mozscreenshots/configurations/${setName}.jsm`
+ );
+ 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"
+ );
+ }
+ // Checks to see if nonexistent configuration have been specified
+ if (restrictions) {
+ let incorrectConfigs = [...restrictions].filter(
+ r => !configurationNames.includes(r)
+ );
+ if (incorrectConfigs.length) {
+ throw new Error("non existent configurations: " + incorrectConfigs);
+ }
+ }
+ let configurations = {};
+ 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;
+ // Filter restricted configurations.
+ if (!restrictions || restrictions.has(config)) {
+ configurations[config] = imported[setName].configurations[config];
+ }
+ }
+ sets.push(configurations);
+ }
+ 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);
+ lazy.BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ "data:text/html;charset=utf-8,<h1>Done!"
+ );
+ browserWindow.restore();
+ Services.prefs.clearUserPref("ui.caretBlinkTime");
+ Services.prefs.clearUserPref("ui.prefersReducedMotion");
+ browserWindow.windowUtils.disableNonTestMouseEvents(false);
+ },
+
+ // helpers
+
+ /**
+ * Calculate the bounding box based on CSS selector from config for cropping
+ *
+ * @param {String[]} selectors - array of CSS selectors for relevant DOM element
+ * @return {Geometry.sys.mjs Rect} Rect holding relevant x, y, width, height with padding
+ **/
+ _findBoundingBox(selectors, windowType) {
+ if (!selectors.length) {
+ throw new Error("No selectors specified.");
+ }
+
+ // Set window type, default "navigator:browser"
+ windowType = windowType || "navigator:browser";
+ let browserWindow = Services.wm.getMostRecentWindow(windowType);
+ // Scale for high-density displays
+ const scale = Cc["@mozilla.org/gfx/screenmanager;1"]
+ .getService(Ci.nsIScreenManager)
+ .screenForRect(
+ browserWindow.screenX,
+ browserWindow.screenY,
+ 1,
+ 1
+ ).defaultCSSScaleFactor;
+
+ const windowLeft = browserWindow.screenX * scale;
+ const windowTop = browserWindow.screenY * scale;
+ const windowWidth = browserWindow.outerWidth * scale;
+ const windowHeight = browserWindow.outerHeight * scale;
+
+ let bounds;
+ const rects = [];
+ // Grab bounding boxes and find the union
+ for (let selector of selectors) {
+ let elements;
+ // Check for function to find anonymous content
+ if (typeof selector == "function") {
+ elements = [selector()];
+ } else {
+ elements = browserWindow.document.querySelectorAll(selector);
+ }
+
+ if (!elements.length) {
+ throw new Error(`No element for '${selector}' found.`);
+ }
+
+ for (let element of elements) {
+ // Calculate box region, convert to Rect
+ let elementRect = element.getBoundingClientRect();
+ // ownerGlobal doesn't exist in content privileged windows.
+ // eslint-disable-next-line mozilla/use-ownerGlobal
+ let win = element.ownerDocument.defaultView;
+ let rect = new Rect(
+ (win.mozInnerScreenX + elementRect.left) * scale,
+ (win.mozInnerScreenY + elementRect.top) * scale,
+ elementRect.width * scale,
+ elementRect.height * scale
+ );
+ rect.inflateFixed(this.croppingPadding * scale);
+ rect.left = Math.max(rect.left, windowLeft);
+ rect.top = Math.max(rect.top, windowTop);
+ rect.right = Math.min(rect.right, windowLeft + windowWidth);
+ rect.bottom = Math.min(rect.bottom, windowTop + windowHeight);
+
+ if (rect.width === 0 && rect.height === 0) {
+ this.mochitestScope.todo(
+ false,
+ `Selector '${selector}' gave a 0x0 rect`
+ );
+ continue;
+ }
+
+ rects.push(rect);
+
+ if (!bounds) {
+ bounds = rect;
+ } else {
+ bounds = bounds.union(rect);
+ }
+ }
+ }
+
+ return { bounds, rects };
+ },
+
+ _do_skip(reason, combo, config, func) {
+ const { todo } = reason;
+ if (todo) {
+ this.mochitestScope.todo(
+ false,
+ `Skipped configuration ` +
+ `[ ${combo.map(e => e.name).join(", ")} ] for failure in ` +
+ `${config.name}.${func}: ${todo}`
+ );
+ } else {
+ this.mochitestScope.info(
+ `\tSkipped configuration ` +
+ `[ ${combo.map(e => e.name).join(", ")} ] ` +
+ `for "${reason}" in ${config.name}.${func}`
+ );
+ }
+ },
+
+ async _performCombo(combo) {
+ let paddedComboIndex = padLeft(
+ this.currentComboIndex + 1,
+ String(this.combos.length).length
+ );
+ this.mochitestScope.info(
+ `Combination ${paddedComboIndex}/${this.combos.length}: ${this._comboName(
+ combo
+ ).substring(1)}`
+ );
+
+ // Notice that this does need to be a closure, not a function, as otherwise
+ // "this" gets replaced and we lose access to this.mochitestScope.
+ const changeConfig = config => {
+ this.mochitestScope.info("calling " + config.name);
+
+ let applyPromise = Promise.resolve(config.applyConfig());
+ let timeoutPromise = new Promise((resolve, reject) => {
+ setTimeout(reject, APPLY_CONFIG_TIMEOUT_MS, "Timed out");
+ });
+
+ this.mochitestScope.info("called " + config.name);
+ // Add a default timeout of 700ms to avoid conflicts when configurations
+ // try to apply at the same time. e.g WindowSize and TabsInTitlebar
+ return Promise.race([applyPromise, timeoutPromise]).then(result => {
+ return new Promise(resolve => {
+ setTimeout(() => resolve(result), 700);
+ });
+ });
+ };
+
+ 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]) {
+ this.mochitestScope.info(`promising ${config.name}`);
+ const reason = await changeConfig(config);
+ if (reason) {
+ this._do_skip(reason, combo, config, "applyConfig");
+ return;
+ }
+ }
+ }
+
+ // Update the lastCombo since it's now been applied regardless of whether it's accepted below.
+ this.mochitestScope.info(
+ "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) {
+ this.mochitestScope.info(
+ `checking if the combo is valid with ${config.name}`
+ );
+ const reason = await config.verifyConfig();
+ if (reason) {
+ this._do_skip(reason, combo, config, "applyConfig");
+ return;
+ }
+ }
+ }
+ } catch (ex) {
+ this.mochitestScope.ok(
+ false,
+ `Unexpected exception in [ ${combo
+ .map(({ name }) => name)
+ .join(", ")} ]: ${ex.toString()}`
+ );
+ this.mochitestScope.info(`\t${ex}`);
+ if (ex.stack) {
+ this.mochitestScope.info(`\t${ex.stack}`);
+ }
+ return;
+ }
+ this.mochitestScope.info(
+ `Configured UI for [ ${combo
+ .map(({ name }) => name)
+ .join(", ")} ] successfully`
+ );
+
+ // Collect selectors from combo configs for cropping region
+ let windowType;
+ const finalSelectors = [];
+ for (const obj of combo) {
+ if (!windowType) {
+ windowType = obj.windowType;
+ } else if (windowType !== obj.windowType) {
+ this.mochitestScope.ok(
+ false,
+ "All configurations in the combo have a single window type"
+ );
+ return;
+ }
+ for (const selector of obj.selectors) {
+ finalSelectors.push(selector);
+ }
+ }
+
+ const { bounds, rects } = this._findBoundingBox(finalSelectors, windowType);
+ this.mochitestScope.ok(bounds, "A valid bounding box was found");
+ if (!bounds) {
+ return;
+ }
+ await this._onConfigurationReady(combo, bounds, rects);
+ },
+
+ async _onConfigurationReady(combo, bounds, rects) {
+ let filename =
+ padLeft(this.currentComboIndex + 1, String(this.combos.length).length) +
+ this._comboName(combo);
+ const imagePath = await lazy.Screenshot.captureExternal(filename);
+
+ let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ await this._cropImage(
+ browserWindow,
+ PathUtils.toFileURI(imagePath),
+ bounds,
+ rects,
+ imagePath
+ ).catch(msg => {
+ throw new Error(
+ `Cropping combo [${combo.map(e => e.name).join(", ")}] failed: ${msg}`
+ );
+ });
+ this.completedCombos++;
+ this.mochitestScope.info("_onConfigurationReady");
+ },
+
+ _comboName(combo) {
+ return combo.reduce(function (a, b) {
+ return a + "_" + b.name;
+ }, "");
+ },
+
+ async _cropImage(window, srcPath, bounds, rects, targetPath) {
+ const { document, Image } = window;
+ const promise = new Promise((resolve, reject) => {
+ const img = new Image();
+ img.onload = () => {
+ // Clip the cropping region to the size of the screenshot
+ // This is necessary mostly to deal with offscreen windows, since we
+ // are capturing an image of the operating system's desktop.
+ bounds.left = Math.max(0, bounds.left);
+ bounds.right = Math.min(img.naturalWidth, bounds.right);
+ bounds.top = Math.max(0, bounds.top);
+ bounds.bottom = Math.min(img.naturalHeight, bounds.bottom);
+
+ // Create a new offscreen canvas with the width and height given by the
+ // size of the region we want to crop to
+ const canvas = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.width = bounds.width;
+ canvas.height = bounds.height;
+ const ctx = canvas.getContext("2d");
+
+ ctx.fillStyle = "hotpink";
+ ctx.fillRect(0, 0, bounds.width, bounds.height);
+
+ for (const rect of rects) {
+ rect.left = Math.max(0, rect.left);
+ rect.right = Math.min(img.naturalWidth, rect.right);
+ rect.top = Math.max(0, rect.top);
+ rect.bottom = Math.min(img.naturalHeight, rect.bottom);
+
+ const width = rect.width;
+ const height = rect.height;
+
+ const screenX = rect.left;
+ const screenY = rect.top;
+
+ const imageX = screenX - bounds.left;
+ const imageY = screenY - bounds.top;
+ ctx.drawImage(
+ img,
+ screenX,
+ screenY,
+ width,
+ height,
+ imageX,
+ imageY,
+ width,
+ height
+ );
+ }
+
+ // Converts the canvas to a binary blob, which can be saved to a png
+ canvas.toBlob(blob => {
+ // Use a filereader to convert the raw binary blob into a writable buffer
+ const fr = new FileReader();
+ fr.onload = e => {
+ const buffer = new Uint8Array(e.target.result);
+ // Save the file and complete the promise
+ IOUtils.write(targetPath, buffer).then(resolve);
+ };
+ // Do the conversion
+ fr.readAsArrayBuffer(blob);
+ });
+ };
+
+ img.onerror = function () {
+ reject(`error loading image ${srcPath}`);
+ };
+ // Load the src image for drawing
+ img.src = srcPath;
+ });
+ return promise;
+ },
+
+ /**
+ * Finds the index of the first comma that is not enclosed within square brackets.
+ * @param {String} envVar - the string that needs to be searched
+ * @return {Integer} index of valid comma or -1 if not found.
+ */
+ findComma(envVar) {
+ let nestingDepth = 0;
+ for (let i = 0; i < envVar.length; i++) {
+ if (envVar[i] === "[") {
+ nestingDepth += 1;
+ } else if (envVar[i] === "]") {
+ nestingDepth -= 1;
+ } else if (envVar[i] === "," && nestingDepth === 0) {
+ return i;
+ }
+ }
+
+ return -1;
+ },
+
+ /**
+ * Splits the environment variable around commas not enclosed in brackets.
+ * @param {String} envVar - The environment variable
+ * @return {String[]} Array of strings containing the configurations
+ * e.g. ["Toolbars[onlyNavBar,allToolbars]","DevTools[jsdebugger,webconsole]","Tabs"]
+ */
+ splitEnv(envVar) {
+ let result = [];
+
+ let commaIndex = this.findComma(envVar);
+ while (commaIndex != -1) {
+ result.push(envVar.slice(0, commaIndex).trim());
+ envVar = envVar.slice(commaIndex + 1);
+ commaIndex = this.findComma(envVar);
+ }
+ result.push(envVar.trim());
+ return result;
+ },
+};
+
+/**
+ * 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;
+}
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/api.js b/browser/tools/mozscreenshots/mozscreenshots/extension/api.js
new file mode 100644
index 0000000000..a857567103
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/api.js
@@ -0,0 +1,26 @@
+/* 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";
+
+/* globals ExtensionAPI */
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "resProto",
+ "@mozilla.org/network/protocol;1?name=resource",
+ "nsISubstitutingProtocolHandler"
+);
+
+this.mozscreenshots = class extends ExtensionAPI {
+ async onStartup() {
+ let uri = Services.io.newURI("resources/", null, this.extension.rootURI);
+ resProto.setSubstitution("mozscreenshots", uri);
+
+ const { TestRunner } = ChromeUtils.importESModule(
+ "resource://mozscreenshots/TestRunner.sys.mjs"
+ );
+ TestRunner.init(this.extension.rootURI);
+ }
+};
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.sys.mjs
new file mode 100644
index 0000000000..8309eab623
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.sys.mjs
@@ -0,0 +1,77 @@
+/* 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/. */
+
+import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs";
+
+export var AppMenu = {
+ init(libDir) {},
+
+ configurations: {
+ appMenuMainView: {
+ selectors: ["#appMenu-popup"],
+ async applyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ await reopenAppMenu(browserWindow);
+ },
+ },
+
+ appMenuHistorySubview: {
+ selectors: ["#appMenu-popup"],
+ async applyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ await reopenAppMenu(browserWindow);
+
+ browserWindow.document.getElementById("appMenu-library-button").click();
+ let view = browserWindow.document.getElementById("appMenu-libraryView");
+ let promiseViewShown = BrowserTestUtils.waitForEvent(view, "ViewShown");
+ await promiseViewShown;
+ },
+
+ verifyConfig: verifyConfigHelper,
+ },
+
+ appMenuHelpSubview: {
+ selectors: ["#appMenu-popup"],
+ async applyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ await reopenAppMenu(browserWindow);
+
+ browserWindow.document.getElementById("appMenu-help-button2").click();
+ let view = browserWindow.document.getElementById("PanelUI-helpView");
+ let promiseViewShown = BrowserTestUtils.waitForEvent(view, "ViewShown");
+ await promiseViewShown;
+ },
+
+ verifyConfig: verifyConfigHelper,
+ },
+ },
+};
+
+async function reopenAppMenu(browserWindow) {
+ browserWindow.PanelUI.hide();
+ let promiseViewShown = BrowserTestUtils.waitForEvent(
+ browserWindow.PanelUI.panel,
+ "ViewShown"
+ );
+ browserWindow.PanelUI.show();
+ await promiseViewShown;
+}
+
+function verifyConfigHelper() {
+ if (isCustomizing()) {
+ return "navigator:browser has the customizing attribute";
+ }
+ return undefined;
+}
+
+function isCustomizing() {
+ let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ if (browserWindow.document.documentElement.hasAttribute("customizing")) {
+ return true;
+ }
+ return false;
+}
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.sys.mjs
new file mode 100644
index 0000000000..2bc12d9012
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.sys.mjs
@@ -0,0 +1,97 @@
+/* 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/. */
+
+import { CustomizableUI } from "resource:///modules/CustomizableUI.sys.mjs";
+
+export var Buttons = {
+ init(libDir) {
+ createWidget();
+ },
+
+ configurations: {
+ navBarButtons: {
+ selectors: ["#nav-bar"],
+ applyConfig: async () => {
+ CustomizableUI.addWidgetToArea(
+ "screenshot-widget",
+ CustomizableUI.AREA_NAVBAR
+ );
+ },
+ },
+
+ tabsToolbarButtons: {
+ selectors: ["#TabsToolbar"],
+ applyConfig: async () => {
+ CustomizableUI.addWidgetToArea(
+ "screenshot-widget",
+ CustomizableUI.AREA_TABSTRIP
+ );
+ },
+ },
+
+ menuPanelButtons: {
+ selectors: ["#widget-overflow"],
+ applyConfig: async () => {
+ CustomizableUI.addWidgetToArea(
+ "screenshot-widget",
+ CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
+ );
+ },
+
+ async verifyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ if (browserWindow.PanelUI.panel.state == "closed") {
+ return "The button isn't shown when the panel isn't open.";
+ }
+ return undefined;
+ },
+ },
+
+ custPaletteButtons: {
+ selectors: ["#customization-palette"],
+ applyConfig: async () => {
+ CustomizableUI.removeWidgetFromArea("screenshot-widget");
+ },
+
+ async verifyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ if (
+ browserWindow.document.documentElement.getAttribute("customizing") !=
+ "true"
+ ) {
+ return "The button isn't shown when we're not in customize mode.";
+ }
+ return undefined;
+ },
+ },
+ },
+};
+
+function createWidget() {
+ let id = "screenshot-widget";
+ let spec = {
+ 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/thumb-down.svg);" +
+ "}";
+ st.appendChild(browserWindow.document.createTextNode(styles));
+ browserWindow.document.documentElement.appendChild(st);
+}
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.sys.mjs
new file mode 100644
index 0000000000..2ada0e831c
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.sys.mjs
@@ -0,0 +1,320 @@
+/* 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/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs";
+import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+import { UrlClassifierTestUtils } from "resource://testing-common/UrlClassifierTestUtils.sys.mjs";
+
+import { SitePermissions } from "resource:///modules/SitePermissions.sys.mjs";
+
+const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+const CC_SELECTORS = ["#identity-popup", "#urlbar-input-container"];
+const PP_SELECTORS = ["#protections-popup", "#urlbar-input-container"];
+
+const RESOURCE_PATH =
+ "browser/browser/tools/mozscreenshots/mozscreenshots/extension/mozscreenshots/browser/resources/lib/controlCenter";
+const HTTP_PAGE = "http://example.com/";
+const HTTPS_PAGE = "https://example.com/";
+const PERMISSIONS_PAGE = "https://test1.example.com/";
+const HTTP_PASSWORD_PAGE = `http://test2.example.org/${RESOURCE_PATH}/password.html`;
+const MIXED_CONTENT_URL = `https://example.com/${RESOURCE_PATH}/mixed.html`;
+const MIXED_ACTIVE_CONTENT_URL = `https://example.com/${RESOURCE_PATH}/mixed_active.html`;
+const MIXED_PASSIVE_CONTENT_URL = `https://example.com/${RESOURCE_PATH}/mixed_passive.html`;
+const TRACKING_PAGE = `http://tracking.example.org/${RESOURCE_PATH}/tracking.html`;
+
+export var ControlCenter = {
+ init(libDir) {},
+
+ configurations: {
+ about: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ await loadPage("about:rights");
+ await openIdentityPopup();
+ },
+ },
+
+ localFile: {
+ // This selector is different so we can exclude the changing file: path
+ selectors: ["#identity-popup-security-button"],
+ async applyConfig() {
+ let channel = NetUtil.newChannel({
+ uri: "resource://mozscreenshots/lib/mozscreenshots.html",
+ loadUsingSystemPrincipal: true,
+ });
+ channel = channel.QueryInterface(Ci.nsIFileChannel);
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ let gBrowser = browserWindow.gBrowser;
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ channel.file.path
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await openIdentityPopup();
+ },
+ },
+
+ http: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ await loadPage(HTTP_PAGE);
+ await openIdentityPopup();
+ },
+ },
+
+ httpSubView: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ await loadPage(HTTP_PAGE);
+ await openIdentityPopup(true);
+ },
+ },
+
+ https: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ await loadPage(HTTPS_PAGE);
+ await openIdentityPopup();
+ },
+ },
+
+ httpsSubView: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ await loadPage(HTTPS_PAGE);
+ await openIdentityPopup(true);
+ },
+ },
+
+ singlePermission: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ PERMISSIONS_PAGE
+ );
+ SitePermissions.setForPrincipal(
+ principal,
+ "camera",
+ SitePermissions.ALLOW
+ );
+
+ await loadPage(PERMISSIONS_PAGE);
+ await openIdentityPopup();
+ },
+ },
+
+ allPermissions: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ // TODO: (Bug 1330601) Rewrite this to consider temporary (TAB) permission states.
+ // There are 2 possible non-default permission states, so we alternate between them.
+ let states = [SitePermissions.ALLOW, SitePermissions.BLOCK];
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ PERMISSIONS_PAGE
+ );
+ SitePermissions.listPermissions().forEach(function (permission, index) {
+ SitePermissions.setForPrincipal(
+ principal,
+ permission,
+ states[index % 2]
+ );
+ });
+
+ await loadPage(PERMISSIONS_PAGE);
+ await openIdentityPopup();
+ },
+ },
+
+ mixed: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ await loadPage(MIXED_CONTENT_URL);
+ await openIdentityPopup();
+ },
+ },
+
+ mixedSubView: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ await loadPage(MIXED_CONTENT_URL);
+ await openIdentityPopup(true);
+ },
+ },
+
+ mixedPassive: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ await loadPage(MIXED_PASSIVE_CONTENT_URL);
+ await openIdentityPopup();
+ },
+ },
+
+ mixedPassiveSubView: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ await loadPage(MIXED_PASSIVE_CONTENT_URL);
+ await openIdentityPopup(true);
+ },
+ },
+
+ mixedActive: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ await loadPage(MIXED_ACTIVE_CONTENT_URL);
+ await openIdentityPopup();
+ },
+ },
+
+ mixedActiveSubView: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ await loadPage(MIXED_ACTIVE_CONTENT_URL);
+ await openIdentityPopup(true);
+ },
+ },
+
+ mixedActiveUnblocked: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ let gBrowser = browserWindow.gBrowser;
+ await loadPage(MIXED_ACTIVE_CONTENT_URL);
+ gBrowser.ownerGlobal.gIdentityHandler.disableMixedContentProtection();
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ MIXED_ACTIVE_CONTENT_URL
+ );
+ await openIdentityPopup();
+ },
+ },
+
+ mixedActiveUnblockedSubView: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ let gBrowser = browserWindow.gBrowser;
+ await loadPage(MIXED_ACTIVE_CONTENT_URL);
+ gBrowser.ownerGlobal.gIdentityHandler.disableMixedContentProtection();
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ MIXED_ACTIVE_CONTENT_URL
+ );
+ await openIdentityPopup(true);
+ },
+ },
+
+ httpPassword: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ await loadPage(HTTP_PASSWORD_PAGE);
+ await openIdentityPopup();
+ },
+ },
+
+ httpPasswordSubView: {
+ selectors: CC_SELECTORS,
+ async applyConfig() {
+ await loadPage(HTTP_PASSWORD_PAGE);
+ await openIdentityPopup(true);
+ },
+ },
+
+ trackingProtectionNoElements: {
+ selectors: PP_SELECTORS,
+ async applyConfig() {
+ Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
+
+ await loadPage(HTTP_PAGE);
+ await openProtectionsPopup();
+ },
+ },
+
+ trackingProtectionEnabled: {
+ selectors: PP_SELECTORS,
+ async applyConfig() {
+ Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ await loadPage(TRACKING_PAGE);
+ await openProtectionsPopup();
+ },
+ },
+
+ trackingProtectionDisabled: {
+ selectors: PP_SELECTORS,
+ async applyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ let gBrowser = browserWindow.gBrowser;
+ Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ await loadPage(TRACKING_PAGE);
+
+ // unblock the page
+ let loaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ TRACKING_PAGE
+ );
+ gBrowser.ownerGlobal.gProtectionsHandler.disableForCurrentPage();
+ await loaded;
+ await openProtectionsPopup();
+ },
+ },
+ },
+};
+
+async function loadPage(url) {
+ let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ let gBrowser = browserWindow.gBrowser;
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, url);
+}
+
+async function openIdentityPopup(expand) {
+ let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ let gBrowser = browserWindow.gBrowser;
+ let { gIdentityHandler } = gBrowser.ownerGlobal;
+ // Ensure the popup is available, if it's never been opened.
+ gIdentityHandler._initializePopup();
+ gIdentityHandler._identityPopup.hidePopup();
+ // Disable the popup shadow on OSX until we have figured out bug 1425253.
+ if (AppConstants.platform == "macosx") {
+ gIdentityHandler._identityPopup.classList.add("no-shadow");
+ }
+ gIdentityHandler._identityIconBox.click();
+ if (expand) {
+ // give some time for opening to avoid weird style issues
+ await new Promise(c => setTimeout(c, 500));
+ gIdentityHandler._identityPopup
+ .querySelector("#identity-popup-security-button")
+ .click();
+ }
+}
+
+async function openProtectionsPopup() {
+ let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ let gBrowser = browserWindow.gBrowser;
+ let { gProtectionsHandler } = gBrowser.ownerGlobal;
+ // Force initializing the popup; we can't add classes otherwise.
+ gProtectionsHandler._initializePopup();
+ gProtectionsHandler._protectionsPopup.hidePopup();
+ // Disable the popup shadow on OSX until we have figured out bug 1425253.
+ if (AppConstants.platform == "macosx") {
+ gProtectionsHandler._protectionsPopup.classList.add("no-shadow");
+ }
+ gProtectionsHandler.showProtectionsPopup();
+ // Wait for any animation.
+ await new Promise(_ => setTimeout(_, 500));
+}
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.sys.mjs
new file mode 100644
index 0000000000..a5a1b65b69
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.sys.mjs
@@ -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/. */
+
+import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+export var CustomizeMode = {
+ init(libDir) {},
+
+ configurations: {
+ notCustomizing: {
+ selectors: ["#navigator-toolbox"],
+ 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: {
+ selectors: ["#navigator-toolbox", "#customization-container"],
+ 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();
+ });
+ },
+ },
+ },
+};
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.sys.mjs
new file mode 100644
index 0000000000..addef011c0
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.sys.mjs
@@ -0,0 +1,74 @@
+/* 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/. */
+
+import { require } from "resource://devtools/shared/loader/Loader.sys.mjs";
+
+const { gDevTools } = require("devtools/client/framework/devtools");
+
+import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+async function showToolboxForSelectedTab(toolId, hostType) {
+ const browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ const tab = browserWindow.gBrowser.selectedTab;
+ return gDevTools.showToolboxForTab(tab, { toolId, hostType });
+}
+
+function selectToolbox(toolbox) {
+ return toolbox.win.document.querySelector("#toolbox-container");
+}
+
+export var DevTools = {
+ init(libDir) {
+ let panels = [
+ "options",
+ "webconsole",
+ "jsdebugger",
+ "styleeditor",
+ "performance",
+ "netmonitor",
+ ];
+
+ panels.forEach(panel => {
+ this.configurations[panel] = {};
+ this.configurations[panel].applyConfig = async function () {
+ Services.prefs.setIntPref("devtools.toolbox.footer.height", 800);
+ let toolbox = await showToolboxForSelectedTab(panel, "bottom");
+ this.selectors = [selectToolbox.bind(null, toolbox)];
+ await new Promise(resolve => setTimeout(resolve, 500));
+ };
+ });
+ },
+
+ configurations: {
+ bottomToolbox: {
+ async applyConfig() {
+ Services.prefs.clearUserPref("devtools.toolbox.footer.height");
+ let toolbox = await showToolboxForSelectedTab("inspector", "bottom");
+ this.selectors = [selectToolbox.bind(null, toolbox)];
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ },
+ },
+ sideToolbox: {
+ async applyConfig() {
+ let toolbox = await showToolboxForSelectedTab("inspector", "right");
+ this.selectors = [selectToolbox.bind(null, toolbox)];
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ },
+ async verifyConfig() {
+ return "Panel sizes are regularly inconsistent";
+ },
+ },
+ undockedToolbox: {
+ windowType: "devtools:toolbox",
+ async applyConfig() {
+ let toolbox = await showToolboxForSelectedTab("inspector", "window");
+ this.selectors = [selectToolbox.bind(null, toolbox)];
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ },
+ async verifyConfig() {
+ return "Panel sizes are regularly inconsistent";
+ },
+ },
+ },
+};
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.sys.mjs
new file mode 100644
index 0000000000..91c3349ec7
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.sys.mjs
@@ -0,0 +1,51 @@
+/* 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/. */
+
+import { AddonManager } from "resource://gre/modules/AddonManager.sys.mjs";
+
+export var LightweightThemes = {
+ init(libDir) {},
+
+ configurations: {
+ noLWT: {
+ selectors: [],
+ async applyConfig() {
+ let addon = await AddonManager.getAddonByID(
+ "default-theme@mozilla.org"
+ );
+ await addon.enable();
+ },
+ },
+
+ compactLight: {
+ selectors: [],
+ async applyConfig() {
+ let addon = await AddonManager.getAddonByID(
+ "firefox-compact-light@mozilla.org"
+ );
+ await addon.enable();
+ },
+ },
+
+ compactDark: {
+ selectors: [],
+ async applyConfig() {
+ let addon = await AddonManager.getAddonByID(
+ "firefox-compact-dark@mozilla.org"
+ );
+ await addon.enable();
+ },
+ },
+
+ alpenGlow: {
+ selectors: [],
+ async applyConfig() {
+ let addon = await AddonManager.getAddonByID(
+ "firefox-alpenglow@mozilla.org"
+ );
+ await addon.enable();
+ },
+ },
+ },
+};
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.sys.mjs
new file mode 100644
index 0000000000..de12c69e81
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.sys.mjs
@@ -0,0 +1,168 @@
+/* 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/. */
+
+// Various parts here are run in the content process.
+/* global content */
+
+import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs";
+
+const URL =
+ "https://test1.example.com/browser/browser/tools/mozscreenshots/mozscreenshots/extension/mozscreenshots/browser/resources/lib/permissionPrompts.html";
+let lastTab = null;
+
+export var PermissionPrompts = {
+ init(libDir) {
+ Services.prefs.setBoolPref("media.navigator.permission.fake", true);
+ Services.prefs.setBoolPref("extensions.install.requireBuiltInCerts", false);
+ Services.prefs.setBoolPref("signon.rememberSignons", true);
+ },
+
+ configurations: {
+ shareDevices: {
+ selectors: ["#notification-popup", "#identity-box"],
+ async applyConfig() {
+ await closeLastTab();
+ await clickOn("#webRTC-shareDevices");
+ },
+ },
+
+ shareMicrophone: {
+ selectors: ["#notification-popup", "#identity-box"],
+ async applyConfig() {
+ await closeLastTab();
+ await clickOn("#webRTC-shareMicrophone");
+ },
+ },
+
+ shareVideoAndMicrophone: {
+ selectors: ["#notification-popup", "#identity-box"],
+ async applyConfig() {
+ await closeLastTab();
+ await clickOn("#webRTC-shareDevices2");
+ },
+ },
+
+ shareScreen: {
+ selectors: ["#notification-popup", "#identity-box"],
+ async applyConfig() {
+ await closeLastTab();
+ await clickOn("#webRTC-shareScreen");
+ },
+ },
+
+ geo: {
+ selectors: ["#notification-popup", "#identity-box"],
+ async applyConfig() {
+ await closeLastTab();
+ await clickOn("#geo");
+ },
+ },
+
+ persistentStorage: {
+ selectors: ["#notification-popup", "#identity-box"],
+ async applyConfig() {
+ await closeLastTab();
+ await clickOn("#persistent-storage");
+ },
+ },
+
+ loginCapture: {
+ selectors: ["#notification-popup", "#identity-box"],
+ async applyConfig() {
+ await closeLastTab();
+ // we need to emulate user input in the form for the save-password prompt to be shown
+ await clickOn("#login-capture", function beforeContentFn() {
+ const { E10SUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/E10SUtils.sys.mjs"
+ );
+ E10SUtils.wrapHandlingUserInput(content, true, function () {
+ let element = content.document.querySelector(
+ "input[type=password]"
+ );
+ element.setUserInput("123456");
+ });
+ });
+ },
+ },
+
+ notifications: {
+ selectors: ["#notification-popup", "#identity-box"],
+ async applyConfig() {
+ await closeLastTab();
+ await clickOn("#web-notifications");
+ },
+ },
+
+ addons: {
+ selectors: ["#notification-popup", "#identity-box"],
+ async applyConfig() {
+ Services.prefs.setBoolPref("xpinstall.whitelist.required", true);
+
+ await closeLastTab();
+ await clickOn("#addons");
+ },
+ },
+
+ addonsNoWhitelist: {
+ selectors: ["#notification-popup", "#identity-box"],
+ async applyConfig() {
+ Services.prefs.setBoolPref("xpinstall.whitelist.required", false);
+
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ let notification = browserWindow.document.getElementById(
+ "addon-webext-permissions-notification"
+ );
+
+ await closeLastTab();
+ await clickOn("#addons");
+
+ // We want to skip the progress-notification, so we wait for
+ // the install-confirmation screen to be "not hidden" = shown.
+ return BrowserTestUtils.waitForCondition(
+ () => !notification.hidden,
+ "addon install confirmation did not show",
+ 200
+ ).catch(msg => {
+ return Promise.resolve({ todo: msg });
+ });
+ },
+ },
+ },
+};
+
+async function closeLastTab() {
+ if (!lastTab) {
+ return;
+ }
+ BrowserTestUtils.removeTab(lastTab);
+ lastTab = null;
+}
+
+async function clickOn(selector, beforeContentFn) {
+ let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+
+ // Save the tab so we can close it later.
+ lastTab = await BrowserTestUtils.openNewForegroundTab(
+ browserWindow.gBrowser,
+ URL
+ );
+
+ let { SpecialPowers } = lastTab.ownerGlobal;
+ if (beforeContentFn) {
+ await SpecialPowers.spawn(lastTab.linkedBrowser, [], beforeContentFn);
+ }
+
+ await SpecialPowers.spawn(lastTab.linkedBrowser, [selector], arg => {
+ /* eslint-env mozilla/chrome-script */
+ let element = content.document.querySelector(arg);
+ return EventUtils.synthesizeClick(element);
+ });
+
+ // Wait for the popup to actually be shown before making the screenshot
+ await BrowserTestUtils.waitForEvent(
+ browserWindow.PopupNotifications.panel,
+ "popupshown"
+ );
+}
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.sys.mjs
new file mode 100644
index 0000000000..77026523ba
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.sys.mjs
@@ -0,0 +1,183 @@
+/* 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/. */
+
+// Various parts here are run in the content process.
+/* global content */
+
+import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs";
+
+export var Preferences = {
+ init(libDir) {
+ let panes = [
+ ["paneGeneral"],
+ ["paneGeneral", browsingGroup],
+ ["paneGeneral", connectionDialog],
+ ["paneSearch"],
+ ["panePrivacy"],
+ ["panePrivacy", cacheGroup],
+ ["panePrivacy", clearRecentHistoryDialog],
+ ["panePrivacy", certManager],
+ ["panePrivacy", deviceManager],
+ ["panePrivacy", DNTDialog],
+ ["paneSync"],
+ ];
+
+ for (let [primary, customFn] of panes) {
+ let configName = primary.replace(/^pane/, "prefs");
+ if (customFn) {
+ configName += "-" + customFn.name;
+ }
+ this.configurations[configName] = {};
+ this.configurations[configName].selectors = ["#browser"];
+ if (primary == "panePrivacy" && customFn) {
+ this.configurations[configName].applyConfig = async () => {
+ return { todo: `${configName} times out on the try server` };
+ };
+ } else {
+ this.configurations[configName].applyConfig = prefHelper.bind(
+ null,
+ primary,
+ customFn
+ );
+ }
+ }
+ },
+
+ configurations: {},
+};
+
+let prefHelper = async function (primary, customFn = null) {
+ let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ let selectedBrowser = browserWindow.gBrowser.selectedBrowser;
+
+ // close any dialog that might still be open
+ await selectedBrowser.ownerGlobal.SpecialPowers.spawn(
+ selectedBrowser,
+ [],
+ async function () {
+ // Check that gSubDialog is defined on the content window
+ // and that there is an open dialog to close
+ if (!content.window.gSubDialog || !content.window.gSubDialog._topDialog) {
+ return;
+ }
+ content.window.gSubDialog.close();
+ }
+ );
+
+ let readyPromise = null;
+ if (selectedBrowser.currentURI.specIgnoringRef == "about:preferences") {
+ if (
+ selectedBrowser.currentURI.spec ==
+ "about:preferences#" + primary.replace(/^pane/, "")
+ ) {
+ // We're already on the correct pane.
+ readyPromise = Promise.resolve();
+ } else {
+ readyPromise = paintPromise(browserWindow);
+ }
+ } else {
+ readyPromise = TestUtils.topicObserved("sync-pane-loaded");
+ }
+
+ browserWindow.openPreferences(primary);
+
+ await readyPromise;
+
+ if (customFn) {
+ let customPaintPromise = paintPromise(browserWindow);
+ let result = await customFn(selectedBrowser);
+ await customPaintPromise;
+ return result;
+ }
+ return undefined;
+};
+
+function paintPromise(browserWindow) {
+ return new Promise(resolve => {
+ browserWindow.addEventListener(
+ "MozAfterPaint",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+async function browsingGroup(aBrowser) {
+ await aBrowser.ownerGlobal.SpecialPowers.spawn(
+ aBrowser,
+ [],
+ async function () {
+ content.document.getElementById("browsingGroup").scrollIntoView();
+ }
+ );
+}
+
+async function cacheGroup(aBrowser) {
+ await aBrowser.ownerGlobal.SpecialPowers.spawn(
+ aBrowser,
+ [],
+ async function () {
+ content.document.getElementById("cacheGroup").scrollIntoView();
+ }
+ );
+}
+
+async function DNTDialog(aBrowser) {
+ return aBrowser.ownerGlobal.SpecialPowers.spawn(
+ aBrowser,
+ [],
+ async function () {
+ const button = content.document.getElementById("doNotTrackSettings");
+ if (!button) {
+ return {
+ todo: "The dialog may have exited before we could click the button",
+ };
+ }
+ button.click();
+ return undefined;
+ }
+ );
+}
+
+async function connectionDialog(aBrowser) {
+ await aBrowser.ownerGlobal.SpecialPowers.spawn(
+ aBrowser,
+ [],
+ async function () {
+ content.document.getElementById("connectionSettings").click();
+ }
+ );
+}
+
+async function clearRecentHistoryDialog(aBrowser) {
+ await aBrowser.ownerGlobal.SpecialPowers.spawn(
+ aBrowser,
+ [],
+ async function () {
+ content.document.getElementById("clearHistoryButton").click();
+ }
+ );
+}
+
+async function certManager(aBrowser) {
+ await aBrowser.ownerGlobal.SpecialPowers.spawn(
+ aBrowser,
+ [],
+ async function () {
+ content.document.getElementById("viewCertificatesButton").click();
+ }
+ );
+}
+
+async function deviceManager(aBrowser) {
+ await aBrowser.ownerGlobal.SpecialPowers.spawn(
+ aBrowser,
+ [],
+ async function () {
+ content.document.getElementById("viewSecurityDevicesButton").click();
+ }
+ );
+}
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.sys.mjs
new file mode 100644
index 0000000000..85b134b8bc
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.sys.mjs
@@ -0,0 +1,219 @@
+/* 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/. */
+
+const CUST_TAB = "chrome://browser/skin/customize.svg";
+const PREFS_TAB = "chrome://global/skin/icons/settings.svg";
+const DEFAULT_FAVICON_TAB = `data:text/html,<meta%20charset="utf-8"><title>No%20favicon</title>`;
+
+import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs";
+
+export var Tabs = {
+ init(libDir) {},
+
+ configurations: {
+ fiveTabs: {
+ selectors: ["#tabbrowser-tabs"],
+ async applyConfig() {
+ fiveTabsHelper();
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ hoverTab(browserWindow.gBrowser.tabs[3]);
+ await new Promise((resolve, reject) => {
+ setTimeout(resolve, 3000);
+ });
+ await allTabTitlesDisplayed(browserWindow);
+ },
+ },
+
+ fourPinned: {
+ selectors: ["#tabbrowser-tabs"],
+ async applyConfig() {
+ fiveTabsHelper();
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ let tab = browserWindow.gBrowser.addTab(PREFS_TAB, {
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ browserWindow.gBrowser.pinTab(tab);
+ tab = browserWindow.gBrowser.addTab(CUST_TAB, {
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ browserWindow.gBrowser.pinTab(tab);
+ tab = browserWindow.gBrowser.addTab("about:privatebrowsing", {
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ browserWindow.gBrowser.pinTab(tab);
+ tab = browserWindow.gBrowser.addTab("about:home", {
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ browserWindow.gBrowser.pinTab(tab);
+ browserWindow.gBrowser.selectTabAtIndex(5);
+ hoverTab(browserWindow.gBrowser.tabs[2]);
+ // also hover the new tab button
+ let newTabButton = browserWindow.gBrowser.tabContainer.newTabButton;
+ hoverTab(newTabButton);
+
+ await new Promise((resolve, reject) => {
+ setTimeout(resolve, 3000);
+ });
+ await allTabTitlesDisplayed(browserWindow);
+ },
+ },
+
+ twoPinnedWithOverflow: {
+ selectors: ["#tabbrowser-tabs"],
+ async applyConfig() {
+ fiveTabsHelper();
+
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ browserWindow.gBrowser.loadTabs(
+ [
+ PREFS_TAB,
+ CUST_TAB,
+ "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",
+ ],
+ {
+ inBackground: true,
+ replace: true,
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ }
+ );
+ browserWindow.gBrowser.pinTab(browserWindow.gBrowser.tabs[1]);
+ browserWindow.gBrowser.pinTab(browserWindow.gBrowser.tabs[2]);
+ browserWindow.gBrowser.selectTabAtIndex(3);
+ hoverTab(browserWindow.gBrowser.tabs[5]);
+
+ await new Promise((resolve, reject) => {
+ setTimeout(resolve, 3000);
+ });
+
+ // Make sure the tabstrip is scrolled all the way to the left.
+ browserWindow.gBrowser.tabContainer.arrowScrollbox.scrollByIndex(
+ -100,
+ true
+ );
+
+ await allTabTitlesDisplayed(browserWindow);
+ },
+ },
+ },
+};
+
+/* helpers */
+
+async function allTabTitlesDisplayed(browserWindow) {
+ let specToTitleMap = {
+ "about:home": "New Tab",
+ "about:newtab": "New Tab",
+ "about:addons": "Add-ons Manager",
+ "about:privatebrowsing": "about:privatebrowsing",
+ };
+ specToTitleMap[PREFS_TAB] = "global/skin/icons/settings.svg";
+ specToTitleMap[CUST_TAB] = "browser/skin/customize.svg";
+ specToTitleMap[DEFAULT_FAVICON_TAB] = "No favicon";
+
+ let tabTitlePromises = [];
+ for (let tab of browserWindow.gBrowser.tabs) {
+ function getSpec() {
+ return (
+ tab.linkedBrowser &&
+ tab.linkedBrowser.documentURI &&
+ tab.linkedBrowser.documentURI.spec
+ );
+ }
+ function tabTitleLoaded() {
+ let spec = getSpec();
+ return spec ? tab.label == specToTitleMap[spec] : false;
+ }
+ let promise = TestUtils.waitForCondition(
+ tabTitleLoaded,
+ `Tab (${getSpec()}) should be showing "${
+ specToTitleMap[getSpec()]
+ }". Got "${tab.label}"`
+ );
+ tabTitlePromises.push(promise);
+ }
+
+ return Promise.all(tabTitlePromises);
+}
+
+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,
+ ],
+ {
+ inBackground: true,
+ replace: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ }
+ );
+ 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(Services.io.newURI(url), {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ if (gBrowser.selectedTab.pinned) {
+ gBrowser.unpinTab(gBrowser.selectedTab);
+ }
+ let newTabButton = gBrowser.tabContainer.newTabButton;
+ hoverTab(newTabButton, false);
+}
+
+function hoverTab(tab, hover = true) {
+ if (hover) {
+ InspectorUtils.addPseudoClassLock(tab, ":hover");
+ } else {
+ InspectorUtils.clearPseudoClassLocks(tab);
+ }
+}
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.sys.mjs
new file mode 100644
index 0000000000..bc5b12c219
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.sys.mjs
@@ -0,0 +1,28 @@
+/* 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/. */
+
+const PREF_TABS_IN_TITLEBAR = "browser.tabs.inTitlebar";
+
+export var TabsInTitlebar = {
+ init(libDir) {},
+
+ configurations: {
+ tabsInTitlebar: {
+ selectors: ["#navigator-toolbox"],
+ async applyConfig() {
+ Services.prefs.setIntPref(PREF_TABS_IN_TITLEBAR, 1);
+ return undefined;
+ },
+ },
+
+ tabsOutsideTitlebar: {
+ selectors: ["#navigator-toolbox"].concat(
+ Services.appinfo.OS == "Linux" ? [] : ["#titlebar"]
+ ),
+ async applyConfig() {
+ Services.prefs.setIntPref(PREF_TABS_IN_TITLEBAR, 0);
+ },
+ },
+ },
+};
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.sys.mjs
new file mode 100644
index 0000000000..06b1159a5e
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.sys.mjs
@@ -0,0 +1,54 @@
+/* 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/. */
+
+export var Toolbars = {
+ init(libDir) {},
+
+ configurations: {
+ onlyNavBar: {
+ selectors: ["#navigator-toolbox"],
+ async applyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ let personalToolbar =
+ browserWindow.document.getElementById("PersonalToolbar");
+ browserWindow.setToolbarVisibility(personalToolbar, false);
+ toggleMenubarIfNecessary(false);
+ },
+ },
+
+ allToolbars: {
+ selectors: ["#navigator-toolbox"],
+ async applyConfig() {
+ // Boookmarks and menubar
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ let personalToolbar =
+ browserWindow.document.getElementById("PersonalToolbar");
+ browserWindow.setToolbarVisibility(personalToolbar, true);
+ toggleMenubarIfNecessary(true);
+ },
+
+ async verifyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ if (browserWindow.fullScreen) {
+ return "The bookmark toolbar and menubar are not shown in fullscreen.";
+ }
+ return undefined;
+ },
+ },
+ },
+};
+
+// 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);
+ }
+}
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/UIDensities.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/UIDensities.sys.mjs
new file mode 100644
index 0000000000..37cc123727
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/UIDensities.sys.mjs
@@ -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/. */
+
+export var UIDensities = {
+ init(libDir) {},
+
+ configurations: {
+ compactDensity: {
+ selectors: ["#navigator-toolbox", "#appMenu-popup", "#widget-overflow"],
+ async applyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ browserWindow.gCustomizeMode.setUIDensity(
+ browserWindow.gUIDensity.MODE_COMPACT
+ );
+ },
+ },
+
+ normalDensity: {
+ selectors: ["#navigator-toolbox", "#appMenu-popup", "#widget-overflow"],
+ async applyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ browserWindow.gCustomizeMode.setUIDensity(
+ browserWindow.gUIDensity.MODE_NORMAL
+ );
+ },
+ },
+
+ touchDensity: {
+ selectors: ["#navigator-toolbox", "#appMenu-popup", "#widget-overflow"],
+ async applyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ browserWindow.gCustomizeMode.setUIDensity(
+ browserWindow.gUIDensity.MODE_TOUCH
+ );
+ },
+ },
+ },
+};
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.sys.mjs
new file mode 100644
index 0000000000..98a4e3ec00
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.sys.mjs
@@ -0,0 +1,68 @@
+/* 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/. */
+
+import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs";
+
+export var WindowSize = {
+ init(libDir) {
+ Services.prefs.setBoolPref("browser.fullscreen.autohide", false);
+ },
+
+ configurations: {
+ maximized: {
+ selectors: [":root"],
+ async applyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ await toggleFullScreen(browserWindow, 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.
+ await new Promise((resolve, reject) => {
+ setTimeout(function waitToLeaveFS() {
+ browserWindow.maximize();
+ resolve();
+ }, 5000);
+ });
+ },
+ },
+
+ normal: {
+ selectors: [":root"],
+ async applyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ await toggleFullScreen(browserWindow, false);
+ browserWindow.restore();
+ await new Promise((resolve, reject) => {
+ setTimeout(resolve, 5000);
+ });
+ },
+ },
+
+ fullScreen: {
+ selectors: [":root"],
+ async applyConfig() {
+ let browserWindow =
+ Services.wm.getMostRecentWindow("navigator:browser");
+ await toggleFullScreen(browserWindow, true);
+ // OS X Lion fullscreen transition takes a while
+ await new Promise((resolve, reject) => {
+ setTimeout(resolve, 5000);
+ });
+ },
+ },
+ },
+};
+
+function toggleFullScreen(browserWindow, wantsFS) {
+ browserWindow.fullScreen = wantsFS;
+ return BrowserTestUtils.waitForCondition(() => {
+ return (
+ wantsFS ==
+ browserWindow.document.documentElement.hasAttribute("inFullscreen")
+ );
+ }, "waiting for @inFullscreen change");
+}
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/borderify.xpi b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/borderify.xpi
new file mode 100644
index 0000000000..66ae92ed21
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/borderify.xpi
Binary files differ
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed.html b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed.html
new file mode 100644
index 0000000000..d71d9946d3
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed.html
@@ -0,0 +1,15 @@
+<!-- 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/. -->
+
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Mixed Content test</title>
+ </head>
+ <body>
+ <iframe style="visibility:hidden" src="http://example.com"></iframe>
+ <img style="visibility:hidden" src="http://example.com/tests/image/test/mochitest/blue.png"></img>
+ </body>
+</html>
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed_active.html b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed_active.html
new file mode 100644
index 0000000000..4ce8e78dc4
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed_active.html
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf8" />
+ <title>Mixed Active Content test</title>
+ </head>
+ <body>
+ <iframe style="visibility: hidden" src="http://example.com"></iframe>
+ </body>
+</html>
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed_passive.html b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed_passive.html
new file mode 100644
index 0000000000..a8b74e08b2
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed_passive.html
@@ -0,0 +1,14 @@
+<!-- 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/. -->
+
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>Mixed Passive Content test</title>
+ </head>
+ <body>
+ <img style="visibility:hidden" src="http://example.com/tests/image/test/mochitest/blue.png"></img>
+ </body>
+</html>
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/password.html b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/password.html
new file mode 100644
index 0000000000..c51ee25732
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/password.html
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf8" />
+ <title>HTTP Password test</title>
+ </head>
+ <body>
+ <form>
+ <input type="password" />
+ <button type="submit">Submit</button>
+ </form>
+ </body>
+</html>
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/tracking.html b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/tracking.html
new file mode 100644
index 0000000000..8ef07c3e73
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/tracking.html
@@ -0,0 +1,17 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf8" />
+ <title>Tracking test</title>
+ </head>
+ <body>
+ <iframe
+ style="visibility: hidden"
+ src="http://tracking.example.com/"
+ ></iframe>
+ </body>
+</html>
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-script.js b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-script.js
new file mode 100644
index 0000000000..808c08c8d3
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-script.js
@@ -0,0 +1,21 @@
+/* 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/. */
+
+console.log(document, document.body);
+console.assert(false, "Failing mozscreenshots assertion");
+
+console.group("Grouped Message");
+console.log("group message 1");
+console.groupEnd();
+
+console.count("counter");
+console.count("counter");
+
+console.log("first", { a: 1 }, "second", { b: "hello" }, "third", {
+ c: new Map(),
+});
+console.log("first", { a: 1 }, "second", { b: "hello" });
+console.log("first", { a: 1 }, "\nsecond", { b: "hello" });
+console.log("first", "\nsecond");
+console.log("\nfirst", "second");
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-style.css b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-style.css
new file mode 100644
index 0000000000..145a4e57d4
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-style.css
@@ -0,0 +1,28 @@
+/* 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/. */
+
+body {
+ --background-color: #f3f3f3;
+ background: tomato;
+}
+
+body {
+ background: var(--background-color);
+ color: #222;
+ padding: 0 10px;
+ margin: 0;
+}
+
+header {
+ background: #eee;
+ border-bottom: solid 2px #ccc;
+ margin: 0 -10px;
+ margin-bottom: 5px;
+ padding: 4px;
+}
+
+header h1 {
+ margin: 0;
+ padding: 0;
+}
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots.html b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots.html
new file mode 100644
index 0000000000..494bdba159
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<!-- 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/. -->
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>mozscreenshots</title>
+ <link rel="stylesheet" href="mozscreenshots-style.css" />
+ <script>
+ console.info("This page was generated by mozscreenshots");
+ </script>
+ </head>
+
+ <body>
+ <header><h1>mozscreenshots</h1></header>
+
+ <p>This page was generated by mozscreenshots</p>
+
+ <img src="robot.png" />
+
+ <p>
+ Welcome Humans! We invite others to keep the Manifesto’s principles; use
+ the creation delivery and commitment. Advancing the Manifesto. We have
+ shiny metal posteriors which <strong>should not be bitten</strong>. And
+ they have distilled a public benefit is committed to use Mozilla project
+ one basic and communities of modern life.
+ </p>
+
+ <p>
+ Robots have
+ <mark>distilled a balance between commercial aspects of life</mark>.
+ </p>
+
+ <p>
+ Robots may not they have shown in education communication collaboration
+ business entertainment and other people to pursue; speak to continue; and
+ opportunity are many different ways to benefit the public benefit the
+ Manifesto’s principles; build and motivate us and trademarks
+ infrastructure funds and trademarks infrastructure funds and enable
+ open-source technologies and provide a whole.
+ </p>
+
+ <p>
+ And they have seen things you people who believe that Mozilla Manifesto.
+ We are to: articulate a vision of individual human being to benefit the
+ lives of these efforts we will: build and promote models for creating
+ economic value for the Internet. We create world-class open and anticipate
+ the Mozilla Manifesto
+ <strong>There are Your Plastic Pal Who's Fun To Be With</strong>
+ </p>
+
+ <p>
+ Some Foundation to advance this vision of individual human being or not
+ deeply involved in groups and promote models for the Manifesto principles
+ will not come to support the Mozilla Foundation Pledge The Mozilla
+ Manifesto in its activities. People are to: articulate a set of consumer
+ products that support <mark>the Internet is a human being</mark> or not be
+ treated as individuals working together in the development of the Internet
+ open and with goodwill!Specifically we believe that we will: build and
+ society as a public good as a result of the lives of collaborative
+ activities. Specifically we have seen things you people acting as
+ optional. Individuals must not come to continue to develop new ways of the
+ Internet are fundamental and with us to ensure that openness innovation
+ and very effective way that the Manifesto There are many benefits; a
+ global public benefit; and society as optional.
+ </p>
+
+ <p>
+ <strong
+ >We have metal posteriors which should not deeply involved in a
+ reality.</strong
+ >
+ Individuals must remain open source software promotes the Internet is a
+ balance between commercial profit and within the Mozilla Corporation.
+ Invitation The Internet are key to join us to life on their own. The
+ Internet a whole. The Internet as a vision of the Mozilla contributors
+ proud of time attention and motivate us and provide a reality.
+ </p>
+
+ <script src="mozscreenshots-script.js"></script>
+ </body>
+</html>
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/permissionPrompts.html b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/permissionPrompts.html
new file mode 100644
index 0000000000..92ec9805b5
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/permissionPrompts.html
@@ -0,0 +1,64 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Permission Prompts</title>
+ </head>
+ <body>
+ <button
+ id="geo"
+ onclick="navigator.geolocation.getCurrentPosition(() => {})"
+ >
+ Geolocation
+ </button>
+ <button id="xr" onclick="content.navigator.getVRDisplays();">WebXR</button>
+ <button id="persistent-storage" onclick="navigator.storage.persist()">
+ Persistent Storage
+ </button>
+ <button
+ id="webRTC-shareDevices"
+ onclick="shareDevice({video: true, fake: true});"
+ >
+ Video
+ </button>
+ <button
+ id="webRTC-shareMicrophone"
+ onclick="shareDevice({audio: true, fake: true});"
+ >
+ Audio
+ </button>
+ <button
+ id="webRTC-shareDevices2"
+ onclick="shareDevice({audio: true, video: true, fake: true});"
+ >
+ Audio and Video
+ </button>
+ <button
+ id="webRTC-shareScreen"
+ onclick="shareDevice({video: {mediaSource: 'screen'}});"
+ >
+ Screen
+ </button>
+ <button id="web-notifications" onclick="Notification.requestPermission()">
+ web-notifications
+ </button>
+ <a id="addons" href="borderify.xpi">Install Add-On</a>
+ <form>
+ <input type="email" id="email" value="email@example.com" />
+ <input type="password" id="password" value="" />
+ <button type="submit" id="login-capture">Login</button>
+ </form>
+
+ <script>
+ // Share device used in onclick calls above.
+ /* exported shareDevice */
+ function shareDevice(config) {
+ navigator.mediaDevices.getUserMedia(config);
+ }
+ </script>
+ </body>
+</html>
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot.png b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot.png
new file mode 100644
index 0000000000..e94c4e3621
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot.png
Binary files differ
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_center.png b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_center.png
new file mode 100644
index 0000000000..9207d38ed4
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_center.png
Binary files differ
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_cropped_diagonal.png b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_cropped_diagonal.png
new file mode 100644
index 0000000000..3a23fe8794
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_cropped_diagonal.png
Binary files differ
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_diagonal.png b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_diagonal.png
new file mode 100644
index 0000000000..56ab0cc380
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_diagonal.png
Binary files differ
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_uncropped.png b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_uncropped.png
new file mode 100644
index 0000000000..83760bbae7
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_uncropped.png
Binary files differ
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_upperleft.png b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_upperleft.png
new file mode 100644
index 0000000000..dc0d1b2371
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_upperleft.png
Binary files differ
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/manifest.json b/browser/tools/mozscreenshots/mozscreenshots/extension/manifest.json
new file mode 100644
index 0000000000..375bb96f47
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/manifest.json
@@ -0,0 +1,23 @@
+{
+ "manifest_version": 2,
+ "name": "mozscreenshots",
+ "description": "Take screenshots of Mozilla applications in various UI configurations.",
+ "version": "1.0",
+
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "mozscreenshots@mozilla.org"
+ }
+ },
+
+ "experiment_apis": {
+ "mozscreenshots": {
+ "schema": "schema.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "api.js",
+ "events": ["startup"]
+ }
+ }
+ }
+}
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/moz.build b/browser/tools/mozscreenshots/mozscreenshots/extension/moz.build
new file mode 100644
index 0000000000..173f6e6dae
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/moz.build
@@ -0,0 +1,58 @@
+# -*- Mode: python; 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"
+
+USE_EXTENSION_MANIFEST = True
+
+FINAL_TARGET_FILES += [
+ "api.js",
+ "manifest.json",
+ "schema.json",
+]
+
+FINAL_TARGET_FILES.resources += [
+ "Screenshot.sys.mjs",
+ "TestRunner.sys.mjs",
+]
+
+FINAL_TARGET_FILES.resources.configurations += [
+ "configurations/AppMenu.sys.mjs",
+ "configurations/Buttons.sys.mjs",
+ "configurations/ControlCenter.sys.mjs",
+ "configurations/CustomizeMode.sys.mjs",
+ "configurations/DevTools.sys.mjs",
+ "configurations/LightweightThemes.sys.mjs",
+ "configurations/PermissionPrompts.sys.mjs",
+ "configurations/Preferences.sys.mjs",
+ "configurations/Tabs.sys.mjs",
+ "configurations/TabsInTitlebar.sys.mjs",
+ "configurations/Toolbars.sys.mjs",
+ "configurations/UIDensities.sys.mjs",
+ "configurations/WindowSize.sys.mjs",
+]
+
+FINAL_TARGET_FILES.resources.lib += [
+ "lib/borderify.xpi",
+ "lib/mozscreenshots-script.js",
+ "lib/mozscreenshots-style.css",
+ "lib/mozscreenshots.html",
+ "lib/permissionPrompts.html",
+ "lib/robot.png",
+ "lib/robot_center.png",
+ "lib/robot_cropped_diagonal.png",
+ "lib/robot_diagonal.png",
+ "lib/robot_uncropped.png",
+ "lib/robot_upperleft.png",
+]
+
+FINAL_TARGET_FILES.resources.lib.controlCenter += [
+ "lib/controlCenter/mixed.html",
+ "lib/controlCenter/mixed_active.html",
+ "lib/controlCenter/mixed_passive.html",
+ "lib/controlCenter/password.html",
+ "lib/controlCenter/tracking.html",
+]
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/schema.json b/browser/tools/mozscreenshots/mozscreenshots/extension/schema.json
new file mode 100644
index 0000000000..fe51488c70
--- /dev/null
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/schema.json
@@ -0,0 +1 @@
+[]
diff --git a/browser/tools/mozscreenshots/permissionPrompts/browser.ini b/browser/tools/mozscreenshots/permissionPrompts/browser.ini
new file mode 100644
index 0000000000..9a97184c73
--- /dev/null
+++ b/browser/tools/mozscreenshots/permissionPrompts/browser.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+subsuite = screenshots
+support-files =
+ ../head.js
+prefs =
+ signon.testOnlyUserHasInteractedByPrefValue=true
+ signon.testOnlyUserHasInteractedWithDocument=true
+
+[browser_permissionPrompts.js]
+skip-if = os == 'mac' # times out on macosx1014, see 1570098
diff --git a/browser/tools/mozscreenshots/permissionPrompts/browser_permissionPrompts.js b/browser/tools/mozscreenshots/permissionPrompts/browser_permissionPrompts.js
new file mode 100644
index 0000000000..071409bc6e
--- /dev/null
+++ b/browser/tools/mozscreenshots/permissionPrompts/browser_permissionPrompts.js
@@ -0,0 +1,16 @@
+/* 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/. */
+
+/* import-globals-from ../head.js */
+
+"use strict";
+
+add_task(async function capture() {
+ if (!shouldCapture()) {
+ return;
+ }
+ let sets = ["LightweightThemes", "PermissionPrompts"];
+
+ await TestRunner.start(sets, "permissionPrompts");
+});
diff --git a/browser/tools/mozscreenshots/preferences/browser.ini b/browser/tools/mozscreenshots/preferences/browser.ini
new file mode 100644
index 0000000000..a9b12879ed
--- /dev/null
+++ b/browser/tools/mozscreenshots/preferences/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+subsuite = screenshots
+support-files =
+ ../head.js
+
+[browser_preferences.js]
+skip-if = os == 'mac' # macosx1014 times out, see 1570086
diff --git a/browser/tools/mozscreenshots/preferences/browser_preferences.js b/browser/tools/mozscreenshots/preferences/browser_preferences.js
new file mode 100644
index 0000000000..7e51ab5549
--- /dev/null
+++ b/browser/tools/mozscreenshots/preferences/browser_preferences.js
@@ -0,0 +1,16 @@
+/* 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/. */
+
+/* import-globals-from ../head.js */
+
+"use strict";
+
+add_task(async function capture() {
+ if (!shouldCapture()) {
+ return;
+ }
+ let sets = ["Preferences"];
+
+ await TestRunner.start(sets, "preferences");
+});
diff --git a/browser/tools/mozscreenshots/primaryUI/browser.ini b/browser/tools/mozscreenshots/primaryUI/browser.ini
new file mode 100644
index 0000000000..ad542e3b0d
--- /dev/null
+++ b/browser/tools/mozscreenshots/primaryUI/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+subsuite = screenshots
+support-files =
+ ../head.js
+
+[browser_primaryUI.js]
+skip-if = os == 'mac' # macosx1014 times out, see bug 1570102
diff --git a/browser/tools/mozscreenshots/primaryUI/browser_primaryUI.js b/browser/tools/mozscreenshots/primaryUI/browser_primaryUI.js
new file mode 100644
index 0000000000..48c1a024c4
--- /dev/null
+++ b/browser/tools/mozscreenshots/primaryUI/browser_primaryUI.js
@@ -0,0 +1,23 @@
+/* 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/. */
+
+/* import-globals-from ../head.js */
+
+"use strict";
+
+add_task(async function capture() {
+ if (!shouldCapture()) {
+ return;
+ }
+
+ let sets = [
+ "TabsInTitlebar",
+ "Tabs",
+ "WindowSize",
+ "Toolbars",
+ "LightweightThemes",
+ "UIDensities",
+ ];
+ await TestRunner.start(sets, "primaryUI");
+});
diff --git a/browser/tools/mozscreenshots/tests/browser/browser.ini b/browser/tools/mozscreenshots/tests/browser/browser.ini
new file mode 100644
index 0000000000..c7c7805bca
--- /dev/null
+++ b/browser/tools/mozscreenshots/tests/browser/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+subsuite = screenshots
+support-files =
+ ../../head.js
+
+[browser_boundingbox.js]
+[browser_screenshots.js]
+[browser_screenshots_cropping.js]
diff --git a/browser/tools/mozscreenshots/tests/browser/browser_boundingbox.js b/browser/tools/mozscreenshots/tests/browser/browser_boundingbox.js
new file mode 100644
index 0000000000..cef3fa7de1
--- /dev/null
+++ b/browser/tools/mozscreenshots/tests/browser/browser_boundingbox.js
@@ -0,0 +1,173 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+/* import-globals-from ../../head.js */
+
+add_task(async function () {
+ const scale = window.docShell.QueryInterface(
+ Ci.nsIBaseWindow
+ ).devicePixelsPerDesktopPixel;
+ let { bounds, rects } = TestRunner._findBoundingBox(["#tabbrowser-tabs"]);
+ let tabBar = document.querySelector("#tabbrowser-tabs");
+ let tabBarRect = tabBar.getBoundingClientRect();
+
+ // Calculate expected values
+ let expectedLeft = scale * (tabBar.screenX - TestRunner.croppingPadding);
+ let expectedTop = scale * (tabBar.screenY - TestRunner.croppingPadding);
+ let expectedRight =
+ scale * (tabBarRect.width + TestRunner.croppingPadding * 2) + expectedLeft;
+ let expectedBottom =
+ scale * (tabBarRect.height + TestRunner.croppingPadding * 2) + expectedTop;
+
+ // Calculate browser region
+ let windowLeft = window.screenX * scale;
+ let windowTop = window.screenY * scale;
+ let windowRight = window.outerWidth * scale + windowLeft;
+ let windowBottom = window.outerHeight * scale + windowTop;
+
+ // Adjust values based on browser window
+ expectedLeft = Math.max(expectedLeft, windowLeft);
+ expectedTop = Math.max(expectedTop, windowTop);
+ expectedRight = Math.min(expectedRight, windowRight);
+ expectedBottom = Math.min(expectedBottom, windowBottom);
+ // Check width calculation on simple example
+ is(
+ bounds.width,
+ expectedRight - expectedLeft,
+ "Checking _findBoundingBox width calculation"
+ );
+ // Check height calculation on simple example
+ is(
+ bounds.height,
+ expectedBottom - expectedTop,
+ "Checking _findBoundingBox height caclulation"
+ );
+ is(
+ bounds.left,
+ rects[0].left,
+ "Checking _findBoundingBox union.left and rect.left is the same for a single selector"
+ );
+ is(
+ bounds.right,
+ rects[0].right,
+ "Checking _findBoundingBox union.right and rect.right is the same for a single selector"
+ );
+ is(
+ bounds.top,
+ rects[0].top,
+ "Checking _findBoundingBox union.top and rect.top is the same for a single selector"
+ );
+ is(
+ bounds.bottom,
+ rects[0].bottom,
+ "Checking _findBoundingBox union.bottom and rect.bottom is the same for a single selector"
+ );
+
+ let result = TestRunner._findBoundingBox(["#forward-button", "#TabsToolbar"]);
+ bounds = result.bounds;
+ rects = result.rects;
+
+ let tabToolbar = document.querySelector("#TabsToolbar");
+ let tabToolbarRect = tabToolbar.getBoundingClientRect();
+ let fButton = document.querySelector("#forward-button");
+ let fButtonRect = fButton.getBoundingClientRect();
+
+ // Calculate expected values
+ expectedLeft =
+ scale *
+ (Math.min(tabToolbar.screenX, fButton.screenX) -
+ TestRunner.croppingPadding);
+ expectedTop =
+ scale *
+ (Math.min(tabToolbar.screenY, fButton.screenY) -
+ TestRunner.croppingPadding);
+ expectedRight =
+ scale *
+ (Math.max(
+ tabToolbarRect.width + tabToolbar.screenX,
+ fButtonRect.width + fButton.screenX
+ ) +
+ TestRunner.croppingPadding);
+ expectedBottom =
+ scale *
+ (Math.max(
+ tabToolbarRect.height + tabToolbar.screenY,
+ fButtonRect.height + fButton.screenY
+ ) +
+ TestRunner.croppingPadding);
+
+ // Adjust values based on browser window
+ expectedLeft = Math.max(expectedLeft, windowLeft);
+ expectedTop = Math.max(expectedTop, windowTop);
+ expectedRight = Math.min(expectedRight, windowRight);
+ expectedBottom = Math.min(expectedBottom, windowBottom);
+
+ // Check width calculation on union
+ is(
+ bounds.width,
+ expectedRight - expectedLeft,
+ "Checking _findBoundingBox union width calculation"
+ );
+ // Check height calculation on union
+ is(
+ bounds.height,
+ expectedBottom - expectedTop,
+ "Checking _findBoundingBox union height calculation"
+ );
+ // Check single selector's left position
+ is(
+ rects[0].left,
+ Math.max(
+ scale * (fButton.screenX - TestRunner.croppingPadding),
+ windowLeft
+ ),
+ "Checking single selector's left position when _findBoundingBox has multiple selectors"
+ );
+ // Check single selector's right position
+ is(
+ rects[0].right,
+ Math.min(
+ scale *
+ (fButtonRect.width + fButton.screenX + TestRunner.croppingPadding),
+ windowRight
+ ),
+ "Checking single selector's right position when _findBoundingBox has multiple selectors"
+ );
+ // Check single selector's top position
+ is(
+ rects[0].top,
+ Math.max(scale * (fButton.screenY - TestRunner.croppingPadding), windowTop),
+ "Checking single selector's top position when _findBoundingBox has multiple selectors"
+ );
+ // Check single selector's bottom position
+ is(
+ rects[0].bottom,
+ Math.min(
+ scale *
+ (fButtonRect.height + fButton.screenY + TestRunner.croppingPadding),
+ windowBottom
+ ),
+ "Checking single selector's bottom position when _findBoundingBox has multiple selectors"
+ );
+
+ // Check that nonexistent selectors throws an exception
+ Assert.throws(
+ () => {
+ TestRunner._findBoundingBox(["#does_not_exist"]);
+ },
+ /No element for '#does_not_exist' found/,
+ "Checking that nonexistent selectors throws an exception"
+ );
+
+ // Check that no selectors throws an exception
+ Assert.throws(
+ () => {
+ TestRunner._findBoundingBox([]);
+ },
+ /No selectors specified/,
+ "Checking that no selectors throws an exception"
+ );
+});
diff --git a/browser/tools/mozscreenshots/tests/browser/browser_screenshots.js b/browser/tools/mozscreenshots/tests/browser/browser_screenshots.js
new file mode 100644
index 0000000000..88f7c6a704
--- /dev/null
+++ b/browser/tools/mozscreenshots/tests/browser/browser_screenshots.js
@@ -0,0 +1,21 @@
+/* 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";
+
+/* import-globals-from ../../head.js */
+
+add_task(async function capture() {
+ let setsEnv = Services.env.get("MOZSCREENSHOTS_SETS");
+ if (!setsEnv) {
+ ok(
+ true,
+ "MOZSCREENSHOTS_SETS wasn't specified so there's nothing to capture"
+ );
+ return;
+ }
+
+ let sets = TestRunner.splitEnv(setsEnv.trim());
+ await TestRunner.start(sets);
+});
diff --git a/browser/tools/mozscreenshots/tests/browser/browser_screenshots_cropping.js b/browser/tools/mozscreenshots/tests/browser/browser_screenshots_cropping.js
new file mode 100644
index 0000000000..80fcde5d68
--- /dev/null
+++ b/browser/tools/mozscreenshots/tests/browser/browser_screenshots_cropping.js
@@ -0,0 +1,153 @@
+/* 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";
+
+/* import-globals-from ../../head.js */
+
+const { Rect } = ChromeUtils.importESModule(
+ "resource://gre/modules/Geometry.sys.mjs"
+);
+
+async function draw(window, src) {
+ const { document, Image } = window;
+
+ const promise = new Promise((resolve, reject) => {
+ const img = new Image();
+
+ img.onload = function () {
+ // Create a new offscreen canvas
+ const canvas = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.width = img.naturalWidth;
+ canvas.height = img.naturalHeight;
+ const ctx = canvas.getContext("2d");
+
+ ctx.drawImage(img, 0, 0);
+
+ resolve(canvas);
+ };
+
+ img.onerror = function () {
+ reject(`error loading image ${src}`);
+ };
+
+ // Load the src image for drawing
+ img.src = src;
+ });
+
+ return promise;
+}
+
+async function compareImages(window, expected, test) {
+ const testCanvas = await draw(window, test);
+ const expectedCanvas = await draw(window, expected);
+
+ is(
+ testCanvas.width,
+ expectedCanvas.width,
+ "The test and expected images must be the same size"
+ );
+ is(
+ testCanvas.height,
+ expectedCanvas.height,
+ "The test and expected images must be the same size"
+ );
+
+ var maxDifference = {};
+ var differences = window.windowUtils.compareCanvases(
+ expectedCanvas,
+ testCanvas,
+ maxDifference
+ );
+
+ // Fuzz for minor differences that can be caused by the encoder.
+ if (maxDifference.value > 1) {
+ return differences;
+ }
+ return 0;
+}
+
+async function cropAndCompare(window, src, expected, test, region, subregions) {
+ await TestRunner._cropImage(window, src, region, subregions, test);
+
+ return compareImages(window, expected, PathUtils.toFileURI(test));
+}
+
+add_task(async function crop() {
+ const window = Services.wm.getMostRecentWindow("navigator:browser");
+
+ const tmp = PathUtils.tempDir;
+ is(
+ await cropAndCompare(
+ window,
+ "resource://mozscreenshots/lib/robot.png",
+ "resource://mozscreenshots/lib/robot_upperleft.png",
+ PathUtils.join(tmp, "test_cropped_upperleft.png"),
+ new Rect(0, 0, 32, 32),
+ [new Rect(0, 0, 32, 32)]
+ ),
+ 0,
+ "The image should be cropped to the upper left quadrant"
+ );
+
+ is(
+ await cropAndCompare(
+ window,
+ "resource://mozscreenshots/lib/robot.png",
+ "resource://mozscreenshots/lib/robot_center.png",
+ PathUtils.join(tmp, "test_cropped_center.png"),
+ new Rect(16, 16, 32, 32),
+ [new Rect(16, 16, 32, 32)]
+ ),
+ 0,
+ "The image should be cropped to the center of the image"
+ );
+
+ is(
+ await cropAndCompare(
+ window,
+ "resource://mozscreenshots/lib/robot.png",
+ "resource://mozscreenshots/lib/robot_uncropped.png",
+ PathUtils.join(tmp, "test_uncropped.png"),
+ new Rect(-8, -9, 80, 80),
+ [new Rect(-8, -9, 80, 80)]
+ ),
+ 0,
+ "The image should be not be cropped, and the cropping region should be clipped to the size of the image"
+ );
+
+ is(
+ await cropAndCompare(
+ window,
+ "resource://mozscreenshots/lib/robot.png",
+ "resource://mozscreenshots/lib/robot_diagonal.png",
+ PathUtils.join(tmp, "test_diagonal.png"),
+ new Rect(0, 0, 64, 64),
+ [
+ new Rect(0, 0, 16, 16),
+ new Rect(16, 16, 16, 16),
+ new Rect(32, 32, 16, 16),
+ new Rect(48, 48, 16, 16),
+ ]
+ ),
+ 0,
+ "The image should be contain squares across the diagonal"
+ );
+
+ is(
+ await cropAndCompare(
+ window,
+ "resource://mozscreenshots/lib/robot.png",
+ "resource://mozscreenshots/lib/robot_cropped_diagonal.png",
+ PathUtils.join(tmp, "test_cropped_diagonal.png"),
+ new Rect(16, 16, 48, 48),
+ [new Rect(16, 16, 16, 16), new Rect(32, 32, 16, 16)]
+ ),
+ 0,
+ "The image should be cropped with squares across the diagonal"
+ );
+});
diff --git a/browser/tools/mozscreenshots/tests/xpcshell/test_testConfigurations.js b/browser/tools/mozscreenshots/tests/xpcshell/test_testConfigurations.js
new file mode 100644
index 0000000000..8461e38939
--- /dev/null
+++ b/browser/tools/mozscreenshots/tests/xpcshell/test_testConfigurations.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { TestRunner } = ChromeUtils.importESModule(
+ "resource://test/TestRunner.sys.mjs"
+);
+
+add_task(async function capture() {
+ equal(TestRunner.findComma("Toolbars,Devs"), 8);
+ equal(TestRunner.findComma("Toolbars"), -1);
+ equal(TestRunner.findComma("Toolbars[onlyNavBar,allToolbars],DevTools"), 32);
+ equal(
+ TestRunner.findComma(
+ "Toolbars[onlyNavBar,allToolbars],DevTools[bottomToolbox,sideToolbox]"
+ ),
+ 32
+ );
+ equal(
+ TestRunner.findComma(
+ "Toolbars[[onlyNavBar],[]], Tabs[ [fiveTabbed], [[[fourPinned]]] ]"
+ ),
+ 25
+ );
+ equal(TestRunner.findComma("[[[[[[[[[[[[[[[[[[[[]]"), -1);
+ equal(TestRunner.findComma("Preferences[[[[[,]]]]]"), -1);
+
+ deepEqual(TestRunner.splitEnv("Toolbars"), ["Toolbars"]);
+ deepEqual(TestRunner.splitEnv("Buttons,Tabs"), ["Buttons", "Tabs"]);
+ deepEqual(TestRunner.splitEnv("Buttons, Tabs"), ["Buttons", "Tabs"]);
+ deepEqual(TestRunner.splitEnv(" Buttons , Tabs "), [
+ "Buttons",
+ "Tabs",
+ ]);
+ deepEqual(TestRunner.splitEnv("Toolbars[onlyNavBar,allToolbars],DevTools"), [
+ "Toolbars[onlyNavBar,allToolbars]",
+ "DevTools",
+ ]);
+ deepEqual(
+ TestRunner.splitEnv(
+ "Toolbars[onlyNavBar,allToolbars],DevTools[bottomToolbox]"
+ ),
+ ["Toolbars[onlyNavBar,allToolbars]", "DevTools[bottomToolbox]"]
+ );
+ deepEqual(
+ TestRunner.splitEnv(
+ "Toolbars[onlyNavBar,allToolbars],DevTools[bottomToolbox],Tabs"
+ ),
+ ["Toolbars[onlyNavBar,allToolbars]", "DevTools[bottomToolbox]", "Tabs"]
+ );
+
+ let filteredData = TestRunner.filterRestrictions("Toolbars[onlyNavBar]");
+ equal(filteredData.trimmedSetName, "Toolbars");
+ ok(filteredData.restrictions.has("onlyNavBar"));
+
+ filteredData = TestRunner.filterRestrictions(
+ "DevTools[bottomToolbox,sideToolbox]"
+ );
+ equal(filteredData.trimmedSetName, "DevTools");
+ ok(filteredData.restrictions.has("bottomToolbox"));
+ ok(filteredData.restrictions.has("sideToolbox"));
+});
diff --git a/browser/tools/mozscreenshots/tests/xpcshell/xpcshell.ini b/browser/tools/mozscreenshots/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..c318ae4971
--- /dev/null
+++ b/browser/tools/mozscreenshots/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+skip-if = toolkit == 'android' # bug 1730213
+support-files = ../../mozscreenshots/extension/TestRunner.sys.mjs
+
+[test_testConfigurations.js]