diff options
Diffstat (limited to '')
54 files changed, 3383 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..2c7fc4c3fd --- /dev/null +++ b/browser/tools/mozscreenshots/head.js @@ -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/. */ + +/* 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.import("resource://mozscreenshots/TestRunner.jsm") + .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.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.jsm new file mode 100644 index 0000000000..155f2741bc --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.jsm @@ -0,0 +1,204 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["Screenshot"]; + +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "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); +}); + +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.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/TestRunner.jsm new file mode 100644 index 0000000000..b29ec3748b --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/TestRunner.jsm @@ -0,0 +1,660 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["TestRunner"]; + +const APPLY_CONFIG_TIMEOUT_MS = 60 * 1000; +const HOME_PAGE = "resource://mozscreenshots/lib/mozscreenshots.html"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +const { Rect } = ChromeUtils.importESModule( + "resource://gre/modules/Geometry.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BrowserTestUtils: "resource://testing-common/BrowserTestUtils.sys.mjs", +}); +// Screenshot.jsm must be imported this way for xpcshell tests to work +ChromeUtils.defineModuleGetter( + lazy, + "Screenshot", + "resource://mozscreenshots/Screenshot.jsm" +); + +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.loadURI(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.loadURI( + 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..b3361265ae --- /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.import( + "resource://mozscreenshots/TestRunner.jsm" + ); + TestRunner.init(this.extension.rootURI); + } +}; diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.jsm new file mode 100644 index 0000000000..43394a3edd --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.jsm @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["AppMenu"]; + +const { BrowserTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); + +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( + 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.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.jsm new file mode 100644 index 0000000000..95c8e9b0e5 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.jsm @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["Buttons"]; + +const { CustomizableUI } = ChromeUtils.import( + "resource:///modules/CustomizableUI.jsm" +); + +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/Toolbar.png);" + + " -moz-image-region: rect(0px, 18px, 18px, 0px);" + + "}"; + st.appendChild(browserWindow.document.createTextNode(styles)); + browserWindow.document.documentElement.appendChild(st); +} diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.jsm new file mode 100644 index 0000000000..9f77fea59a --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.jsm @@ -0,0 +1,332 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["ControlCenter"]; + +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +const { BrowserTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); +const { SitePermissions } = ChromeUtils.import( + "resource:///modules/SitePermissions.jsm" +); +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +let { UrlClassifierTestUtils } = ChromeUtils.import( + "resource://testing-common/UrlClassifierTestUtils.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`; + +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.loadURI(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.loadURI(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.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.jsm new file mode 100644 index 0000000000..ce67d7e577 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.jsm @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["CustomizeMode"]; + +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +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.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.jsm new file mode 100644 index 0000000000..61a6f9b331 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.jsm @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["DevTools"]; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { gDevTools } = require("devtools/client/framework/devtools"); +const { setTimeout } = ChromeUtils.importESModule( + "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"); +} + +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.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.jsm new file mode 100644 index 0000000000..bb01f0b444 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.jsm @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["LightweightThemes"]; + +const { AddonManager } = ChromeUtils.import( + "resource://gre/modules/AddonManager.jsm" +); + +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.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.jsm new file mode 100644 index 0000000000..93093cc742 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.jsm @@ -0,0 +1,175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["PermissionPrompts"]; + +const { BrowserTestUtils } = ChromeUtils.importESModule( + "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; + +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 => { + const { EventUtils } = ChromeUtils.importESModule( + "resource://specialpowers/SpecialPowersEventUtils.sys.mjs" + ); + + 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.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.jsm new file mode 100644 index 0000000000..4dc9ff7ec4 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.jsm @@ -0,0 +1,185 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["Preferences"]; + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); +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.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.jsm new file mode 100644 index 0000000000..3ec09f8790 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.jsm @@ -0,0 +1,225 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["Tabs"]; + +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>`; + +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +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(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.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.jsm new file mode 100644 index 0000000000..b271d6be28 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.jsm @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["TabsInTitlebar"]; + +const PREF_TABS_IN_TITLEBAR = "browser.tabs.inTitlebar"; + +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.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.jsm new file mode 100644 index 0000000000..e3fea231c1 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.jsm @@ -0,0 +1,63 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["Toolbars"]; + +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.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/UIDensities.jsm new file mode 100644 index 0000000000..a47212317e --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/UIDensities.jsm @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["UIDensities"]; + +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.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.jsm new file mode 100644 index 0000000000..6d2867667b --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.jsm @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["WindowSize"]; + +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +const { BrowserTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); + +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 Binary files differnew file mode 100644 index 0000000000..66ae92ed21 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/borderify.xpi 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..1ac9bd8953 --- /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..8855b66ef2 --- /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..bb2ef3bb1e --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/tracking.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>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..6778cf7daa --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots.html @@ -0,0 +1,36 @@ +<!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..be27ffd824 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/permissionPrompts.html @@ -0,0 +1,36 @@ +<!-- 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 Binary files differnew file mode 100644 index 0000000000..e94c4e3621 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot.png diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_center.png b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_center.png Binary files differnew file mode 100644 index 0000000000..9207d38ed4 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_center.png diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_cropped_diagonal.png b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_cropped_diagonal.png Binary files differnew file mode 100644 index 0000000000..3a23fe8794 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_cropped_diagonal.png diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_diagonal.png b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_diagonal.png Binary files differnew file mode 100644 index 0000000000..56ab0cc380 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_diagonal.png diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_uncropped.png b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_uncropped.png Binary files differnew file mode 100644 index 0000000000..83760bbae7 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_uncropped.png diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_upperleft.png b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_upperleft.png Binary files differnew file mode 100644 index 0000000000..dc0d1b2371 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot_upperleft.png diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/manifest.json b/browser/tools/mozscreenshots/mozscreenshots/extension/manifest.json new file mode 100644 index 0000000000..25983424c1 --- /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", + + "applications": { + "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..3b96b08ee1 --- /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.jsm", + "TestRunner.jsm", +] + +FINAL_TARGET_FILES.resources.configurations += [ + "configurations/AppMenu.jsm", + "configurations/Buttons.jsm", + "configurations/ControlCenter.jsm", + "configurations/CustomizeMode.jsm", + "configurations/DevTools.jsm", + "configurations/LightweightThemes.jsm", + "configurations/PermissionPrompts.jsm", + "configurations/Preferences.jsm", + "configurations/Tabs.jsm", + "configurations/TabsInTitlebar.jsm", + "configurations/Toolbars.jsm", + "configurations/UIDensities.jsm", + "configurations/WindowSize.jsm", +] + +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..914b74b345 --- /dev/null +++ b/browser/tools/mozscreenshots/tests/browser/browser.ini @@ -0,0 +1,8 @@ +[DEFAULT] +subsuite = screenshots +support-files = + ../../head.js + +[browser_screenshots.js] +[browser_screenshots_cropping.js] +[browser_boundingbox.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..2c868551f0 --- /dev/null +++ b/browser/tools/mozscreenshots/tests/browser/browser_boundingbox.js @@ -0,0 +1,172 @@ +/* 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..f478893331 --- /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.osTempDir; + 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..4df120c2de --- /dev/null +++ b/browser/tools/mozscreenshots/tests/xpcshell/test_testConfigurations.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { TestRunner } = ChromeUtils.import("resource://test/TestRunner.jsm"); + +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..9fdeccff3f --- /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.jsm + +[test_testConfigurations.js] |