diff options
Diffstat (limited to 'browser/tools/mozscreenshots')
54 files changed, 3336 insertions, 0 deletions
diff --git a/browser/tools/mozscreenshots/controlCenter/browser.toml b/browser/tools/mozscreenshots/controlCenter/browser.toml new file mode 100644 index 0000000000..4c4ae1b4b8 --- /dev/null +++ b/browser/tools/mozscreenshots/controlCenter/browser.toml @@ -0,0 +1,7 @@ +[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.toml b/browser/tools/mozscreenshots/devtools/browser.toml new file mode 100644 index 0000000000..8434df166e --- /dev/null +++ b/browser/tools/mozscreenshots/devtools/browser.toml @@ -0,0 +1,6 @@ +[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..a1876b9c30 --- /dev/null +++ b/browser/tools/mozscreenshots/head.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* exported TestRunner, shouldCapture */ + +"use strict"; + +const chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( + Ci.nsIChromeRegistry +); +const EXTENSION_DIR = + "chrome://mochitests/content/browser/browser/tools/mozscreenshots/mozscreenshots/extension/mozscreenshots/browser/"; + +let TestRunner; + +async function setup() { + // This timeout doesn't actually end the job even if it is hit - the buildbot timeout will + // handle things for us if the test actually hangs. + requestLongerTimeout(100); + + // Generate output so mozprocess knows we're still alive for the long session. + SimpleTest.requestCompleteLog(); + + info("installing extension temporarily"); + let chromeURL = Services.io.newURI(EXTENSION_DIR); + let dir = chromeRegistry + .convertChromeURL(chromeURL) + .QueryInterface(Ci.nsIFileURL).file; + await AddonManager.installTemporaryAddon(dir); + + info("Checking for mozscreenshots extension"); + + let aAddon = await AddonManager.getAddonByID("mozscreenshots@mozilla.org"); + isnot(aAddon, null, "The mozscreenshots extension should be installed"); + TestRunner = ChromeUtils.importESModule( + "resource://mozscreenshots/TestRunner.sys.mjs" + ).TestRunner; + TestRunner.initTest(this); +} + +/** + * Used by pre-defined sets of configurations to decide whether to run for a build. + * + * This is not used by browser_screenshots.js which handles when MOZSCREENSHOTS_SETS is set. + * + * @returns {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..ffa4017a0d --- /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.toml", + "devtools/browser.toml", + "permissionPrompts/browser.toml", + "preferences/browser.toml", + "primaryUI/browser.toml", + "tests/browser/browser.toml", +] + +TEST_DIRS += [ + "mozscreenshots/extension", +] + +XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"] diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/Makefile.in b/browser/tools/mozscreenshots/mozscreenshots/extension/Makefile.in new file mode 100644 index 0000000000..aa496e27af --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/Makefile.in @@ -0,0 +1,12 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +OUTPUT_DIR = $(DEPTH)/_tests/testing/mochitest/browser/browser/tools/mozscreenshots/mozscreenshots/extension +GENERATED_DIRS = $(OUTPUT_DIR) +XPI_PKGNAME = mozscreenshots@mozilla.org + +include $(topsrcdir)/config/rules.mk + +libs:: + (cd $(DIST)/xpi-stage && tar $(TAR_CREATE_FLAGS) - $(XPI_NAME)) | (cd $(OUTPUT_DIR) && tar -xf -) diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.sys.mjs new file mode 100644 index 0000000000..a2fe6195ab --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.sys.mjs @@ -0,0 +1,195 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; + +// 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 = {}; +ChromeUtils.defineLazyGetter(lazy, "log", () => { + let { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" + ); + let consoleOptions = { + maxLogLevel: "info", + maxLogLevelPref: PREF_LOG_LEVEL, + prefix: "mozscreenshots", + }; + return new ConsoleAPI(consoleOptions); +}); + +export var Screenshot = { + _extensionPath: null, + _path: null, + _imagePrefix: "", + _imageExtension: ".png", + _screenshotFunction: null, + + init(path, extensionPath, imagePrefix = "") { + this._path = path; + + let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dir.initWithPath(this._path); + if (!dir.exists()) { + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + + this._extensionPath = extensionPath; + this._imagePrefix = imagePrefix; + switch (Services.appinfo.OS) { + case "WINNT": + this._screenshotFunction = this._screenshotWindows; + break; + case "Darwin": + this._screenshotFunction = this._screenshotOSX; + break; + case "Linux": + this._screenshotFunction = this._screenshotLinux; + break; + default: + throw new Error("Unsupported operating system"); + } + }, + + _buildImagePath(baseName) { + return PathUtils.join( + this._path, + this._imagePrefix + baseName + this._imageExtension + ); + }, + + // Capture the whole screen using an external application. + async captureExternal(filename) { + let imagePath = this._buildImagePath(filename); + await this._screenshotFunction(imagePath); + lazy.log.debug("saved screenshot: " + filename); + return imagePath; + }, + + // helpers + + _screenshotWindows(filename) { + return new Promise((resolve, reject) => { + let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile); + exe.append("screenshot.exe"); + if (!exe.exists()) { + exe = Services.dirsvc.get("CurWorkD", Ci.nsIFile).parent; + exe.append("bin"); + exe.append("screenshot.exe"); + } + let process = Cc["@mozilla.org/process/util;1"].createInstance( + Ci.nsIProcess + ); + process.init(exe); + + let args = [filename]; + process.runAsync( + args, + args.length, + this._processObserver(resolve, reject) + ); + }); + }, + + async _screenshotOSX(filename) { + let screencapture = (windowID = null) => { + return new Promise((resolve, reject) => { + // Get the screencapture executable + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath("/usr/sbin/screencapture"); + + let process = Cc["@mozilla.org/process/util;1"].createInstance( + Ci.nsIProcess + ); + process.init(file); + + // Run the process. + let args = ["-x", "-t", "png"]; + args.push(filename); + process.runAsync( + args, + args.length, + this._processObserver(resolve, reject) + ); + }); + }; + + function readWindowID() { + return IOUtils.readUTF8("/tmp/mozscreenshots-windowid"); + } + + let promiseWindowID = () => { + return new Promise((resolve, reject) => { + // Get the window ID of the application (assuming its front-most) + let osascript = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile + ); + osascript.initWithPath("/bin/bash"); + + let osascriptP = Cc["@mozilla.org/process/util;1"].createInstance( + Ci.nsIProcess + ); + osascriptP.init(osascript); + let osaArgs = [ + "-c", + "/usr/bin/osascript -e 'tell application (path to frontmost application as text) to set winID to id of window 1' > /tmp/mozscreenshots-windowid", + ]; + osascriptP.runAsync( + osaArgs, + osaArgs.length, + this._processObserver(resolve, reject) + ); + }); + }; + + await promiseWindowID(); + let windowID = await readWindowID(); + await screencapture(windowID); + }, + + _screenshotLinux(filename) { + return new Promise((resolve, reject) => { + let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile); + exe.append("screentopng"); + if (!exe.exists()) { + exe = Services.dirsvc.get("CurWorkD", Ci.nsIFile).parent; + exe.append("bin"); + exe.append("screentopng"); + } + let process = Cc["@mozilla.org/process/util;1"].createInstance( + Ci.nsIProcess + ); + process.init(exe); + + let args = [filename]; + process.runAsync( + args, + args.length, + this._processObserver(resolve, reject) + ); + }); + }, + + _processObserver(resolve, reject) { + return { + observe(subject, topic, data) { + switch (topic) { + case "process-finished": + try { + // Wait 1s after process to resolve + setTimeout(resolve, 1000); + } catch (ex) { + reject(ex); + } + break; + default: + reject(topic); + break; + } + }, + }; + }, +}; diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/TestRunner.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/TestRunner.sys.mjs new file mode 100644 index 0000000000..7ef72ec275 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/TestRunner.sys.mjs @@ -0,0 +1,655 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const APPLY_CONFIG_TIMEOUT_MS = 60 * 1000; +const HOME_PAGE = "resource://mozscreenshots/lib/mozscreenshots.html"; + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; +import { Rect } from "resource://gre/modules/Geometry.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BrowserTestUtils: "resource://testing-common/BrowserTestUtils.sys.mjs", + // Screenshot.sys.mjs must be imported this way for xpcshell tests to work + Screenshot: "resource://mozscreenshots/Screenshot.sys.mjs", +}); + +export var TestRunner = { + combos: null, + completedCombos: 0, + currentComboIndex: 0, + _lastCombo: null, + _libDir: null, + croppingPadding: 0, + mochitestScope: null, + + init(extensionPath) { + this._extensionPath = extensionPath; + this.setupOS(); + }, + + /** + * Initialize the mochitest interface. This allows TestRunner to integrate + * with mochitest functions like is(...) and ok(...). This must be called + * prior to invoking any of the TestRunner functions. Note that this should + * be properly setup in head.js, so you probably don't need to call it. + */ + initTest(mochitestScope) { + this.mochitestScope = mochitestScope; + }, + + setupOS() { + switch (AppConstants.platform) { + case "macosx": { + this.disableNotificationCenter(); + break; + } + } + }, + + disableNotificationCenter() { + let killall = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + killall.initWithPath("/bin/bash"); + + let killallP = Cc["@mozilla.org/process/util;1"].createInstance( + Ci.nsIProcess + ); + killallP.init(killall); + let ncPlist = + "/System/Library/LaunchAgents/com.apple.notificationcenterui.plist"; + let killallArgs = [ + "-c", + `/bin/launchctl unload -w ${ncPlist} && ` + + "/usr/bin/killall -v NotificationCenter", + ]; + killallP.run(true, killallArgs, killallArgs.length); + }, + + /** + * Load specified sets, execute all combinations of them, and capture screenshots. + */ + async start(setNames, jobName = null) { + let subDirs = [ + "mozscreenshots", + new Date().toISOString().replace(/:/g, "-") + "_" + Services.appinfo.OS, + ]; + let screenshotPath = PathUtils.join(PathUtils.tempDir, ...subDirs); + + const MOZ_UPLOAD_DIR = Services.env.get("MOZ_UPLOAD_DIR"); + const GECKO_HEAD_REPOSITORY = Services.env.get("GECKO_HEAD_REPOSITORY"); + // We don't want to upload images (from MOZ_UPLOAD_DIR) on integration + // branches in order to reduce bandwidth/storage. + if (MOZ_UPLOAD_DIR && !GECKO_HEAD_REPOSITORY.includes("/integration/")) { + screenshotPath = MOZ_UPLOAD_DIR; + } + + this.mochitestScope.info(`Saving screenshots to: ${screenshotPath}`); + + let screenshotPrefix = Services.appinfo.appBuildID; + if (jobName) { + screenshotPrefix += "-" + jobName; + } + screenshotPrefix += "_"; + lazy.Screenshot.init(screenshotPath, this._extensionPath, screenshotPrefix); + this._libDir = this._extensionPath + .QueryInterface(Ci.nsIFileURL) + .file.clone(); + this._libDir.append("chrome"); + this._libDir.append("mozscreenshots"); + this._libDir.append("lib"); + + let sets = this.loadSets(setNames); + + this.mochitestScope.info(`${sets.length} sets: ${setNames}`); + this.combos = new LazyProduct(sets); + this.mochitestScope.info(this.combos.length + " combinations"); + + this.currentComboIndex = this.completedCombos = 0; + this._lastCombo = null; + + // Setup some prefs + Services.prefs.setCharPref( + "extensions.ui.lastCategory", + "addons://list/extension" + ); + // Don't let the caret blink since it causes false positives for image diffs + Services.prefs.setIntPref("ui.caretBlinkTime", -1); + // Disable some animations that can cause false positives, such as the + // reload/stop button spinning animation. + Services.prefs.setIntPref("ui.prefersReducedMotion", 1); + + let browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); + + // Prevent the mouse cursor from causing hover styles or tooltips to appear. + browserWindow.windowUtils.disableNonTestMouseEvents(true); + + // When being automated through Marionette, Firefox shows a prominent indication + // in the urlbar and identity block. We don't want this to show when testing browser UI. + // Note that this doesn't prevent subsequently opened windows from showing the automation UI. + browserWindow.document + .getElementById("main-window") + .removeAttribute("remotecontrol"); + + let selectedBrowser = browserWindow.gBrowser.selectedBrowser; + lazy.BrowserTestUtils.startLoadingURIString(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]" + * @returns {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"]. + * @returns {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.importESModule( + `resource://mozscreenshots/configurations/${setName}.sys.mjs` + ); + 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.startLoadingURIString( + 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 + * @returns {Rect} + * A Geometry.sys.mjs 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 + * @returns {number} 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 + * @returns {string[]} Array of strings containing the configurations + * e.g. ["Toolbars[onlyNavBar,allToolbars]","DevTools[jsdebugger,webconsole]","Tabs"] + */ + splitEnv(envVar) { + let result = []; + + let commaIndex = this.findComma(envVar); + while (commaIndex != -1) { + result.push(envVar.slice(0, commaIndex).trim()); + envVar = envVar.slice(commaIndex + 1); + commaIndex = this.findComma(envVar); + } + result.push(envVar.trim()); + return result; + }, +}; + +/** + * Helper to lazily compute the Cartesian product of all of the sets of configurations. + */ +function LazyProduct(sets) { + /** + * An entry for each set with the value being: + * [the number of permutations of the sets with lower index, + * the number of items in the set at the index] + */ + this.sets = sets; + this.lookupTable = []; + let combinations = 1; + for (let i = this.sets.length - 1; i >= 0; i--) { + let set = this.sets[i]; + let setLength = Object.keys(set).length; + this.lookupTable[i] = [combinations, setLength]; + combinations *= setLength; + } +} +LazyProduct.prototype = { + get length() { + let last = this.lookupTable[0]; + if (!last) { + return 0; + } + return last[0] * last[1]; + }, + + item(n) { + // For set i, get the item from the set with the floored value of + // (n / the number of permutations of the sets already chosen from) modulo the length of set i + let result = []; + for (let i = this.sets.length - 1; i >= 0; i--) { + let priorCombinations = this.lookupTable[i][0]; + let setLength = this.lookupTable[i][1]; + let keyIndex = Math.floor(n / priorCombinations) % setLength; + let keys = Object.keys(this.sets[i]); + result[i] = this.sets[i][keys[keyIndex]]; + } + return result; + }, +}; + +function padLeft(number, width, padding = "0") { + return padding.repeat(Math.max(0, width - String(number).length)) + number; +} diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/api.js b/browser/tools/mozscreenshots/mozscreenshots/extension/api.js new file mode 100644 index 0000000000..a857567103 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/api.js @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* globals ExtensionAPI */ + +XPCOMUtils.defineLazyServiceGetter( + this, + "resProto", + "@mozilla.org/network/protocol;1?name=resource", + "nsISubstitutingProtocolHandler" +); + +this.mozscreenshots = class extends ExtensionAPI { + async onStartup() { + let uri = Services.io.newURI("resources/", null, this.extension.rootURI); + resProto.setSubstitution("mozscreenshots", uri); + + const { TestRunner } = ChromeUtils.importESModule( + "resource://mozscreenshots/TestRunner.sys.mjs" + ); + TestRunner.init(this.extension.rootURI); + } +}; diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.sys.mjs new file mode 100644 index 0000000000..8309eab623 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.sys.mjs @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs"; + +export var AppMenu = { + init(libDir) {}, + + configurations: { + appMenuMainView: { + selectors: ["#appMenu-popup"], + async applyConfig() { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + await reopenAppMenu(browserWindow); + }, + }, + + appMenuHistorySubview: { + selectors: ["#appMenu-popup"], + async applyConfig() { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + await reopenAppMenu(browserWindow); + + browserWindow.document.getElementById("appMenu-library-button").click(); + let view = browserWindow.document.getElementById("appMenu-libraryView"); + let promiseViewShown = BrowserTestUtils.waitForEvent(view, "ViewShown"); + await promiseViewShown; + }, + + verifyConfig: verifyConfigHelper, + }, + + appMenuHelpSubview: { + selectors: ["#appMenu-popup"], + async applyConfig() { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + await reopenAppMenu(browserWindow); + + browserWindow.document.getElementById("appMenu-help-button2").click(); + let view = browserWindow.document.getElementById("PanelUI-helpView"); + let promiseViewShown = BrowserTestUtils.waitForEvent(view, "ViewShown"); + await promiseViewShown; + }, + + verifyConfig: verifyConfigHelper, + }, + }, +}; + +async function reopenAppMenu(browserWindow) { + browserWindow.PanelUI.hide(); + let promiseViewShown = BrowserTestUtils.waitForEvent( + browserWindow.PanelUI.panel, + "ViewShown" + ); + browserWindow.PanelUI.show(); + await promiseViewShown; +} + +function verifyConfigHelper() { + if (isCustomizing()) { + return "navigator:browser has the customizing attribute"; + } + return undefined; +} + +function isCustomizing() { + let browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); + if (browserWindow.document.documentElement.hasAttribute("customizing")) { + return true; + } + return false; +} diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.sys.mjs new file mode 100644 index 0000000000..2bc12d9012 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.sys.mjs @@ -0,0 +1,97 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { CustomizableUI } from "resource:///modules/CustomizableUI.sys.mjs"; + +export var Buttons = { + init(libDir) { + createWidget(); + }, + + configurations: { + navBarButtons: { + selectors: ["#nav-bar"], + applyConfig: async () => { + CustomizableUI.addWidgetToArea( + "screenshot-widget", + CustomizableUI.AREA_NAVBAR + ); + }, + }, + + tabsToolbarButtons: { + selectors: ["#TabsToolbar"], + applyConfig: async () => { + CustomizableUI.addWidgetToArea( + "screenshot-widget", + CustomizableUI.AREA_TABSTRIP + ); + }, + }, + + menuPanelButtons: { + selectors: ["#widget-overflow"], + applyConfig: async () => { + CustomizableUI.addWidgetToArea( + "screenshot-widget", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + }, + + async verifyConfig() { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + if (browserWindow.PanelUI.panel.state == "closed") { + return "The button isn't shown when the panel isn't open."; + } + return undefined; + }, + }, + + custPaletteButtons: { + selectors: ["#customization-palette"], + applyConfig: async () => { + CustomizableUI.removeWidgetFromArea("screenshot-widget"); + }, + + async verifyConfig() { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + if ( + browserWindow.document.documentElement.getAttribute("customizing") != + "true" + ) { + return "The button isn't shown when we're not in customize mode."; + } + return undefined; + }, + }, + }, +}; + +function createWidget() { + let id = "screenshot-widget"; + let spec = { + id, + label: "My Button", + removable: true, + tooltiptext: "", + type: "button", + }; + CustomizableUI.createWidget(spec); + + // Append a <style> for the image + let browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); + let st = browserWindow.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "style" + ); + let styles = + "" + + "#screenshot-widget > .toolbarbutton-icon {" + + " list-style-image: url(chrome://browser/skin/thumb-down.svg);" + + "}"; + st.appendChild(browserWindow.document.createTextNode(styles)); + browserWindow.document.documentElement.appendChild(st); +} diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.sys.mjs new file mode 100644 index 0000000000..587b573ed3 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.sys.mjs @@ -0,0 +1,320 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs"; +import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; +import { UrlClassifierTestUtils } from "resource://testing-common/UrlClassifierTestUtils.sys.mjs"; + +import { SitePermissions } from "resource:///modules/SitePermissions.sys.mjs"; + +import { NetUtil } from "resource://gre/modules/NetUtil.sys.mjs"; + +const CC_SELECTORS = ["#identity-popup", "#urlbar-input-container"]; +const PP_SELECTORS = ["#protections-popup", "#urlbar-input-container"]; + +const RESOURCE_PATH = + "browser/browser/tools/mozscreenshots/mozscreenshots/extension/mozscreenshots/browser/resources/lib/controlCenter"; +const HTTP_PAGE = "http://example.com/"; +const HTTPS_PAGE = "https://example.com/"; +const PERMISSIONS_PAGE = "https://test1.example.com/"; +const HTTP_PASSWORD_PAGE = `http://test2.example.org/${RESOURCE_PATH}/password.html`; +const MIXED_CONTENT_URL = `https://example.com/${RESOURCE_PATH}/mixed.html`; +const MIXED_ACTIVE_CONTENT_URL = `https://example.com/${RESOURCE_PATH}/mixed_active.html`; +const MIXED_PASSIVE_CONTENT_URL = `https://example.com/${RESOURCE_PATH}/mixed_passive.html`; +const TRACKING_PAGE = `http://tracking.example.org/${RESOURCE_PATH}/tracking.html`; + +export var ControlCenter = { + init(libDir) {}, + + configurations: { + about: { + selectors: CC_SELECTORS, + async applyConfig() { + await loadPage("about:rights"); + await openIdentityPopup(); + }, + }, + + localFile: { + // This selector is different so we can exclude the changing file: path + selectors: ["#identity-popup-security-button"], + async applyConfig() { + let channel = NetUtil.newChannel({ + uri: "resource://mozscreenshots/lib/mozscreenshots.html", + loadUsingSystemPrincipal: true, + }); + channel = channel.QueryInterface(Ci.nsIFileChannel); + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + let gBrowser = browserWindow.gBrowser; + BrowserTestUtils.startLoadingURIString( + 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.startLoadingURIString(gBrowser.selectedBrowser, url); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, url); +} + +async function openIdentityPopup(expand) { + let browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); + let gBrowser = browserWindow.gBrowser; + let { gIdentityHandler } = gBrowser.ownerGlobal; + // Ensure the popup is available, if it's never been opened. + gIdentityHandler._initializePopup(); + gIdentityHandler._identityPopup.hidePopup(); + // Disable the popup shadow on OSX until we have figured out bug 1425253. + if (AppConstants.platform == "macosx") { + gIdentityHandler._identityPopup.classList.add("no-shadow"); + } + gIdentityHandler._identityIconBox.click(); + if (expand) { + // give some time for opening to avoid weird style issues + await new Promise(c => setTimeout(c, 500)); + gIdentityHandler._identityPopup + .querySelector("#identity-popup-security-button") + .click(); + } +} + +async function openProtectionsPopup() { + let browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); + let gBrowser = browserWindow.gBrowser; + let { gProtectionsHandler } = gBrowser.ownerGlobal; + // Force initializing the popup; we can't add classes otherwise. + gProtectionsHandler._initializePopup(); + gProtectionsHandler._protectionsPopup.hidePopup(); + // Disable the popup shadow on OSX until we have figured out bug 1425253. + if (AppConstants.platform == "macosx") { + gProtectionsHandler._protectionsPopup.classList.add("no-shadow"); + } + gProtectionsHandler.showProtectionsPopup(); + // Wait for any animation. + await new Promise(_ => setTimeout(_, 500)); +} diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.sys.mjs new file mode 100644 index 0000000000..a5a1b65b69 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.sys.mjs @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; + +export var CustomizeMode = { + init(libDir) {}, + + configurations: { + notCustomizing: { + selectors: ["#navigator-toolbox"], + applyConfig() { + return new Promise(resolve => { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + if ( + !browserWindow.document.documentElement.hasAttribute("customizing") + ) { + resolve("notCustomizing: already not customizing"); + return; + } + function onCustomizationEnds() { + browserWindow.gNavToolbox.removeEventListener( + "aftercustomization", + onCustomizationEnds + ); + // Wait for final changes + setTimeout( + () => resolve("notCustomizing: onCustomizationEnds"), + 500 + ); + } + browserWindow.gNavToolbox.addEventListener( + "aftercustomization", + onCustomizationEnds + ); + browserWindow.gCustomizeMode.exit(); + }); + }, + }, + + customizing: { + selectors: ["#navigator-toolbox", "#customization-container"], + applyConfig() { + return new Promise(resolve => { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + if ( + browserWindow.document.documentElement.hasAttribute("customizing") + ) { + resolve("customizing: already customizing"); + return; + } + function onCustomizing() { + browserWindow.gNavToolbox.removeEventListener( + "customizationready", + onCustomizing + ); + // Wait for final changes + setTimeout(() => resolve("customizing: onCustomizing"), 500); + } + browserWindow.gNavToolbox.addEventListener( + "customizationready", + onCustomizing + ); + browserWindow.gCustomizeMode.enter(); + }); + }, + }, + }, +}; diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.sys.mjs new file mode 100644 index 0000000000..addef011c0 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.sys.mjs @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { require } from "resource://devtools/shared/loader/Loader.sys.mjs"; + +const { gDevTools } = require("devtools/client/framework/devtools"); + +import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; + +async function showToolboxForSelectedTab(toolId, hostType) { + const browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); + const tab = browserWindow.gBrowser.selectedTab; + return gDevTools.showToolboxForTab(tab, { toolId, hostType }); +} + +function selectToolbox(toolbox) { + return toolbox.win.document.querySelector("#toolbox-container"); +} + +export var DevTools = { + init(libDir) { + let panels = [ + "options", + "webconsole", + "jsdebugger", + "styleeditor", + "performance", + "netmonitor", + ]; + + panels.forEach(panel => { + this.configurations[panel] = {}; + this.configurations[panel].applyConfig = async function () { + Services.prefs.setIntPref("devtools.toolbox.footer.height", 800); + let toolbox = await showToolboxForSelectedTab(panel, "bottom"); + this.selectors = [selectToolbox.bind(null, toolbox)]; + await new Promise(resolve => setTimeout(resolve, 500)); + }; + }); + }, + + configurations: { + bottomToolbox: { + async applyConfig() { + Services.prefs.clearUserPref("devtools.toolbox.footer.height"); + let toolbox = await showToolboxForSelectedTab("inspector", "bottom"); + this.selectors = [selectToolbox.bind(null, toolbox)]; + await new Promise(resolve => setTimeout(resolve, 1000)); + }, + }, + sideToolbox: { + async applyConfig() { + let toolbox = await showToolboxForSelectedTab("inspector", "right"); + this.selectors = [selectToolbox.bind(null, toolbox)]; + await new Promise(resolve => setTimeout(resolve, 1000)); + }, + async verifyConfig() { + return "Panel sizes are regularly inconsistent"; + }, + }, + undockedToolbox: { + windowType: "devtools:toolbox", + async applyConfig() { + let toolbox = await showToolboxForSelectedTab("inspector", "window"); + this.selectors = [selectToolbox.bind(null, toolbox)]; + await new Promise(resolve => setTimeout(resolve, 1000)); + }, + async verifyConfig() { + return "Panel sizes are regularly inconsistent"; + }, + }, + }, +}; diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.sys.mjs new file mode 100644 index 0000000000..91c3349ec7 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.sys.mjs @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { AddonManager } from "resource://gre/modules/AddonManager.sys.mjs"; + +export var LightweightThemes = { + init(libDir) {}, + + configurations: { + noLWT: { + selectors: [], + async applyConfig() { + let addon = await AddonManager.getAddonByID( + "default-theme@mozilla.org" + ); + await addon.enable(); + }, + }, + + compactLight: { + selectors: [], + async applyConfig() { + let addon = await AddonManager.getAddonByID( + "firefox-compact-light@mozilla.org" + ); + await addon.enable(); + }, + }, + + compactDark: { + selectors: [], + async applyConfig() { + let addon = await AddonManager.getAddonByID( + "firefox-compact-dark@mozilla.org" + ); + await addon.enable(); + }, + }, + + alpenGlow: { + selectors: [], + async applyConfig() { + let addon = await AddonManager.getAddonByID( + "firefox-alpenglow@mozilla.org" + ); + await addon.enable(); + }, + }, + }, +}; diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.sys.mjs new file mode 100644 index 0000000000..de12c69e81 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.sys.mjs @@ -0,0 +1,168 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Various parts here are run in the content process. +/* global content */ + +import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs"; + +const URL = + "https://test1.example.com/browser/browser/tools/mozscreenshots/mozscreenshots/extension/mozscreenshots/browser/resources/lib/permissionPrompts.html"; +let lastTab = null; + +export var PermissionPrompts = { + init(libDir) { + Services.prefs.setBoolPref("media.navigator.permission.fake", true); + Services.prefs.setBoolPref("extensions.install.requireBuiltInCerts", false); + Services.prefs.setBoolPref("signon.rememberSignons", true); + }, + + configurations: { + shareDevices: { + selectors: ["#notification-popup", "#identity-box"], + async applyConfig() { + await closeLastTab(); + await clickOn("#webRTC-shareDevices"); + }, + }, + + shareMicrophone: { + selectors: ["#notification-popup", "#identity-box"], + async applyConfig() { + await closeLastTab(); + await clickOn("#webRTC-shareMicrophone"); + }, + }, + + shareVideoAndMicrophone: { + selectors: ["#notification-popup", "#identity-box"], + async applyConfig() { + await closeLastTab(); + await clickOn("#webRTC-shareDevices2"); + }, + }, + + shareScreen: { + selectors: ["#notification-popup", "#identity-box"], + async applyConfig() { + await closeLastTab(); + await clickOn("#webRTC-shareScreen"); + }, + }, + + geo: { + selectors: ["#notification-popup", "#identity-box"], + async applyConfig() { + await closeLastTab(); + await clickOn("#geo"); + }, + }, + + persistentStorage: { + selectors: ["#notification-popup", "#identity-box"], + async applyConfig() { + await closeLastTab(); + await clickOn("#persistent-storage"); + }, + }, + + loginCapture: { + selectors: ["#notification-popup", "#identity-box"], + async applyConfig() { + await closeLastTab(); + // we need to emulate user input in the form for the save-password prompt to be shown + await clickOn("#login-capture", function beforeContentFn() { + const { E10SUtils } = ChromeUtils.importESModule( + "resource://gre/modules/E10SUtils.sys.mjs" + ); + E10SUtils.wrapHandlingUserInput(content, true, function () { + let element = content.document.querySelector( + "input[type=password]" + ); + element.setUserInput("123456"); + }); + }); + }, + }, + + notifications: { + selectors: ["#notification-popup", "#identity-box"], + async applyConfig() { + await closeLastTab(); + await clickOn("#web-notifications"); + }, + }, + + addons: { + selectors: ["#notification-popup", "#identity-box"], + async applyConfig() { + Services.prefs.setBoolPref("xpinstall.whitelist.required", true); + + await closeLastTab(); + await clickOn("#addons"); + }, + }, + + addonsNoWhitelist: { + selectors: ["#notification-popup", "#identity-box"], + async applyConfig() { + Services.prefs.setBoolPref("xpinstall.whitelist.required", false); + + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + let notification = browserWindow.document.getElementById( + "addon-webext-permissions-notification" + ); + + await closeLastTab(); + await clickOn("#addons"); + + // We want to skip the progress-notification, so we wait for + // the install-confirmation screen to be "not hidden" = shown. + return BrowserTestUtils.waitForCondition( + () => !notification.hidden, + "addon install confirmation did not show", + 200 + ).catch(msg => { + return Promise.resolve({ todo: msg }); + }); + }, + }, + }, +}; + +async function closeLastTab() { + if (!lastTab) { + return; + } + BrowserTestUtils.removeTab(lastTab); + lastTab = null; +} + +async function clickOn(selector, beforeContentFn) { + let browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); + + // Save the tab so we can close it later. + lastTab = await BrowserTestUtils.openNewForegroundTab( + browserWindow.gBrowser, + URL + ); + + let { SpecialPowers } = lastTab.ownerGlobal; + if (beforeContentFn) { + await SpecialPowers.spawn(lastTab.linkedBrowser, [], beforeContentFn); + } + + await SpecialPowers.spawn(lastTab.linkedBrowser, [selector], arg => { + /* eslint-env mozilla/chrome-script */ + let element = content.document.querySelector(arg); + return EventUtils.synthesizeClick(element); + }); + + // Wait for the popup to actually be shown before making the screenshot + await BrowserTestUtils.waitForEvent( + browserWindow.PopupNotifications.panel, + "popupshown" + ); +} diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.sys.mjs new file mode 100644 index 0000000000..d988ba88cf --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.sys.mjs @@ -0,0 +1,165 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Various parts here are run in the content process. +/* global content */ + +import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs"; + +export var Preferences = { + init(libDir) { + let panes = [ + ["paneGeneral"], + ["paneGeneral", browsingGroup], + ["paneGeneral", connectionDialog], + ["paneSearch"], + ["panePrivacy"], + ["panePrivacy", cacheGroup], + ["panePrivacy", clearRecentHistoryDialog], + ["panePrivacy", certManager], + ["panePrivacy", deviceManager], + ["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 = new Promise(r => browserWindow.requestAnimationFrame(r)); + } + } 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 connectionDialog(aBrowser) { + await aBrowser.ownerGlobal.SpecialPowers.spawn( + aBrowser, + [], + async function () { + content.document.getElementById("connectionSettings").click(); + } + ); +} + +async function clearRecentHistoryDialog(aBrowser) { + await aBrowser.ownerGlobal.SpecialPowers.spawn( + aBrowser, + [], + async function () { + content.document.getElementById("clearHistoryButton").click(); + } + ); +} + +async function certManager(aBrowser) { + await aBrowser.ownerGlobal.SpecialPowers.spawn( + aBrowser, + [], + async function () { + content.document.getElementById("viewCertificatesButton").click(); + } + ); +} + +async function deviceManager(aBrowser) { + await aBrowser.ownerGlobal.SpecialPowers.spawn( + aBrowser, + [], + async function () { + content.document.getElementById("viewSecurityDevicesButton").click(); + } + ); +} diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.sys.mjs new file mode 100644 index 0000000000..85b134b8bc --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.sys.mjs @@ -0,0 +1,219 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const CUST_TAB = "chrome://browser/skin/customize.svg"; +const PREFS_TAB = "chrome://global/skin/icons/settings.svg"; +const DEFAULT_FAVICON_TAB = `data:text/html,<meta%20charset="utf-8"><title>No%20favicon</title>`; + +import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; +import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs"; + +export var Tabs = { + init(libDir) {}, + + configurations: { + fiveTabs: { + selectors: ["#tabbrowser-tabs"], + async applyConfig() { + fiveTabsHelper(); + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + hoverTab(browserWindow.gBrowser.tabs[3]); + await new Promise((resolve, reject) => { + setTimeout(resolve, 3000); + }); + await allTabTitlesDisplayed(browserWindow); + }, + }, + + fourPinned: { + selectors: ["#tabbrowser-tabs"], + async applyConfig() { + fiveTabsHelper(); + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + let tab = browserWindow.gBrowser.addTab(PREFS_TAB, { + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + }); + browserWindow.gBrowser.pinTab(tab); + tab = browserWindow.gBrowser.addTab(CUST_TAB, { + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + }); + browserWindow.gBrowser.pinTab(tab); + tab = browserWindow.gBrowser.addTab("about:privatebrowsing", { + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + }); + browserWindow.gBrowser.pinTab(tab); + tab = browserWindow.gBrowser.addTab("about:home", { + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + }); + browserWindow.gBrowser.pinTab(tab); + browserWindow.gBrowser.selectTabAtIndex(5); + hoverTab(browserWindow.gBrowser.tabs[2]); + // also hover the new tab button + let newTabButton = browserWindow.gBrowser.tabContainer.newTabButton; + hoverTab(newTabButton); + + await new Promise((resolve, reject) => { + setTimeout(resolve, 3000); + }); + await allTabTitlesDisplayed(browserWindow); + }, + }, + + twoPinnedWithOverflow: { + selectors: ["#tabbrowser-tabs"], + async applyConfig() { + fiveTabsHelper(); + + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + browserWindow.gBrowser.loadTabs( + [ + PREFS_TAB, + CUST_TAB, + "about:home", + DEFAULT_FAVICON_TAB, + "about:newtab", + "about:addons", + "about:home", + DEFAULT_FAVICON_TAB, + "about:newtab", + "about:addons", + "about:home", + DEFAULT_FAVICON_TAB, + "about:newtab", + "about:addons", + "about:home", + DEFAULT_FAVICON_TAB, + "about:newtab", + "about:addons", + "about:home", + DEFAULT_FAVICON_TAB, + "about:newtab", + "about:addons", + "about:home", + DEFAULT_FAVICON_TAB, + "about:newtab", + "about:addons", + "about:home", + DEFAULT_FAVICON_TAB, + "about:newtab", + ], + { + inBackground: true, + replace: true, + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + } + ); + browserWindow.gBrowser.pinTab(browserWindow.gBrowser.tabs[1]); + browserWindow.gBrowser.pinTab(browserWindow.gBrowser.tabs[2]); + browserWindow.gBrowser.selectTabAtIndex(3); + hoverTab(browserWindow.gBrowser.tabs[5]); + + await new Promise((resolve, reject) => { + setTimeout(resolve, 3000); + }); + + // Make sure the tabstrip is scrolled all the way to the left. + browserWindow.gBrowser.tabContainer.arrowScrollbox.scrollByIndex( + -100, + true + ); + + await allTabTitlesDisplayed(browserWindow); + }, + }, + }, +}; + +/* helpers */ + +async function allTabTitlesDisplayed(browserWindow) { + let specToTitleMap = { + "about:home": "New Tab", + "about:newtab": "New Tab", + "about:addons": "Add-ons Manager", + "about:privatebrowsing": "about:privatebrowsing", + }; + specToTitleMap[PREFS_TAB] = "global/skin/icons/settings.svg"; + specToTitleMap[CUST_TAB] = "browser/skin/customize.svg"; + specToTitleMap[DEFAULT_FAVICON_TAB] = "No favicon"; + + let tabTitlePromises = []; + for (let tab of browserWindow.gBrowser.tabs) { + function getSpec() { + return ( + tab.linkedBrowser && + tab.linkedBrowser.documentURI && + tab.linkedBrowser.documentURI.spec + ); + } + function tabTitleLoaded() { + let spec = getSpec(); + return spec ? tab.label == specToTitleMap[spec] : false; + } + let promise = TestUtils.waitForCondition( + tabTitleLoaded, + `Tab (${getSpec()}) should be showing "${ + specToTitleMap[getSpec()] + }". Got "${tab.label}"` + ); + tabTitlePromises.push(promise); + } + + return Promise.all(tabTitlePromises); +} + +function fiveTabsHelper() { + // some with no favicon and some with. Selected tab in middle. + closeAllButOneTab("about:addons"); + + let browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); + browserWindow.gBrowser.loadTabs( + [ + "about:addons", + "about:home", + DEFAULT_FAVICON_TAB, + "about:newtab", + CUST_TAB, + ], + { + inBackground: true, + replace: true, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + } + ); + browserWindow.gBrowser.selectTabAtIndex(1); +} + +function closeAllButOneTab(url = "about:blank") { + let browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); + let gBrowser = browserWindow.gBrowser; + // Close all tabs except the last so we don't quit the browser. + while (gBrowser.tabs.length > 1) { + gBrowser.removeTab(gBrowser.selectedTab, { animate: false }); + } + gBrowser.selectedBrowser.loadURI(Services.io.newURI(url), { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + if (gBrowser.selectedTab.pinned) { + gBrowser.unpinTab(gBrowser.selectedTab); + } + let newTabButton = gBrowser.tabContainer.newTabButton; + hoverTab(newTabButton, false); +} + +function hoverTab(tab, hover = true) { + if (hover) { + InspectorUtils.addPseudoClassLock(tab, ":hover"); + } else { + InspectorUtils.clearPseudoClassLocks(tab); + } +} diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.sys.mjs new file mode 100644 index 0000000000..bc5b12c219 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.sys.mjs @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const PREF_TABS_IN_TITLEBAR = "browser.tabs.inTitlebar"; + +export var TabsInTitlebar = { + init(libDir) {}, + + configurations: { + tabsInTitlebar: { + selectors: ["#navigator-toolbox"], + async applyConfig() { + Services.prefs.setIntPref(PREF_TABS_IN_TITLEBAR, 1); + return undefined; + }, + }, + + tabsOutsideTitlebar: { + selectors: ["#navigator-toolbox"].concat( + Services.appinfo.OS == "Linux" ? [] : ["#titlebar"] + ), + async applyConfig() { + Services.prefs.setIntPref(PREF_TABS_IN_TITLEBAR, 0); + }, + }, + }, +}; diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.sys.mjs new file mode 100644 index 0000000000..06b1159a5e --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.sys.mjs @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export var Toolbars = { + init(libDir) {}, + + configurations: { + onlyNavBar: { + selectors: ["#navigator-toolbox"], + async applyConfig() { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + let personalToolbar = + browserWindow.document.getElementById("PersonalToolbar"); + browserWindow.setToolbarVisibility(personalToolbar, false); + toggleMenubarIfNecessary(false); + }, + }, + + allToolbars: { + selectors: ["#navigator-toolbox"], + async applyConfig() { + // Boookmarks and menubar + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + let personalToolbar = + browserWindow.document.getElementById("PersonalToolbar"); + browserWindow.setToolbarVisibility(personalToolbar, true); + toggleMenubarIfNecessary(true); + }, + + async verifyConfig() { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + if (browserWindow.fullScreen) { + return "The bookmark toolbar and menubar are not shown in fullscreen."; + } + return undefined; + }, + }, + }, +}; + +// helpers + +function toggleMenubarIfNecessary(visible) { + let browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); + // The menubar is not shown on OS X or while in fullScreen + if (Services.appinfo.OS != "Darwin" /* && !browserWindow.fullScreen*/) { + let menubar = browserWindow.document.getElementById("toolbar-menubar"); + browserWindow.setToolbarVisibility(menubar, visible); + } +} diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/UIDensities.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/UIDensities.sys.mjs new file mode 100644 index 0000000000..37cc123727 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/UIDensities.sys.mjs @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export var UIDensities = { + init(libDir) {}, + + configurations: { + compactDensity: { + selectors: ["#navigator-toolbox", "#appMenu-popup", "#widget-overflow"], + async applyConfig() { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + browserWindow.gCustomizeMode.setUIDensity( + browserWindow.gUIDensity.MODE_COMPACT + ); + }, + }, + + normalDensity: { + selectors: ["#navigator-toolbox", "#appMenu-popup", "#widget-overflow"], + async applyConfig() { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + browserWindow.gCustomizeMode.setUIDensity( + browserWindow.gUIDensity.MODE_NORMAL + ); + }, + }, + + touchDensity: { + selectors: ["#navigator-toolbox", "#appMenu-popup", "#widget-overflow"], + async applyConfig() { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + browserWindow.gCustomizeMode.setUIDensity( + browserWindow.gUIDensity.MODE_TOUCH + ); + }, + }, + }, +}; diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.sys.mjs new file mode 100644 index 0000000000..98a4e3ec00 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.sys.mjs @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; +import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs"; + +export var WindowSize = { + init(libDir) { + Services.prefs.setBoolPref("browser.fullscreen.autohide", false); + }, + + configurations: { + maximized: { + selectors: [":root"], + async applyConfig() { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + await toggleFullScreen(browserWindow, false); + + // Wait for the Lion fullscreen transition to end as there doesn't seem to be an event + // and trying to maximize while still leaving fullscreen doesn't work. + await new Promise((resolve, reject) => { + setTimeout(function waitToLeaveFS() { + browserWindow.maximize(); + resolve(); + }, 5000); + }); + }, + }, + + normal: { + selectors: [":root"], + async applyConfig() { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + await toggleFullScreen(browserWindow, false); + browserWindow.restore(); + await new Promise((resolve, reject) => { + setTimeout(resolve, 5000); + }); + }, + }, + + fullScreen: { + selectors: [":root"], + async applyConfig() { + let browserWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + await toggleFullScreen(browserWindow, true); + // OS X Lion fullscreen transition takes a while + await new Promise((resolve, reject) => { + setTimeout(resolve, 5000); + }); + }, + }, + }, +}; + +function toggleFullScreen(browserWindow, wantsFS) { + browserWindow.fullScreen = wantsFS; + return BrowserTestUtils.waitForCondition(() => { + return ( + wantsFS == + browserWindow.document.documentElement.hasAttribute("inFullscreen") + ); + }, "waiting for @inFullscreen change"); +} diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/borderify.xpi b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/borderify.xpi 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..4ce8e78dc4 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed_active.html @@ -0,0 +1,14 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf8" /> + <title>Mixed Active Content test</title> + </head> + <body> + <iframe style="visibility: hidden" src="http://example.com"></iframe> + </body> +</html> diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed_passive.html b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed_passive.html new file mode 100644 index 0000000000..a8b74e08b2 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/mixed_passive.html @@ -0,0 +1,14 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf8"> + <title>Mixed Passive Content test</title> + </head> + <body> + <img style="visibility:hidden" src="http://example.com/tests/image/test/mochitest/blue.png"></img> + </body> +</html> diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/password.html b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/password.html new file mode 100644 index 0000000000..c51ee25732 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/password.html @@ -0,0 +1,17 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf8" /> + <title>HTTP Password test</title> + </head> + <body> + <form> + <input type="password" /> + <button type="submit">Submit</button> + </form> + </body> +</html> diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/tracking.html b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/tracking.html new file mode 100644 index 0000000000..8ef07c3e73 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/controlCenter/tracking.html @@ -0,0 +1,17 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf8" /> + <title>Tracking test</title> + </head> + <body> + <iframe + style="visibility: hidden" + src="http://tracking.example.com/" + ></iframe> + </body> +</html> diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-script.js b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-script.js new file mode 100644 index 0000000000..808c08c8d3 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-script.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +console.log(document, document.body); +console.assert(false, "Failing mozscreenshots assertion"); + +console.group("Grouped Message"); +console.log("group message 1"); +console.groupEnd(); + +console.count("counter"); +console.count("counter"); + +console.log("first", { a: 1 }, "second", { b: "hello" }, "third", { + c: new Map(), +}); +console.log("first", { a: 1 }, "second", { b: "hello" }); +console.log("first", { a: 1 }, "\nsecond", { b: "hello" }); +console.log("first", "\nsecond"); +console.log("\nfirst", "second"); diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-style.css b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-style.css new file mode 100644 index 0000000000..145a4e57d4 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-style.css @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +body { + --background-color: #f3f3f3; + background: tomato; +} + +body { + background: var(--background-color); + color: #222; + padding: 0 10px; + margin: 0; +} + +header { + background: #eee; + border-bottom: solid 2px #ccc; + margin: 0 -10px; + margin-bottom: 5px; + padding: 4px; +} + +header h1 { + margin: 0; + padding: 0; +} diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots.html b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots.html new file mode 100644 index 0000000000..494bdba159 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots.html @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html> + <head> + <meta charset="UTF-8" /> + <title>mozscreenshots</title> + <link rel="stylesheet" href="mozscreenshots-style.css" /> + <script> + console.info("This page was generated by mozscreenshots"); + </script> + </head> + + <body> + <header><h1>mozscreenshots</h1></header> + + <p>This page was generated by mozscreenshots</p> + + <img src="robot.png" /> + + <p> + Welcome Humans! We invite others to keep the Manifesto’s principles; use + the creation delivery and commitment. Advancing the Manifesto. We have + shiny metal posteriors which <strong>should not be bitten</strong>. And + they have distilled a public benefit is committed to use Mozilla project + one basic and communities of modern life. + </p> + + <p> + Robots have + <mark>distilled a balance between commercial aspects of life</mark>. + </p> + + <p> + Robots may not they have shown in education communication collaboration + business entertainment and other people to pursue; speak to continue; and + opportunity are many different ways to benefit the public benefit the + Manifesto’s principles; build and motivate us and trademarks + infrastructure funds and trademarks infrastructure funds and enable + open-source technologies and provide a whole. + </p> + + <p> + And they have seen things you people who believe that Mozilla Manifesto. + We are to: articulate a vision of individual human being to benefit the + lives of these efforts we will: build and promote models for creating + economic value for the Internet. We create world-class open and anticipate + the Mozilla Manifesto + <strong>There are Your Plastic Pal Who's Fun To Be With</strong> + </p> + + <p> + Some Foundation to advance this vision of individual human being or not + deeply involved in groups and promote models for the Manifesto principles + will not come to support the Mozilla Foundation Pledge The Mozilla + Manifesto in its activities. People are to: articulate a set of consumer + products that support <mark>the Internet is a human being</mark> or not be + treated as individuals working together in the development of the Internet + open and with goodwill!Specifically we believe that we will: build and + society as a public good as a result of the lives of collaborative + activities. Specifically we have seen things you people acting as + optional. Individuals must not come to continue to develop new ways of the + Internet are fundamental and with us to ensure that openness innovation + and very effective way that the Manifesto There are many benefits; a + global public benefit; and society as optional. + </p> + + <p> + <strong + >We have metal posteriors which should not deeply involved in a + reality.</strong + > + Individuals must remain open source software promotes the Internet is a + balance between commercial profit and within the Mozilla Corporation. + Invitation The Internet are key to join us to life on their own. The + Internet a whole. The Internet as a vision of the Mozilla contributors + proud of time attention and motivate us and provide a reality. + </p> + + <script src="mozscreenshots-script.js"></script> + </body> +</html> diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/permissionPrompts.html b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/permissionPrompts.html new file mode 100644 index 0000000000..92ec9805b5 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/permissionPrompts.html @@ -0,0 +1,64 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <title>Permission Prompts</title> + </head> + <body> + <button + id="geo" + onclick="navigator.geolocation.getCurrentPosition(() => {})" + > + Geolocation + </button> + <button id="xr" onclick="content.navigator.getVRDisplays();">WebXR</button> + <button id="persistent-storage" onclick="navigator.storage.persist()"> + Persistent Storage + </button> + <button + id="webRTC-shareDevices" + onclick="shareDevice({video: true, fake: true});" + > + Video + </button> + <button + id="webRTC-shareMicrophone" + onclick="shareDevice({audio: true, fake: true});" + > + Audio + </button> + <button + id="webRTC-shareDevices2" + onclick="shareDevice({audio: true, video: true, fake: true});" + > + Audio and Video + </button> + <button + id="webRTC-shareScreen" + onclick="shareDevice({video: {mediaSource: 'screen'}});" + > + Screen + </button> + <button id="web-notifications" onclick="Notification.requestPermission()"> + web-notifications + </button> + <a id="addons" href="borderify.xpi">Install Add-On</a> + <form> + <input type="email" id="email" value="email@example.com" /> + <input type="password" id="password" value="" /> + <button type="submit" id="login-capture">Login</button> + </form> + + <script> + // Share device used in onclick calls above. + /* exported shareDevice */ + function shareDevice(config) { + navigator.mediaDevices.getUserMedia(config); + } + </script> + </body> +</html> diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot.png b/browser/tools/mozscreenshots/mozscreenshots/extension/lib/robot.png 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..375bb96f47 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/manifest.json @@ -0,0 +1,23 @@ +{ + "manifest_version": 2, + "name": "mozscreenshots", + "description": "Take screenshots of Mozilla applications in various UI configurations.", + "version": "1.0", + + "browser_specific_settings": { + "gecko": { + "id": "mozscreenshots@mozilla.org" + } + }, + + "experiment_apis": { + "mozscreenshots": { + "schema": "schema.json", + "parent": { + "scopes": ["addon_parent"], + "script": "api.js", + "events": ["startup"] + } + } + } +} diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/moz.build b/browser/tools/mozscreenshots/mozscreenshots/extension/moz.build new file mode 100644 index 0000000000..173f6e6dae --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/moz.build @@ -0,0 +1,58 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPI_NAME = "mozscreenshots" + +USE_EXTENSION_MANIFEST = True + +FINAL_TARGET_FILES += [ + "api.js", + "manifest.json", + "schema.json", +] + +FINAL_TARGET_FILES.resources += [ + "Screenshot.sys.mjs", + "TestRunner.sys.mjs", +] + +FINAL_TARGET_FILES.resources.configurations += [ + "configurations/AppMenu.sys.mjs", + "configurations/Buttons.sys.mjs", + "configurations/ControlCenter.sys.mjs", + "configurations/CustomizeMode.sys.mjs", + "configurations/DevTools.sys.mjs", + "configurations/LightweightThemes.sys.mjs", + "configurations/PermissionPrompts.sys.mjs", + "configurations/Preferences.sys.mjs", + "configurations/Tabs.sys.mjs", + "configurations/TabsInTitlebar.sys.mjs", + "configurations/Toolbars.sys.mjs", + "configurations/UIDensities.sys.mjs", + "configurations/WindowSize.sys.mjs", +] + +FINAL_TARGET_FILES.resources.lib += [ + "lib/borderify.xpi", + "lib/mozscreenshots-script.js", + "lib/mozscreenshots-style.css", + "lib/mozscreenshots.html", + "lib/permissionPrompts.html", + "lib/robot.png", + "lib/robot_center.png", + "lib/robot_cropped_diagonal.png", + "lib/robot_diagonal.png", + "lib/robot_uncropped.png", + "lib/robot_upperleft.png", +] + +FINAL_TARGET_FILES.resources.lib.controlCenter += [ + "lib/controlCenter/mixed.html", + "lib/controlCenter/mixed_active.html", + "lib/controlCenter/mixed_passive.html", + "lib/controlCenter/password.html", + "lib/controlCenter/tracking.html", +] diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/schema.json b/browser/tools/mozscreenshots/mozscreenshots/extension/schema.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/schema.json @@ -0,0 +1 @@ +[] diff --git a/browser/tools/mozscreenshots/permissionPrompts/browser.toml b/browser/tools/mozscreenshots/permissionPrompts/browser.toml new file mode 100644 index 0000000000..0ddc6302ed --- /dev/null +++ b/browser/tools/mozscreenshots/permissionPrompts/browser.toml @@ -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.toml b/browser/tools/mozscreenshots/preferences/browser.toml new file mode 100644 index 0000000000..e369728681 --- /dev/null +++ b/browser/tools/mozscreenshots/preferences/browser.toml @@ -0,0 +1,6 @@ +[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.toml b/browser/tools/mozscreenshots/primaryUI/browser.toml new file mode 100644 index 0000000000..db83048498 --- /dev/null +++ b/browser/tools/mozscreenshots/primaryUI/browser.toml @@ -0,0 +1,6 @@ +[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.toml b/browser/tools/mozscreenshots/tests/browser/browser.toml new file mode 100644 index 0000000000..23be01d935 --- /dev/null +++ b/browser/tools/mozscreenshots/tests/browser/browser.toml @@ -0,0 +1,9 @@ +[DEFAULT] +subsuite = "screenshots" +support-files = ["../../head.js"] + +["browser_boundingbox.js"] + +["browser_screenshots.js"] + +["browser_screenshots_cropping.js"] diff --git a/browser/tools/mozscreenshots/tests/browser/browser_boundingbox.js b/browser/tools/mozscreenshots/tests/browser/browser_boundingbox.js new file mode 100644 index 0000000000..cef3fa7de1 --- /dev/null +++ b/browser/tools/mozscreenshots/tests/browser/browser_boundingbox.js @@ -0,0 +1,173 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +/* import-globals-from ../../head.js */ + +add_task(async function () { + const scale = window.docShell.QueryInterface( + Ci.nsIBaseWindow + ).devicePixelsPerDesktopPixel; + let { bounds, rects } = TestRunner._findBoundingBox(["#tabbrowser-tabs"]); + let tabBar = document.querySelector("#tabbrowser-tabs"); + let tabBarRect = tabBar.getBoundingClientRect(); + + // Calculate expected values + let expectedLeft = scale * (tabBar.screenX - TestRunner.croppingPadding); + let expectedTop = scale * (tabBar.screenY - TestRunner.croppingPadding); + let expectedRight = + scale * (tabBarRect.width + TestRunner.croppingPadding * 2) + expectedLeft; + let expectedBottom = + scale * (tabBarRect.height + TestRunner.croppingPadding * 2) + expectedTop; + + // Calculate browser region + let windowLeft = window.screenX * scale; + let windowTop = window.screenY * scale; + let windowRight = window.outerWidth * scale + windowLeft; + let windowBottom = window.outerHeight * scale + windowTop; + + // Adjust values based on browser window + expectedLeft = Math.max(expectedLeft, windowLeft); + expectedTop = Math.max(expectedTop, windowTop); + expectedRight = Math.min(expectedRight, windowRight); + expectedBottom = Math.min(expectedBottom, windowBottom); + // Check width calculation on simple example + is( + bounds.width, + expectedRight - expectedLeft, + "Checking _findBoundingBox width calculation" + ); + // Check height calculation on simple example + is( + bounds.height, + expectedBottom - expectedTop, + "Checking _findBoundingBox height caclulation" + ); + is( + bounds.left, + rects[0].left, + "Checking _findBoundingBox union.left and rect.left is the same for a single selector" + ); + is( + bounds.right, + rects[0].right, + "Checking _findBoundingBox union.right and rect.right is the same for a single selector" + ); + is( + bounds.top, + rects[0].top, + "Checking _findBoundingBox union.top and rect.top is the same for a single selector" + ); + is( + bounds.bottom, + rects[0].bottom, + "Checking _findBoundingBox union.bottom and rect.bottom is the same for a single selector" + ); + + let result = TestRunner._findBoundingBox(["#forward-button", "#TabsToolbar"]); + bounds = result.bounds; + rects = result.rects; + + let tabToolbar = document.querySelector("#TabsToolbar"); + let tabToolbarRect = tabToolbar.getBoundingClientRect(); + let fButton = document.querySelector("#forward-button"); + let fButtonRect = fButton.getBoundingClientRect(); + + // Calculate expected values + expectedLeft = + scale * + (Math.min(tabToolbar.screenX, fButton.screenX) - + TestRunner.croppingPadding); + expectedTop = + scale * + (Math.min(tabToolbar.screenY, fButton.screenY) - + TestRunner.croppingPadding); + expectedRight = + scale * + (Math.max( + tabToolbarRect.width + tabToolbar.screenX, + fButtonRect.width + fButton.screenX + ) + + TestRunner.croppingPadding); + expectedBottom = + scale * + (Math.max( + tabToolbarRect.height + tabToolbar.screenY, + fButtonRect.height + fButton.screenY + ) + + TestRunner.croppingPadding); + + // Adjust values based on browser window + expectedLeft = Math.max(expectedLeft, windowLeft); + expectedTop = Math.max(expectedTop, windowTop); + expectedRight = Math.min(expectedRight, windowRight); + expectedBottom = Math.min(expectedBottom, windowBottom); + + // Check width calculation on union + is( + bounds.width, + expectedRight - expectedLeft, + "Checking _findBoundingBox union width calculation" + ); + // Check height calculation on union + is( + bounds.height, + expectedBottom - expectedTop, + "Checking _findBoundingBox union height calculation" + ); + // Check single selector's left position + is( + rects[0].left, + Math.max( + scale * (fButton.screenX - TestRunner.croppingPadding), + windowLeft + ), + "Checking single selector's left position when _findBoundingBox has multiple selectors" + ); + // Check single selector's right position + is( + rects[0].right, + Math.min( + scale * + (fButtonRect.width + fButton.screenX + TestRunner.croppingPadding), + windowRight + ), + "Checking single selector's right position when _findBoundingBox has multiple selectors" + ); + // Check single selector's top position + is( + rects[0].top, + Math.max(scale * (fButton.screenY - TestRunner.croppingPadding), windowTop), + "Checking single selector's top position when _findBoundingBox has multiple selectors" + ); + // Check single selector's bottom position + is( + rects[0].bottom, + Math.min( + scale * + (fButtonRect.height + fButton.screenY + TestRunner.croppingPadding), + windowBottom + ), + "Checking single selector's bottom position when _findBoundingBox has multiple selectors" + ); + + // Check that nonexistent selectors throws an exception + Assert.throws( + () => { + TestRunner._findBoundingBox(["#does_not_exist"]); + }, + /No element for '#does_not_exist' found/, + "Checking that nonexistent selectors throws an exception" + ); + + // Check that no selectors throws an exception + Assert.throws( + () => { + TestRunner._findBoundingBox([]); + }, + /No selectors specified/, + "Checking that no selectors throws an exception" + ); +}); diff --git a/browser/tools/mozscreenshots/tests/browser/browser_screenshots.js b/browser/tools/mozscreenshots/tests/browser/browser_screenshots.js new file mode 100644 index 0000000000..88f7c6a704 --- /dev/null +++ b/browser/tools/mozscreenshots/tests/browser/browser_screenshots.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../head.js */ + +add_task(async function capture() { + let setsEnv = Services.env.get("MOZSCREENSHOTS_SETS"); + if (!setsEnv) { + ok( + true, + "MOZSCREENSHOTS_SETS wasn't specified so there's nothing to capture" + ); + return; + } + + let sets = TestRunner.splitEnv(setsEnv.trim()); + await TestRunner.start(sets); +}); diff --git a/browser/tools/mozscreenshots/tests/browser/browser_screenshots_cropping.js b/browser/tools/mozscreenshots/tests/browser/browser_screenshots_cropping.js new file mode 100644 index 0000000000..80fcde5d68 --- /dev/null +++ b/browser/tools/mozscreenshots/tests/browser/browser_screenshots_cropping.js @@ -0,0 +1,153 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../head.js */ + +const { Rect } = ChromeUtils.importESModule( + "resource://gre/modules/Geometry.sys.mjs" +); + +async function draw(window, src) { + const { document, Image } = window; + + const promise = new Promise((resolve, reject) => { + const img = new Image(); + + img.onload = function () { + // Create a new offscreen canvas + const canvas = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "canvas" + ); + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; + const ctx = canvas.getContext("2d"); + + ctx.drawImage(img, 0, 0); + + resolve(canvas); + }; + + img.onerror = function () { + reject(`error loading image ${src}`); + }; + + // Load the src image for drawing + img.src = src; + }); + + return promise; +} + +async function compareImages(window, expected, test) { + const testCanvas = await draw(window, test); + const expectedCanvas = await draw(window, expected); + + is( + testCanvas.width, + expectedCanvas.width, + "The test and expected images must be the same size" + ); + is( + testCanvas.height, + expectedCanvas.height, + "The test and expected images must be the same size" + ); + + var maxDifference = {}; + var differences = window.windowUtils.compareCanvases( + expectedCanvas, + testCanvas, + maxDifference + ); + + // Fuzz for minor differences that can be caused by the encoder. + if (maxDifference.value > 1) { + return differences; + } + return 0; +} + +async function cropAndCompare(window, src, expected, test, region, subregions) { + await TestRunner._cropImage(window, src, region, subregions, test); + + return compareImages(window, expected, PathUtils.toFileURI(test)); +} + +add_task(async function crop() { + const window = Services.wm.getMostRecentWindow("navigator:browser"); + + const tmp = PathUtils.tempDir; + is( + await cropAndCompare( + window, + "resource://mozscreenshots/lib/robot.png", + "resource://mozscreenshots/lib/robot_upperleft.png", + PathUtils.join(tmp, "test_cropped_upperleft.png"), + new Rect(0, 0, 32, 32), + [new Rect(0, 0, 32, 32)] + ), + 0, + "The image should be cropped to the upper left quadrant" + ); + + is( + await cropAndCompare( + window, + "resource://mozscreenshots/lib/robot.png", + "resource://mozscreenshots/lib/robot_center.png", + PathUtils.join(tmp, "test_cropped_center.png"), + new Rect(16, 16, 32, 32), + [new Rect(16, 16, 32, 32)] + ), + 0, + "The image should be cropped to the center of the image" + ); + + is( + await cropAndCompare( + window, + "resource://mozscreenshots/lib/robot.png", + "resource://mozscreenshots/lib/robot_uncropped.png", + PathUtils.join(tmp, "test_uncropped.png"), + new Rect(-8, -9, 80, 80), + [new Rect(-8, -9, 80, 80)] + ), + 0, + "The image should be not be cropped, and the cropping region should be clipped to the size of the image" + ); + + is( + await cropAndCompare( + window, + "resource://mozscreenshots/lib/robot.png", + "resource://mozscreenshots/lib/robot_diagonal.png", + PathUtils.join(tmp, "test_diagonal.png"), + new Rect(0, 0, 64, 64), + [ + new Rect(0, 0, 16, 16), + new Rect(16, 16, 16, 16), + new Rect(32, 32, 16, 16), + new Rect(48, 48, 16, 16), + ] + ), + 0, + "The image should be contain squares across the diagonal" + ); + + is( + await cropAndCompare( + window, + "resource://mozscreenshots/lib/robot.png", + "resource://mozscreenshots/lib/robot_cropped_diagonal.png", + PathUtils.join(tmp, "test_cropped_diagonal.png"), + new Rect(16, 16, 48, 48), + [new Rect(16, 16, 16, 16), new Rect(32, 32, 16, 16)] + ), + 0, + "The image should be cropped with squares across the diagonal" + ); +}); diff --git a/browser/tools/mozscreenshots/tests/xpcshell/test_testConfigurations.js b/browser/tools/mozscreenshots/tests/xpcshell/test_testConfigurations.js new file mode 100644 index 0000000000..8461e38939 --- /dev/null +++ b/browser/tools/mozscreenshots/tests/xpcshell/test_testConfigurations.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { TestRunner } = ChromeUtils.importESModule( + "resource://test/TestRunner.sys.mjs" +); + +add_task(async function capture() { + equal(TestRunner.findComma("Toolbars,Devs"), 8); + equal(TestRunner.findComma("Toolbars"), -1); + equal(TestRunner.findComma("Toolbars[onlyNavBar,allToolbars],DevTools"), 32); + equal( + TestRunner.findComma( + "Toolbars[onlyNavBar,allToolbars],DevTools[bottomToolbox,sideToolbox]" + ), + 32 + ); + equal( + TestRunner.findComma( + "Toolbars[[onlyNavBar],[]], Tabs[ [fiveTabbed], [[[fourPinned]]] ]" + ), + 25 + ); + equal(TestRunner.findComma("[[[[[[[[[[[[[[[[[[[[]]"), -1); + equal(TestRunner.findComma("Preferences[[[[[,]]]]]"), -1); + + deepEqual(TestRunner.splitEnv("Toolbars"), ["Toolbars"]); + deepEqual(TestRunner.splitEnv("Buttons,Tabs"), ["Buttons", "Tabs"]); + deepEqual(TestRunner.splitEnv("Buttons, Tabs"), ["Buttons", "Tabs"]); + deepEqual(TestRunner.splitEnv(" Buttons , Tabs "), [ + "Buttons", + "Tabs", + ]); + deepEqual(TestRunner.splitEnv("Toolbars[onlyNavBar,allToolbars],DevTools"), [ + "Toolbars[onlyNavBar,allToolbars]", + "DevTools", + ]); + deepEqual( + TestRunner.splitEnv( + "Toolbars[onlyNavBar,allToolbars],DevTools[bottomToolbox]" + ), + ["Toolbars[onlyNavBar,allToolbars]", "DevTools[bottomToolbox]"] + ); + deepEqual( + TestRunner.splitEnv( + "Toolbars[onlyNavBar,allToolbars],DevTools[bottomToolbox],Tabs" + ), + ["Toolbars[onlyNavBar,allToolbars]", "DevTools[bottomToolbox]", "Tabs"] + ); + + let filteredData = TestRunner.filterRestrictions("Toolbars[onlyNavBar]"); + equal(filteredData.trimmedSetName, "Toolbars"); + ok(filteredData.restrictions.has("onlyNavBar")); + + filteredData = TestRunner.filterRestrictions( + "DevTools[bottomToolbox,sideToolbox]" + ); + equal(filteredData.trimmedSetName, "DevTools"); + ok(filteredData.restrictions.has("bottomToolbox")); + ok(filteredData.restrictions.has("sideToolbox")); +}); diff --git a/browser/tools/mozscreenshots/tests/xpcshell/xpcshell.toml b/browser/tools/mozscreenshots/tests/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..16a78095bf --- /dev/null +++ b/browser/tools/mozscreenshots/tests/xpcshell/xpcshell.toml @@ -0,0 +1,5 @@ +[DEFAULT] +skip-if = ["os == 'android'"] # bug 1730213 +support-files = ["../../mozscreenshots/extension/TestRunner.sys.mjs"] + +["test_testConfigurations.js"] |