summaryrefslogtreecommitdiffstats
path: root/browser/components/screenshots
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /browser/components/screenshots
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/screenshots')
-rw-r--r--browser/components/screenshots/ScreenshotsHelperChild.sys.mjs47
-rw-r--r--browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs1593
-rw-r--r--browser/components/screenshots/ScreenshotsUtils.sys.mjs993
-rw-r--r--browser/components/screenshots/content/cancel.svg4
-rw-r--r--browser/components/screenshots/content/copied-notification.svg4
-rw-r--r--browser/components/screenshots/content/copy.svg4
-rw-r--r--browser/components/screenshots/content/download-white.svg4
-rw-r--r--browser/components/screenshots/content/download.svg4
-rw-r--r--browser/components/screenshots/content/icon-welcome-face-without-eyes.svg4
-rw-r--r--browser/components/screenshots/content/menu-fullpage.svg4
-rw-r--r--browser/components/screenshots/content/menu-visible.svg4
-rw-r--r--browser/components/screenshots/content/screenshots.css68
-rw-r--r--browser/components/screenshots/content/screenshots.html68
-rw-r--r--browser/components/screenshots/content/screenshots.js105
-rw-r--r--browser/components/screenshots/fileHelpers.mjs269
-rw-r--r--browser/components/screenshots/jar.mn24
-rw-r--r--browser/components/screenshots/moz.build20
-rw-r--r--browser/components/screenshots/overlay/overlay.css349
-rw-r--r--browser/components/screenshots/overlayHelpers.mjs497
-rw-r--r--browser/components/screenshots/screenshots-buttons.css35
-rw-r--r--browser/components/screenshots/screenshots-buttons.js68
-rw-r--r--browser/components/screenshots/tests/browser/browser.toml55
-rw-r--r--browser/components/screenshots/tests/browser/browser_iframe_test.js123
-rw-r--r--browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js748
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_drag_scroll_test.js465
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_drag_test.js488
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js384
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_overlay_panel_sync.js74
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_page_unload.js42
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_short_page_test.js123
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js466
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js186
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_test_escape.js35
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js175
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_test_page_crash.js54
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_test_screenshot_too_big.js90
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_test_toggle_pref.js289
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_test_toolbar_button.js26
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js356
-rw-r--r--browser/components/screenshots/tests/browser/browser_test_element_picker.js56
-rw-r--r--browser/components/screenshots/tests/browser/browser_test_resize.js100
-rw-r--r--browser/components/screenshots/tests/browser/first-iframe.html23
-rw-r--r--browser/components/screenshots/tests/browser/head.js951
-rw-r--r--browser/components/screenshots/tests/browser/iframe-test-page.html23
-rw-r--r--browser/components/screenshots/tests/browser/large-test-page.html9
-rw-r--r--browser/components/screenshots/tests/browser/second-iframe.html18
-rw-r--r--browser/components/screenshots/tests/browser/short-test-page.html8
-rw-r--r--browser/components/screenshots/tests/browser/test-page-resize.html25
-rw-r--r--browser/components/screenshots/tests/browser/test-page.html27
49 files changed, 9587 insertions, 0 deletions
diff --git a/browser/components/screenshots/ScreenshotsHelperChild.sys.mjs b/browser/components/screenshots/ScreenshotsHelperChild.sys.mjs
new file mode 100644
index 0000000000..550c3fb13c
--- /dev/null
+++ b/browser/components/screenshots/ScreenshotsHelperChild.sys.mjs
@@ -0,0 +1,47 @@
+/* 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 {
+ getBestRectForElement,
+ getElementFromPoint,
+} from "chrome://browser/content/screenshots/overlayHelpers.mjs";
+
+/**
+ * This class is used to get the dimensions of hovered elements within iframes.
+ * The main content process cannot get the dimensions of elements within
+ * iframes so a message will be send to this actor to get the dimensions of the
+ * element for a given point inside the iframe.
+ */
+export class ScreenshotsHelperChild extends JSWindowActorChild {
+ receiveMessage(message) {
+ if (message.name === "ScreenshotsHelper:GetElementRectFromPoint") {
+ return this.getBestElementRectFromPoint(message.data);
+ }
+ return null;
+ }
+
+ async getBestElementRectFromPoint(data) {
+ let { x, y } = data;
+
+ x -= this.contentWindow.mozInnerScreenX;
+ y -= this.contentWindow.mozInnerScreenY;
+
+ let { ele, rect } = await getElementFromPoint(x, y, this.document);
+
+ if (!rect) {
+ rect = getBestRectForElement(ele, this.document);
+ }
+
+ if (rect) {
+ rect = {
+ left: rect.left + this.contentWindow.mozInnerScreenX,
+ right: rect.right + this.contentWindow.mozInnerScreenX,
+ top: rect.top + this.contentWindow.mozInnerScreenY,
+ bottom: rect.bottom + this.contentWindow.mozInnerScreenY,
+ };
+ }
+
+ return rect;
+ }
+}
diff --git a/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs b/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs
new file mode 100644
index 0000000000..5d96a46c88
--- /dev/null
+++ b/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs
@@ -0,0 +1,1593 @@
+/* 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/. */
+
+/**
+ * The Screenshots overlay is inserted into the document's
+ * anonymous content container (see dom/webidl/Document.webidl).
+ *
+ * This container gets cleared automatically when the document navigates.
+ *
+ * To retrieve the AnonymousContent instance, use the `content` getter.
+ */
+
+/*
+ * Below are the states of the screenshots overlay
+ * States:
+ * "crosshairs":
+ * Nothing has happened, and the crosshairs will follow the movement of the mouse
+ * "draggingReady":
+ * The user has pressed the mouse button, but hasn't moved enough to create a selection
+ * "dragging":
+ * The user has pressed down a mouse button, and is dragging out an area far enough to show a selection
+ * "selected":
+ * The user has selected an area
+ * "resizing":
+ * The user is resizing the selection
+ */
+
+import {
+ setMaxDetectHeight,
+ setMaxDetectWidth,
+ getBestRectForElement,
+ getElementFromPoint,
+ Region,
+ WindowDimensions,
+} from "chrome://browser/content/screenshots/overlayHelpers.mjs";
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const STATES = {
+ CROSSHAIRS: "crosshairs",
+ DRAGGING_READY: "draggingReady",
+ DRAGGING: "dragging",
+ SELECTED: "selected",
+ RESIZING: "resizing",
+};
+
+const lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, "overlayLocalization", () => {
+ return new Localization(["browser/screenshotsOverlay.ftl"], true);
+});
+
+const SCREENSHOTS_LAST_SAVED_METHOD_PREF =
+ "screenshots.browser.component.last-saved-method";
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "SCREENSHOTS_LAST_SAVED_METHOD",
+ SCREENSHOTS_LAST_SAVED_METHOD_PREF,
+ "download"
+);
+
+const REGION_CHANGE_THRESHOLD = 5;
+const SCROLL_BY_EDGE = 20;
+
+export class ScreenshotsOverlay {
+ #content;
+ #initialized = false;
+ #state = "";
+ #moverId;
+ #cachedEle;
+ #lastPageX;
+ #lastPageY;
+ #lastClientX;
+ #lastClientY;
+ #previousDimensions;
+ #methodsUsed;
+
+ get markup() {
+ let [cancel, instructions, download, copy] =
+ lazy.overlayLocalization.formatMessagesSync([
+ { id: "screenshots-overlay-cancel-button" },
+ { id: "screenshots-overlay-instructions" },
+ { id: "screenshots-overlay-download-button" },
+ { id: "screenshots-overlay-copy-button" },
+ ]);
+
+ return `
+ <template>
+ <link rel="stylesheet" href="chrome://browser/content/screenshots/overlay/overlay.css" />
+ <div id="screenshots-component">
+ <div id="preview-container" hidden>
+ <div class="face-container">
+ <div class="eye left"><div id="left-eye" class="eyeball"></div></div>
+ <div class="eye right"><div id="right-eye" class="eyeball"></div></div>
+ <div class="face"></div>
+ </div>
+ <div class="preview-instructions">${instructions.value}</div>
+ <button class="screenshots-button ghost-button" id="screenshots-cancel-button">${cancel.value}</button>
+ </div>
+ <div id="hover-highlight" hidden></div>
+ <div id="selection-container" hidden>
+ <div id="top-background" class="bghighlight"></div>
+ <div id="bottom-background" class="bghighlight"></div>
+ <div id="left-background" class="bghighlight"></div>
+ <div id="right-background" class="bghighlight"></div>
+ <div id="highlight" class="highlight" tabindex="0">
+ <div id="mover-topLeft" class="mover-target direction-topLeft" tabindex="0">
+ <div class="mover"></div>
+ </div>
+ <div id="mover-top" class="mover-target direction-top">
+ <div class="mover"></div>
+ </div>
+ <div id="mover-topRight" class="mover-target direction-topRight" tabindex="0">
+ <div class="mover"></div>
+ </div>
+ <div id="mover-left" class="mover-target direction-left">
+ <div class="mover"></div>
+ </div>
+ <div id="mover-right" class="mover-target direction-right">
+ <div class="mover"></div>
+ </div>
+ <div id="mover-bottomLeft" class="mover-target direction-bottomLeft" tabindex="0">
+ <div class="mover"></div>
+ </div>
+ <div id="mover-bottom" class="mover-target direction-bottom">
+ <div class="mover"></div>
+ </div>
+ <div id="mover-bottomRight" class="mover-target direction-bottomRight" tabindex="0">
+ <div class="mover"></div>
+ </div>
+ <div id="selection-size-container">
+ <span id="selection-size"></span>
+ </div>
+ </div>
+ </div>
+ <div id="buttons-container" hidden>
+ <div class="buttons-wrapper">
+ <button id="cancel" class="screenshots-button" title="${cancel.value}" aria-label="${cancel.value}" tabindex="0"><img/></button>
+ <button id="copy" class="screenshots-button" title="${copy.value}" aria-label="${copy.value}" tabindex="0"><img/>${copy.value}</button>
+ <button id="download" class="screenshots-button primary" title="${download.value}" aria-label="${download.value}" tabindex="0"><img/>${download.value}</button>
+ </div>
+ </div>
+ </div>
+ </template>`;
+ }
+
+ get fragment() {
+ if (!this.overlayTemplate) {
+ let parser = new DOMParser();
+ let doc = parser.parseFromString(this.markup, "text/html");
+ this.overlayTemplate = this.document.importNode(
+ doc.querySelector("template"),
+ true
+ );
+ }
+ let fragment = this.overlayTemplate.content.cloneNode(true);
+ return fragment;
+ }
+
+ get initialized() {
+ return this.#initialized;
+ }
+
+ get state() {
+ return this.#state;
+ }
+
+ get methodsUsed() {
+ return this.#methodsUsed;
+ }
+
+ constructor(contentDocument) {
+ this.document = contentDocument;
+ this.window = contentDocument.ownerGlobal;
+
+ this.windowDimensions = new WindowDimensions();
+ this.selectionRegion = new Region(this.windowDimensions);
+ this.hoverElementRegion = new Region(this.windowDimensions);
+ this.resetMethodsUsed();
+ }
+
+ get content() {
+ if (!this.#content || Cu.isDeadWrapper(this.#content)) {
+ return null;
+ }
+ return this.#content;
+ }
+
+ getElementById(id) {
+ return this.content.root.getElementById(id);
+ }
+
+ async initialize() {
+ if (this.initialized) {
+ return;
+ }
+
+ this.windowDimensions.reset();
+
+ this.#content = this.document.insertAnonymousContent();
+ this.#content.root.appendChild(this.fragment);
+
+ this.initializeElements();
+ await this.updateWindowDimensions();
+
+ this.#setState(STATES.CROSSHAIRS);
+
+ this.#initialized = true;
+ }
+
+ /**
+ * Get all the elements that will be used.
+ */
+ initializeElements() {
+ this.previewCancelButton = this.getElementById("screenshots-cancel-button");
+ this.cancelButton = this.getElementById("cancel");
+ this.copyButton = this.getElementById("copy");
+ this.downloadButton = this.getElementById("download");
+
+ this.previewContainer = this.getElementById("preview-container");
+ this.hoverElementContainer = this.getElementById("hover-highlight");
+ this.selectionContainer = this.getElementById("selection-container");
+ this.buttonsContainer = this.getElementById("buttons-container");
+ this.screenshotsContainer = this.getElementById("screenshots-component");
+
+ this.leftEye = this.getElementById("left-eye");
+ this.rightEye = this.getElementById("right-eye");
+
+ this.leftBackgroundEl = this.getElementById("left-background");
+ this.topBackgroundEl = this.getElementById("top-background");
+ this.rightBackgroundEl = this.getElementById("right-background");
+ this.bottomBackgroundEl = this.getElementById("bottom-background");
+ this.highlightEl = this.getElementById("highlight");
+
+ this.topLeftMover = this.getElementById("mover-topLeft");
+ this.topRightMover = this.getElementById("mover-topRight");
+ this.bottomLeftMover = this.getElementById("mover-bottomLeft");
+ this.bottomRightMover = this.getElementById("mover-bottomRight");
+
+ this.selectionSize = this.getElementById("selection-size");
+ }
+
+ /**
+ * Removes all event listeners and removes the overlay from the Anonymous Content
+ */
+ tearDown(options = {}) {
+ if (this.#content) {
+ if (!(options.doNotResetMethods === true)) {
+ this.resetMethodsUsed();
+ }
+ try {
+ this.document.removeAnonymousContent(this.#content);
+ } catch (e) {
+ // If the current window isn't the one the content was inserted into, this
+ // will fail, but that's fine.
+ }
+ }
+ this.#initialized = false;
+ this.#setState("");
+ }
+
+ resetMethodsUsed() {
+ this.#methodsUsed = {
+ element: 0,
+ region: 0,
+ move: 0,
+ resize: 0,
+ };
+ }
+
+ /**
+ * Returns the x and y coordinates of the event relative to both the
+ * viewport and the page.
+ * @param {Event} event The event
+ * @returns
+ * {
+ * clientX: The x position relative to the viewport
+ * clientY: The y position relative to the viewport
+ * pageX: The x position relative to the entire page
+ * pageY: The y position relative to the entire page
+ * }
+ */
+ getCoordinatesFromEvent(event) {
+ const { clientX, clientY, pageX, pageY } = event;
+
+ return { clientX, clientY, pageX, pageY };
+ }
+
+ handleEvent(event) {
+ if (event.button > 0) {
+ return;
+ }
+
+ switch (event.type) {
+ case "click":
+ this.handleClick(event);
+ break;
+ case "pointerdown":
+ this.handlePointerDown(event);
+ break;
+ case "pointermove":
+ this.handlePointerMove(event);
+ break;
+ case "pointerup":
+ this.handlePointerUp(event);
+ break;
+ case "keydown":
+ this.handleKeyDown(event);
+ break;
+ case "keyup":
+ this.handleKeyUp(event);
+ break;
+ }
+ }
+
+ handleClick(event) {
+ switch (event.originalTarget.id) {
+ case "screenshots-cancel-button":
+ case "cancel":
+ this.maybeCancelScreenshots();
+ break;
+ case "copy":
+ this.#dispatchEvent("Screenshots:Copy", {
+ region: this.selectionRegion.dimensions,
+ });
+ break;
+ case "download":
+ this.#dispatchEvent("Screenshots:Download", {
+ region: this.selectionRegion.dimensions,
+ });
+ break;
+ }
+ }
+
+ maybeCancelScreenshots() {
+ if (this.#state === STATES.CROSSHAIRS) {
+ this.#dispatchEvent("Screenshots:Close", {
+ reason: "overlay_cancel",
+ });
+ } else {
+ this.#setState(STATES.CROSSHAIRS);
+ }
+ }
+
+ /**
+ * Handles the pointerdown event depending on the state.
+ * Early return when a pointer down happens on a button.
+ * @param {Event} event The pointerown event
+ */
+ handlePointerDown(event) {
+ if (
+ event.originalTarget.id === "screenshots-cancel-button" ||
+ event.originalTarget.closest("#buttons-container") ===
+ this.buttonsContainer
+ ) {
+ event.stopPropagation();
+ return;
+ }
+
+ const { pageX, pageY } = this.getCoordinatesFromEvent(event);
+
+ switch (this.#state) {
+ case STATES.CROSSHAIRS: {
+ this.crosshairsDragStart(pageX, pageY);
+ break;
+ }
+ case STATES.SELECTED: {
+ this.selectedDragStart(pageX, pageY, event.originalTarget.id);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handles the pointermove event depending on the state
+ * @param {Event} event The pointermove event
+ */
+ handlePointerMove(event) {
+ const { pageX, pageY, clientX, clientY } =
+ this.getCoordinatesFromEvent(event);
+
+ switch (this.#state) {
+ case STATES.CROSSHAIRS: {
+ this.crosshairsMove(clientX, clientY);
+ break;
+ }
+ case STATES.DRAGGING_READY: {
+ this.draggingReadyDrag(pageX, pageY);
+ break;
+ }
+ case STATES.DRAGGING: {
+ this.draggingDrag(pageX, pageY);
+ break;
+ }
+ case STATES.RESIZING: {
+ this.resizingDrag(pageX, pageY);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handles the pointerup event depending on the state
+ * @param {Event} event The pointerup event
+ */
+ handlePointerUp(event) {
+ const { pageX, pageY, clientX, clientY } =
+ this.getCoordinatesFromEvent(event);
+
+ switch (this.#state) {
+ case STATES.DRAGGING_READY: {
+ this.draggingReadyDragEnd(pageX - clientX, pageY - clientY);
+ break;
+ }
+ case STATES.DRAGGING: {
+ this.draggingDragEnd(pageX, pageY, event.originalTarget.id);
+ break;
+ }
+ case STATES.RESIZING: {
+ this.resizingDragEnd(pageX, pageY);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handles when a keydown occurs in the screenshots component.
+ * @param {Event} event The keydown event
+ */
+ handleKeyDown(event) {
+ switch (event.key) {
+ case "ArrowLeft":
+ this.handleArrowLeftKeyDown(event);
+ break;
+ case "ArrowUp":
+ this.handleArrowUpKeyDown(event);
+ break;
+ case "ArrowRight":
+ this.handleArrowRightKeyDown(event);
+ break;
+ case "ArrowDown":
+ this.handleArrowDownKeyDown(event);
+ break;
+ case "Tab":
+ this.maybeLockFocus(event);
+ break;
+ case "Escape":
+ this.maybeCancelScreenshots();
+ break;
+ }
+ }
+
+ /**
+ * Gets the accel key depending on the platform.
+ * metaKey for macOS. ctrlKey for Windows and Linux.
+ * @param {Event} event The keydown event
+ * @returns {Boolean} True if the accel key is pressed, false otherwise.
+ */
+ getAccelKey(event) {
+ if (AppConstants.platform === "macosx") {
+ return event.metaKey;
+ }
+ return event.ctrlKey;
+ }
+
+ /**
+ * Move the region or its left or right side to the left.
+ * Just the arrow key will move the region by 1px.
+ * Arrow key + shift will move the region by 10px.
+ * Arrow key + control/meta will move to the edge of the window.
+ * @param {Event} event The keydown event
+ */
+ handleArrowLeftKeyDown(event) {
+ switch (event.originalTarget.id) {
+ case "highlight":
+ if (this.getAccelKey(event)) {
+ let width = this.selectionRegion.width;
+ this.selectionRegion.left = this.windowDimensions.scrollX;
+ this.selectionRegion.right = this.windowDimensions.scrollX + width;
+ break;
+ }
+
+ this.selectionRegion.right -= 10 ** event.shiftKey;
+ // eslint-disable-next-line no-fallthrough
+ case "mover-topLeft":
+ case "mover-bottomLeft":
+ if (this.getAccelKey(event)) {
+ this.selectionRegion.left = this.windowDimensions.scrollX;
+ break;
+ }
+
+ this.selectionRegion.left -= 10 ** event.shiftKey;
+ this.scrollIfByEdge(
+ this.selectionRegion.left,
+ this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2
+ );
+ break;
+ case "mover-topRight":
+ case "mover-bottomRight":
+ if (this.getAccelKey(event)) {
+ let left = this.selectionRegion.left;
+ this.selectionRegion.left = this.windowDimensions.scrollX;
+ this.selectionRegion.right = left;
+ if (event.originalTarget.id === "mover-topRight") {
+ this.topLeftMover.focus({ focusVisible: true });
+ } else if (event.originalTarget.id === "mover-bottomRight") {
+ this.bottomLeftMover.focus({ focusVisible: true });
+ }
+ break;
+ }
+
+ this.selectionRegion.right -= 10 ** event.shiftKey;
+ if (this.selectionRegion.x1 >= this.selectionRegion.x2) {
+ this.selectionRegion.sortCoords();
+ if (event.originalTarget.id === "mover-topRight") {
+ this.topLeftMover.focus({ focusVisible: true });
+ } else if (event.originalTarget.id === "mover-bottomRight") {
+ this.bottomLeftMover.focus({ focusVisible: true });
+ }
+ }
+ break;
+ default:
+ return;
+ }
+
+ if (this.#state !== STATES.RESIZING) {
+ this.#setState(STATES.RESIZING);
+ }
+
+ event.preventDefault();
+ this.drawSelectionContainer();
+ }
+
+ /**
+ * Move the region or its top or bottom side upward.
+ * Just the arrow key will move the region by 1px.
+ * Arrow key + shift will move the region by 10px.
+ * Arrow key + control/meta will move to the edge of the window.
+ * @param {Event} event The keydown event
+ */
+ handleArrowUpKeyDown(event) {
+ switch (event.originalTarget.id) {
+ case "highlight":
+ if (this.getAccelKey(event)) {
+ let height = this.selectionRegion.height;
+ this.selectionRegion.top = this.windowDimensions.scrollY;
+ this.selectionRegion.bottom = this.windowDimensions.scrollY + height;
+ break;
+ }
+
+ this.selectionRegion.bottom -= 10 ** event.shiftKey;
+ // eslint-disable-next-line no-fallthrough
+ case "mover-topLeft":
+ case "mover-topRight":
+ if (this.getAccelKey(event)) {
+ this.selectionRegion.top = this.windowDimensions.scrollY;
+ break;
+ }
+
+ this.selectionRegion.top -= 10 ** event.shiftKey;
+ this.scrollIfByEdge(
+ this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2,
+ this.selectionRegion.top
+ );
+ break;
+ case "mover-bottomLeft":
+ case "mover-bottomRight":
+ if (this.getAccelKey(event)) {
+ let top = this.selectionRegion.top;
+ this.selectionRegion.top = this.windowDimensions.scrollY;
+ this.selectionRegion.bottom = top;
+ if (event.originalTarget.id === "mover-bottomLeft") {
+ this.topLeftMover.focus({ focusVisible: true });
+ } else if (event.originalTarget.id === "mover-bottomRight") {
+ this.topRightMover.focus({ focusVisible: true });
+ }
+ break;
+ }
+
+ this.selectionRegion.bottom -= 10 ** event.shiftKey;
+ if (this.selectionRegion.y1 >= this.selectionRegion.y2) {
+ this.selectionRegion.sortCoords();
+ if (event.originalTarget.id === "mover-bottomLeft") {
+ this.topLeftMover.focus({ focusVisible: true });
+ } else if (event.originalTarget.id === "mover-bottomRight") {
+ this.topRightMover.focus({ focusVisible: true });
+ }
+ }
+ break;
+ default:
+ return;
+ }
+
+ if (this.#state !== STATES.RESIZING) {
+ this.#setState(STATES.RESIZING);
+ }
+
+ event.preventDefault();
+ this.drawSelectionContainer();
+ }
+
+ /**
+ * Move the region or its left or right side to the right.
+ * Just the arrow key will move the region by 1px.
+ * Arrow key + shift will move the region by 10px.
+ * Arrow key + control/meta will move to the edge of the window.
+ * @param {Event} event The keydown event
+ */
+ handleArrowRightKeyDown(event) {
+ switch (event.originalTarget.id) {
+ case "highlight":
+ if (this.getAccelKey(event)) {
+ let width = this.selectionRegion.width;
+ let { scrollX, clientWidth } = this.windowDimensions.dimensions;
+ this.selectionRegion.right = scrollX + clientWidth;
+ this.selectionRegion.left = this.selectionRegion.right - width;
+ break;
+ }
+
+ this.selectionRegion.left += 10 ** event.shiftKey;
+ // eslint-disable-next-line no-fallthrough
+ case "mover-topRight":
+ case "mover-bottomRight":
+ if (this.getAccelKey(event)) {
+ this.selectionRegion.right =
+ this.windowDimensions.scrollX + this.windowDimensions.clientWidth;
+ break;
+ }
+
+ this.selectionRegion.right += 10 ** event.shiftKey;
+ this.scrollIfByEdge(
+ this.selectionRegion.right,
+ this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2
+ );
+ break;
+ case "mover-topLeft":
+ case "mover-bottomLeft":
+ if (this.getAccelKey(event)) {
+ let right = this.selectionRegion.right;
+ this.selectionRegion.right =
+ this.windowDimensions.scrollX + this.windowDimensions.clientWidth;
+ this.selectionRegion.left = right;
+ if (event.originalTarget.id === "mover-topLeft") {
+ this.topRightMover.focus({ focusVisible: true });
+ } else if (event.originalTarget.id === "mover-bottomLeft") {
+ this.bottomRightMover.focus({ focusVisible: true });
+ }
+ break;
+ }
+
+ this.selectionRegion.left += 10 ** event.shiftKey;
+ if (this.selectionRegion.x1 >= this.selectionRegion.x2) {
+ this.selectionRegion.sortCoords();
+ if (event.originalTarget.id === "mover-topLeft") {
+ this.topRightMover.focus({ focusVisible: true });
+ } else if (event.originalTarget.id === "mover-bottomLeft") {
+ this.bottomRightMover.focus({ focusVisible: true });
+ }
+ }
+ break;
+ default:
+ return;
+ }
+
+ if (this.#state !== STATES.RESIZING) {
+ this.#setState(STATES.RESIZING);
+ }
+
+ event.preventDefault();
+ this.drawSelectionContainer();
+ }
+
+ /**
+ * Move the region or its top or bottom side downward.
+ * Just the arrow key will move the region by 1px.
+ * Arrow key + shift will move the region by 10px.
+ * Arrow key + control/meta will move to the edge of the window.
+ * @param {Event} event The keydown event
+ */
+ handleArrowDownKeyDown(event) {
+ switch (event.originalTarget.id) {
+ case "highlight":
+ if (this.getAccelKey(event)) {
+ let height = this.selectionRegion.height;
+ let { scrollY, clientHeight } = this.windowDimensions.dimensions;
+ this.selectionRegion.bottom = scrollY + clientHeight;
+ this.selectionRegion.top = this.selectionRegion.bottom - height;
+ break;
+ }
+
+ this.selectionRegion.top += 10 ** event.shiftKey;
+ // eslint-disable-next-line no-fallthrough
+ case "mover-bottomLeft":
+ case "mover-bottomRight":
+ if (this.getAccelKey(event)) {
+ this.selectionRegion.bottom =
+ this.windowDimensions.scrollY + this.windowDimensions.clientHeight;
+ break;
+ }
+
+ this.selectionRegion.bottom += 10 ** event.shiftKey;
+ this.scrollIfByEdge(
+ this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2,
+ this.selectionRegion.bottom
+ );
+ break;
+ case "mover-topLeft":
+ case "mover-topRight":
+ if (this.getAccelKey(event)) {
+ let bottom = this.selectionRegion.bottom;
+ this.selectionRegion.bottom =
+ this.windowDimensions.scrollY + this.windowDimensions.clientHeight;
+ this.selectionRegion.top = bottom;
+ if (event.originalTarget.id === "mover-topLeft") {
+ this.bottomLeftMover.focus({ focusVisible: true });
+ } else if (event.originalTarget.id === "mover-topRight") {
+ this.bottomRightMover.focus({ focusVisible: true });
+ }
+ break;
+ }
+
+ this.selectionRegion.top += 10 ** event.shiftKey;
+ if (this.selectionRegion.y1 >= this.selectionRegion.y2) {
+ this.selectionRegion.sortCoords();
+ if (event.originalTarget.id === "mover-topLeft") {
+ this.bottomLeftMover.focus({ focusVisible: true });
+ } else if (event.originalTarget.id === "mover-topRight") {
+ this.bottomRightMover.focus({ focusVisible: true });
+ }
+ }
+ break;
+ default:
+ return;
+ }
+
+ if (this.#state !== STATES.RESIZING) {
+ this.#setState(STATES.RESIZING);
+ }
+
+ event.preventDefault();
+ this.drawSelectionContainer();
+ }
+
+ /**
+ * We lock focus to the overlay when a region is selected.
+ * Can still escape with shift + F6.
+ * @param {Event} event The keydown event
+ */
+ maybeLockFocus(event) {
+ if (this.#state !== STATES.SELECTED) {
+ return;
+ }
+
+ event.preventDefault();
+ if (event.originalTarget.id === "highlight" && event.shiftKey) {
+ this.downloadButton.focus({ focusVisible: true });
+ } else if (event.originalTarget.id === "download" && !event.shiftKey) {
+ this.highlightEl.focus({ focusVisible: true });
+ } else {
+ // The content document can listen for keydown events and prevent moving
+ // focus so we manually move focus to the next element here.
+ let direction = event.shiftKey
+ ? Services.focus.MOVEFOCUS_BACKWARD
+ : Services.focus.MOVEFOCUS_FORWARD;
+ Services.focus.moveFocus(
+ this.window,
+ null,
+ direction,
+ Services.focus.FLAG_BYKEY
+ );
+ }
+ }
+
+ /**
+ * Set the focus to the most recent saved method.
+ * This will default to the download button.
+ */
+ setFocusToActionButton() {
+ if (lazy.SCREENSHOTS_LAST_SAVED_METHOD === "copy") {
+ this.copyButton.focus({ focusVisible: true });
+ } else {
+ this.downloadButton.focus({ focusVisible: true });
+ }
+ }
+
+ /**
+ * Handles when a keydown occurs in the screenshots component.
+ * All we need to do on keyup is set the state to selected.
+ * @param {Event} event The keydown event
+ */
+ handleKeyUp(event) {
+ switch (event.key) {
+ case "ArrowLeft":
+ case "ArrowUp":
+ case "ArrowRight":
+ case "ArrowDown":
+ switch (event.originalTarget.id) {
+ case "highlight":
+ case "mover-bottomLeft":
+ case "mover-bottomRight":
+ case "mover-topLeft":
+ case "mover-topRight":
+ event.preventDefault();
+ this.#setState(STATES.SELECTED);
+ break;
+ }
+ break;
+ }
+ }
+
+ /**
+ * Dispatch a custom event to the ScreenshotsComponentChild actor
+ * @param {String} eventType The name of the event
+ * @param {object} detail Extra details to send to the child actor
+ */
+ #dispatchEvent(eventType, detail) {
+ this.window.dispatchEvent(
+ new CustomEvent(eventType, {
+ bubbles: true,
+ detail,
+ })
+ );
+ }
+
+ /**
+ * Set a new state for the overlay
+ * @param {String} newState
+ */
+ #setState(newState) {
+ if (this.#state === STATES.SELECTED && newState === STATES.CROSSHAIRS) {
+ this.#dispatchEvent("Screenshots:RecordEvent", {
+ eventName: "started",
+ reason: "overlay_retry",
+ });
+ }
+ if (newState !== this.#state) {
+ this.#dispatchEvent("Screenshots:OverlaySelection", {
+ hasSelection: newState == STATES.SELECTED,
+ });
+ }
+ this.#state = newState;
+
+ switch (this.#state) {
+ case STATES.CROSSHAIRS: {
+ this.crosshairsStart();
+ break;
+ }
+ case STATES.DRAGGING_READY: {
+ this.draggingReadyStart();
+ break;
+ }
+ case STATES.DRAGGING: {
+ this.draggingStart();
+ break;
+ }
+ case STATES.SELECTED: {
+ this.selectedStart();
+ break;
+ }
+ case STATES.RESIZING: {
+ this.resizingStart();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Hide hover element, selection and buttons containers.
+ * Show the preview container and the panel.
+ * This is the initial state of the overlay.
+ */
+ crosshairsStart() {
+ this.hideHoverElementContainer();
+ this.hideSelectionContainer();
+ this.hideButtonsContainer();
+ this.showPreviewContainer();
+ this.#dispatchEvent("Screenshots:ShowPanel");
+ this.#previousDimensions = null;
+ this.#cachedEle = null;
+ this.hoverElementRegion.resetDimensions();
+ }
+
+ /**
+ * Hide the panel because we have started dragging.
+ */
+ draggingReadyStart() {
+ this.#dispatchEvent("Screenshots:HidePanel");
+ }
+
+ /**
+ * Hide the preview, hover element and buttons containers.
+ * Show the selection container.
+ */
+ draggingStart() {
+ this.hidePreviewContainer();
+ this.hideButtonsContainer();
+ this.hideHoverElementContainer();
+ this.drawSelectionContainer();
+ }
+
+ /**
+ * Hide the preview and hover element containers.
+ * Draw the selection and buttons containers.
+ */
+ selectedStart() {
+ this.hidePreviewContainer();
+ this.hideHoverElementContainer();
+ this.drawSelectionContainer();
+ this.drawButtonsContainer();
+ }
+
+ /**
+ * Hide the buttons container.
+ * Store the width and height of the current selected region.
+ * The dimensions will be used when moving the region along the edge of the
+ * page and for recording telemetry.
+ */
+ resizingStart() {
+ this.hideButtonsContainer();
+ let { width, height } = this.selectionRegion.dimensions;
+ this.#previousDimensions = { width, height };
+ }
+
+ /**
+ * Dragging has started so we set the initial selection region and set the
+ * state to draggingReady.
+ * @param {Number} pageX The x position relative to the page
+ * @param {Number} pageY The y position relative to the page
+ */
+ crosshairsDragStart(pageX, pageY) {
+ this.selectionRegion.dimensions = {
+ left: pageX,
+ top: pageY,
+ right: pageX,
+ bottom: pageY,
+ };
+
+ this.#setState(STATES.DRAGGING_READY);
+ }
+
+ /**
+ * If the background is clicked we set the state to crosshairs
+ * otherwise set the state to resizing
+ * @param {Number} pageX The x position relative to the page
+ * @param {Number} pageY The y position relative to the page
+ * @param {String} targetId The id of the event target
+ */
+ selectedDragStart(pageX, pageY, targetId) {
+ if (targetId === this.screenshotsContainer.id) {
+ this.#setState(STATES.CROSSHAIRS);
+ return;
+ }
+ this.#moverId = targetId;
+ this.#lastPageX = pageX;
+ this.#lastPageY = pageY;
+
+ this.#setState(STATES.RESIZING);
+ }
+
+ /**
+ * Draw the eyes in the preview container and find the element currently
+ * being hovered.
+ * @param {Number} clientX The x position relative to the viewport
+ * @param {Number} clientY The y position relative to the viewport
+ */
+ crosshairsMove(clientX, clientY) {
+ this.drawPreviewEyes(clientX, clientY);
+
+ this.handleElementHover(clientX, clientY);
+ }
+
+ /**
+ * Set the selection region dimensions and if the region is at least 40
+ * pixels diagnally in distance, set the state to dragging.
+ * @param {Number} pageX The x position relative to the page
+ * @param {Number} pageY The y position relative to the page
+ */
+ draggingReadyDrag(pageX, pageY) {
+ this.selectionRegion.dimensions = {
+ right: pageX,
+ bottom: pageY,
+ };
+
+ if (this.selectionRegion.distance > 40) {
+ this.#setState(STATES.DRAGGING);
+ }
+ }
+
+ /**
+ * Scroll if along the edge of the viewport, update the selection region
+ * dimensions and draw the selection container.
+ * @param {Number} pageX The x position relative to the page
+ * @param {Number} pageY The y position relative to the page
+ */
+ draggingDrag(pageX, pageY) {
+ this.scrollIfByEdge(pageX, pageY);
+ this.selectionRegion.dimensions = {
+ right: pageX,
+ bottom: pageY,
+ };
+
+ this.drawSelectionContainer();
+ }
+
+ /**
+ * Resize the selection region depending on the mover that started the resize.
+ * @param {Number} pageX The x position relative to the page
+ * @param {Number} pageY The y position relative to the page
+ */
+ resizingDrag(pageX, pageY) {
+ this.scrollIfByEdge(pageX, pageY);
+ switch (this.#moverId) {
+ case "mover-topLeft": {
+ this.selectionRegion.dimensions = {
+ left: pageX,
+ top: pageY,
+ };
+ break;
+ }
+ case "mover-top": {
+ this.selectionRegion.dimensions = { top: pageY };
+ break;
+ }
+ case "mover-topRight": {
+ this.selectionRegion.dimensions = {
+ top: pageY,
+ right: pageX,
+ };
+ break;
+ }
+ case "mover-right": {
+ this.selectionRegion.dimensions = {
+ right: pageX,
+ };
+ break;
+ }
+ case "mover-bottomRight": {
+ this.selectionRegion.dimensions = {
+ right: pageX,
+ bottom: pageY,
+ };
+ break;
+ }
+ case "mover-bottom": {
+ this.selectionRegion.dimensions = {
+ bottom: pageY,
+ };
+ break;
+ }
+ case "mover-bottomLeft": {
+ this.selectionRegion.dimensions = {
+ left: pageX,
+ bottom: pageY,
+ };
+ break;
+ }
+ case "mover-left": {
+ this.selectionRegion.dimensions = { left: pageX };
+ break;
+ }
+ case "highlight": {
+ let diffX = this.#lastPageX - pageX;
+ let diffY = this.#lastPageY - pageY;
+
+ let newLeft;
+ let newRight;
+ let newTop;
+ let newBottom;
+
+ // Unpack dimensions to use here
+ let {
+ left: boxLeft,
+ top: boxTop,
+ right: boxRight,
+ bottom: boxBottom,
+ width: boxWidth,
+ height: boxHeight,
+ } = this.selectionRegion.dimensions;
+ let { scrollWidth, scrollHeight } = this.windowDimensions.dimensions;
+
+ // wait until all 4 if elses have completed before setting box dimensions
+ if (boxWidth <= this.#previousDimensions.width && boxLeft === 0) {
+ newLeft = boxRight - this.#previousDimensions.width;
+ } else {
+ newLeft = boxLeft;
+ }
+
+ if (
+ boxWidth <= this.#previousDimensions.width &&
+ boxRight === scrollWidth
+ ) {
+ newRight = boxLeft + this.#previousDimensions.width;
+ } else {
+ newRight = boxRight;
+ }
+
+ if (boxHeight <= this.#previousDimensions.height && boxTop === 0) {
+ newTop = boxBottom - this.#previousDimensions.height;
+ } else {
+ newTop = boxTop;
+ }
+
+ if (
+ boxHeight <= this.#previousDimensions.height &&
+ boxBottom === scrollHeight
+ ) {
+ newBottom = boxTop + this.#previousDimensions.height;
+ } else {
+ newBottom = boxBottom;
+ }
+
+ this.selectionRegion.dimensions = {
+ left: newLeft - diffX,
+ top: newTop - diffY,
+ right: newRight - diffX,
+ bottom: newBottom - diffY,
+ };
+
+ this.#lastPageX = pageX;
+ this.#lastPageY = pageY;
+ break;
+ }
+ }
+ this.drawSelectionContainer();
+ }
+
+ /**
+ * If there is a valid element region, update and draw the selection
+ * container and set the state to selected.
+ * Otherwise set the state to crosshairs.
+ */
+ draggingReadyDragEnd() {
+ if (this.hoverElementRegion.isRegionValid) {
+ this.selectionRegion.dimensions = this.hoverElementRegion.dimensions;
+ this.#setState(STATES.SELECTED);
+ this.setFocusToActionButton();
+ this.#dispatchEvent("Screenshots:RecordEvent", {
+ eventName: "selected",
+ reason: "element",
+ });
+ this.#methodsUsed.element += 1;
+ } else {
+ this.#setState(STATES.CROSSHAIRS);
+ }
+ }
+
+ /**
+ * Update the selection region dimensions and set the state to selected.
+ * @param {Number} pageX The x position relative to the page
+ * @param {Number} pageY The y position relative to the page
+ */
+ draggingDragEnd(pageX, pageY) {
+ this.selectionRegion.dimensions = {
+ right: pageX,
+ bottom: pageY,
+ };
+ this.selectionRegion.sortCoords();
+ this.#setState(STATES.SELECTED);
+ this.maybeRecordRegionSelected();
+ this.#methodsUsed.region += 1;
+ this.setFocusToActionButton();
+ }
+
+ /**
+ * Update the selection region dimensions by calling `resizingDrag` and set
+ * the state to selected.
+ * @param {Number} pageX The x position relative to the page
+ * @param {Number} pageY The y position relative to the page
+ */
+ resizingDragEnd(pageX, pageY) {
+ this.resizingDrag(pageX, pageY);
+ this.selectionRegion.sortCoords();
+ this.#setState(STATES.SELECTED);
+ this.setFocusToActionButton();
+ this.maybeRecordRegionSelected();
+ if (this.#moverId === "highlight") {
+ this.#methodsUsed.move += 1;
+ } else {
+ this.#methodsUsed.resize += 1;
+ }
+ }
+
+ maybeRecordRegionSelected() {
+ let { width, height } = this.selectionRegion.dimensions;
+
+ if (
+ !this.#previousDimensions ||
+ (Math.abs(this.#previousDimensions.width - width) >
+ REGION_CHANGE_THRESHOLD &&
+ Math.abs(this.#previousDimensions.height - height) >
+ REGION_CHANGE_THRESHOLD)
+ ) {
+ this.#dispatchEvent("Screenshots:RecordEvent", {
+ eventName: "selected",
+ reason: "region_selection",
+ });
+ }
+ this.#previousDimensions = { width, height };
+ }
+
+ /**
+ * Draw the preview eyes pointer towards the mouse.
+ * @param {Number} clientX The x position relative to the viewport
+ * @param {Number} clientY The y position relative to the viewport
+ */
+ drawPreviewEyes(clientX, clientY) {
+ let { clientWidth, clientHeight } = this.windowDimensions.dimensions;
+ const xpos = Math.floor((10 * (clientX - clientWidth / 2)) / clientWidth);
+ const ypos = Math.floor((10 * (clientY - clientHeight / 2)) / clientHeight);
+ const move = `transform:translate(${xpos}px, ${ypos}px);`;
+ this.leftEye.style = move;
+ this.rightEye.style = move;
+ }
+
+ showPreviewContainer() {
+ this.previewContainer.hidden = false;
+ }
+
+ hidePreviewContainer() {
+ this.previewContainer.hidden = true;
+ }
+
+ updatePreviewContainer() {
+ let { clientWidth, clientHeight } = this.windowDimensions.dimensions;
+ this.previewContainer.style.width = `${clientWidth}px`;
+ this.previewContainer.style.height = `${clientHeight}px`;
+ }
+
+ /**
+ * Update the screenshots overlay container based on the window dimensions.
+ */
+ updateScreenshotsOverlayContainer() {
+ let { scrollWidth, scrollHeight } = this.windowDimensions.dimensions;
+ this.screenshotsContainer.style = `width:${scrollWidth}px;height:${scrollHeight}px;`;
+ }
+
+ showScreenshotsOverlayContainer() {
+ this.screenshotsContainer.hidden = false;
+ }
+
+ hideScreenshotsOverlayContainer() {
+ this.screenshotsContainer.hidden = true;
+ }
+
+ /**
+ * Draw the hover element container based on the hover element region.
+ */
+ drawHoverElementRegion() {
+ this.showHoverElementContainer();
+
+ let { top, left, width, height } = this.hoverElementRegion.dimensions;
+
+ this.hoverElementContainer.style = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`;
+ }
+
+ showHoverElementContainer() {
+ this.hoverElementContainer.hidden = false;
+ }
+
+ hideHoverElementContainer() {
+ this.hoverElementContainer.hidden = true;
+ }
+
+ /**
+ * Draw each background element and the highlight element base on the
+ * selection region.
+ */
+ drawSelectionContainer() {
+ this.showSelectionContainer();
+
+ let { top, left, right, bottom, width, height } =
+ this.selectionRegion.dimensions;
+
+ this.highlightEl.style = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`;
+
+ this.leftBackgroundEl.style = `top:${top}px;width:${left}px;height:${height}px;`;
+ this.topBackgroundEl.style.height = `${top}px`;
+ this.rightBackgroundEl.style = `top:${top}px;left:${right}px;width:calc(100% - ${right}px);height:${height}px;`;
+ this.bottomBackgroundEl.style = `top:${bottom}px;height:calc(100% - ${bottom}px);`;
+
+ this.updateSelectionSizeText();
+ }
+
+ updateSelectionSizeText() {
+ let dpr = this.windowDimensions.devicePixelRatio;
+ let { width, height } = this.selectionRegion.dimensions;
+
+ let [selectionSizeTranslation] =
+ lazy.overlayLocalization.formatMessagesSync([
+ {
+ id: "screenshots-overlay-selection-region-size",
+ args: {
+ width: Math.floor(width * dpr),
+ height: Math.floor(height * dpr),
+ },
+ },
+ ]);
+ this.selectionSize.textContent = selectionSizeTranslation.value;
+ }
+
+ showSelectionContainer() {
+ this.selectionContainer.hidden = false;
+ }
+
+ hideSelectionContainer() {
+ this.selectionContainer.hidden = true;
+ }
+
+ /**
+ * Draw the buttons container in the bottom right corner of the selection
+ * container if possible.
+ * The buttons will be visible in the viewport if the selection container
+ * is within the viewport, otherwise skip drawing the buttons.
+ */
+ drawButtonsContainer() {
+ this.showButtonsContainer();
+
+ let {
+ left: boxLeft,
+ top: boxTop,
+ right: boxRight,
+ bottom: boxBottom,
+ } = this.selectionRegion.dimensions;
+ let { clientWidth, clientHeight, scrollX, scrollY } =
+ this.windowDimensions.dimensions;
+
+ if (
+ boxTop > scrollY + clientHeight ||
+ boxBottom < scrollY ||
+ boxLeft > scrollX + clientWidth ||
+ boxRight < scrollX
+ ) {
+ // The box is offscreen so need to draw the buttons
+ return;
+ }
+
+ let top = boxBottom;
+
+ if (scrollY + clientHeight - boxBottom < 70) {
+ if (boxBottom < scrollY + clientHeight) {
+ top = boxBottom - 60;
+ } else if (scrollY + clientHeight - boxTop < 70) {
+ top = boxTop - 60;
+ } else {
+ top = scrollY + clientHeight - 60;
+ }
+ }
+
+ if (boxRight < 300) {
+ this.buttonsContainer.style.left = `${boxLeft}px`;
+ this.buttonsContainer.style.right = "";
+ } else {
+ this.buttonsContainer.style.right = `calc(100% - ${boxRight}px)`;
+ this.buttonsContainer.style.left = "";
+ }
+
+ this.buttonsContainer.style.top = `${top}px`;
+ }
+
+ showButtonsContainer() {
+ this.buttonsContainer.hidden = false;
+ }
+
+ hideButtonsContainer() {
+ this.buttonsContainer.hidden = true;
+ }
+
+ /**
+ * Set the pointer events to none on the screenshots elements so
+ * elementFromPoint can find the real element at the given point.
+ */
+ setPointerEventsNone() {
+ this.screenshotsContainer.style.pointerEvents = "none";
+ }
+
+ resetPointerEvents() {
+ this.screenshotsContainer.style.pointerEvents = "";
+ }
+
+ /**
+ * Try to find a reasonable element for a given point.
+ * If a reasonable element is found, draw the hover element container for
+ * that element region.
+ * @param {Number} clientX The x position relative to the viewport
+ * @param {Number} clientY The y position relative to the viewport
+ */
+ async handleElementHover(clientX, clientY) {
+ this.setPointerEventsNone();
+ let promise = getElementFromPoint(clientX, clientY, this.document);
+ this.resetPointerEvents();
+ let { ele, rect } = await promise;
+
+ if (
+ this.#cachedEle &&
+ !this.window.HTMLIFrameElement.isInstance(this.#cachedEle) &&
+ this.#cachedEle === ele
+ ) {
+ // Still hovering over the same element
+ return;
+ }
+ this.#cachedEle = ele;
+
+ if (!rect) {
+ // this means we found an element that wasn't an iframe
+ rect = getBestRectForElement(ele, this.document);
+ }
+
+ if (rect) {
+ let { scrollX, scrollY } = this.windowDimensions.dimensions;
+ let { left, top, right, bottom } = rect;
+ let newRect = {
+ left: left + scrollX,
+ top: top + scrollY,
+ right: right + scrollX,
+ bottom: bottom + scrollY,
+ };
+ this.hoverElementRegion.dimensions = newRect;
+ this.drawHoverElementRegion();
+ } else {
+ this.hoverElementRegion.resetDimensions();
+ this.hideHoverElementContainer();
+ }
+ }
+
+ /**
+ * Scroll the viewport if near one or both of the edges.
+ * @param {Number} pageX The x position relative to the page
+ * @param {Number} pageY The y position relative to the page
+ */
+ scrollIfByEdge(pageX, pageY) {
+ let { scrollX, scrollY, clientWidth, clientHeight } =
+ this.windowDimensions.dimensions;
+
+ if (pageY - scrollY < SCROLL_BY_EDGE) {
+ // Scroll up
+ this.scrollWindow(0, -(SCROLL_BY_EDGE - (pageY - scrollY)));
+ } else if (scrollY + clientHeight - pageY < SCROLL_BY_EDGE) {
+ // Scroll down
+ this.scrollWindow(0, SCROLL_BY_EDGE - (scrollY + clientHeight - pageY));
+ }
+
+ if (pageX - scrollX <= SCROLL_BY_EDGE) {
+ // Scroll left
+ this.scrollWindow(-(SCROLL_BY_EDGE - (pageX - scrollX)), 0);
+ } else if (scrollX + clientWidth - pageX <= SCROLL_BY_EDGE) {
+ // Scroll right
+ this.scrollWindow(SCROLL_BY_EDGE - (scrollX + clientWidth - pageX), 0);
+ }
+ }
+
+ /**
+ * Scroll the window by the given amount.
+ * @param {Number} x The x amount to scroll
+ * @param {Number} y The y amount to scroll
+ */
+ scrollWindow(x, y) {
+ this.window.scrollBy(x, y);
+ this.updateScreenshotsOverlayDimensions("scroll");
+ }
+
+ /**
+ * The page was resized or scrolled. We need to update the screenshots
+ * container size so we don't draw outside the page bounds.
+ * @param {String} eventType will be "scroll" or "resize"
+ */
+ async updateScreenshotsOverlayDimensions(eventType) {
+ let updateWindowDimensionsPromise = this.updateWindowDimensions();
+
+ if (this.#state === STATES.CROSSHAIRS) {
+ if (eventType === "resize") {
+ this.hideHoverElementContainer();
+ this.#cachedEle = null;
+ } else if (eventType === "scroll") {
+ if (this.#lastClientX && this.#lastClientY) {
+ this.#cachedEle = null;
+ this.handleElementHover(this.#lastClientX, this.#lastClientY);
+ }
+ }
+ } else if (this.#state === STATES.SELECTED) {
+ await updateWindowDimensionsPromise;
+ this.selectionRegion.shift();
+ this.drawSelectionContainer();
+ this.drawButtonsContainer();
+ this.updateSelectionSizeText();
+ }
+ }
+
+ /**
+ * Returns the window's dimensions for the current window.
+ *
+ * @return {Object} An object containing window dimensions
+ * {
+ * clientWidth: The width of the viewport
+ * clientHeight: The height of the viewport
+ * scrollWidth: The width of the enitre page
+ * scrollHeight: The height of the entire page
+ * scrollX: The X scroll offset of the viewport
+ * scrollY: The Y scroll offest of the viewport
+ * scrollMinX: The X mininmun the viewport can scroll to
+ * scrollMinY: The Y mininmun the viewport can scroll to
+ * }
+ */
+ getDimensionsFromWindow() {
+ let {
+ innerHeight,
+ innerWidth,
+ scrollMaxY,
+ scrollMaxX,
+ scrollMinY,
+ scrollMinX,
+ scrollY,
+ scrollX,
+ } = this.window;
+
+ let scrollWidth = innerWidth + scrollMaxX - scrollMinX;
+ let scrollHeight = innerHeight + scrollMaxY - scrollMinY;
+ let clientHeight = innerHeight;
+ let clientWidth = innerWidth;
+
+ const scrollbarHeight = {};
+ const scrollbarWidth = {};
+ this.window.windowUtils.getScrollbarSize(
+ false,
+ scrollbarWidth,
+ scrollbarHeight
+ );
+ scrollWidth -= scrollbarWidth.value;
+ scrollHeight -= scrollbarHeight.value;
+ clientWidth -= scrollbarWidth.value;
+ clientHeight -= scrollbarHeight.value;
+
+ return {
+ clientWidth,
+ clientHeight,
+ scrollWidth,
+ scrollHeight,
+ scrollX,
+ scrollY,
+ scrollMinX,
+ scrollMinY,
+ };
+ }
+
+ /**
+ * We have to be careful not to draw the overlay larger than the document
+ * because the overlay is absolutely position and within the document so we
+ * can cause the document to overflow when it shouldn't. To mitigate this,
+ * we will temporarily position the overlay to position fixed with width and
+ * height 100% so the overlay is within the document bounds. Then we will get
+ * the dimensions of the document to correctly draw the overlay.
+ */
+ async updateWindowDimensions() {
+ // Setting the screenshots container attribute "resizing" will make the
+ // overlay fixed position with width and height of 100% percent so it
+ // does not draw outside the actual document.
+ this.screenshotsContainer.toggleAttribute("resizing", true);
+
+ await new Promise(r => this.window.requestAnimationFrame(r));
+
+ let {
+ clientWidth,
+ clientHeight,
+ scrollWidth,
+ scrollHeight,
+ scrollX,
+ scrollY,
+ scrollMinX,
+ scrollMinY,
+ } = this.getDimensionsFromWindow();
+ this.screenshotsContainer.toggleAttribute("resizing", false);
+
+ this.windowDimensions.dimensions = {
+ clientWidth,
+ clientHeight,
+ scrollWidth,
+ scrollHeight,
+ scrollX,
+ scrollY,
+ scrollMinX,
+ scrollMinY,
+ devicePixelRatio: this.window.devicePixelRatio,
+ };
+
+ this.updatePreviewContainer();
+ this.updateScreenshotsOverlayContainer();
+
+ setMaxDetectHeight(Math.max(clientHeight + 100, 700));
+ setMaxDetectWidth(Math.max(clientWidth + 100, 1000));
+ }
+}
diff --git a/browser/components/screenshots/ScreenshotsUtils.sys.mjs b/browser/components/screenshots/ScreenshotsUtils.sys.mjs
new file mode 100644
index 0000000000..68e4f896bf
--- /dev/null
+++ b/browser/components/screenshots/ScreenshotsUtils.sys.mjs
@@ -0,0 +1,993 @@
+/* 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 { getFilename } from "chrome://browser/content/screenshots/fileHelpers.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const SCREENSHOTS_LAST_SCREENSHOT_METHOD_PREF =
+ "screenshots.browser.component.last-screenshot-method";
+const SCREENSHOTS_LAST_SAVED_METHOD_PREF =
+ "screenshots.browser.component.last-saved-method";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetters(lazy, {
+ AlertsService: ["@mozilla.org/alerts-service;1", "nsIAlertsService"],
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "SCREENSHOTS_LAST_SAVED_METHOD",
+ SCREENSHOTS_LAST_SAVED_METHOD_PREF,
+ "download"
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "SCREENSHOTS_LAST_SCREENSHOT_METHOD",
+ SCREENSHOTS_LAST_SCREENSHOT_METHOD_PREF,
+ "visible"
+);
+
+ChromeUtils.defineLazyGetter(lazy, "screenshotsLocalization", () => {
+ return new Localization(["browser/screenshots.ftl"], true);
+});
+
+// The max dimension for a canvas is 32,767 https://searchfox.org/mozilla-central/rev/f40d29a11f2eb4685256b59934e637012ea6fb78/gfx/cairo/cairo/src/cairo-image-surface.c#62.
+// The max number of pixels for a canvas is 472,907,776 pixels (i.e., 22,528 x 20,992) https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
+// We have to limit screenshots to these dimensions otherwise it will cause an error.
+export const MAX_CAPTURE_DIMENSION = 32766;
+export const MAX_CAPTURE_AREA = 472907776;
+export const MAX_SNAPSHOT_DIMENSION = 1024;
+
+export class ScreenshotsComponentParent extends JSWindowActorParent {
+ async receiveMessage(message) {
+ let region, title;
+ let browser = message.target.browsingContext.topFrameElement;
+ // ignore message from child actors with no associated browser element
+ if (!browser) {
+ return;
+ }
+ if (
+ ScreenshotsUtils.getUIPhase(browser) == UIPhases.CLOSED &&
+ !ScreenshotsUtils.browserToScreenshotsState.has(browser)
+ ) {
+ // We've already exited or never opened and there's no UI or state that could
+ // handle this message. We additionally check for screenshot-state to ensure we
+ // don't ignore an overlay message when there is no current selection - which
+ // otherwise looks like the UIPhases.CLOSED state.
+ return;
+ }
+ switch (message.name) {
+ case "Screenshots:CancelScreenshot":
+ let { reason } = message.data;
+ ScreenshotsUtils.cancel(browser, reason);
+ break;
+ case "Screenshots:CopyScreenshot":
+ ScreenshotsUtils.closePanel(browser);
+ ({ region } = message.data);
+ await ScreenshotsUtils.copyScreenshotFromRegion(region, browser);
+ ScreenshotsUtils.exit(browser);
+ break;
+ case "Screenshots:DownloadScreenshot":
+ ScreenshotsUtils.closePanel(browser);
+ ({ title, region } = message.data);
+ await ScreenshotsUtils.downloadScreenshotFromRegion(
+ title,
+ region,
+ browser
+ );
+ ScreenshotsUtils.exit(browser);
+ break;
+ case "Screenshots:OverlaySelection":
+ ScreenshotsUtils.setPerBrowserState(browser, {
+ hasOverlaySelection: message.data.hasSelection,
+ });
+ break;
+ case "Screenshots:ShowPanel":
+ ScreenshotsUtils.openPanel(browser);
+ break;
+ case "Screenshots:HidePanel":
+ ScreenshotsUtils.closePanel(browser);
+ break;
+ }
+ }
+
+ didDestroy() {
+ // When restoring a crashed tab the browser is null
+ let browser = this.browsingContext.topFrameElement;
+ if (browser) {
+ ScreenshotsUtils.exit(browser);
+ }
+ }
+}
+
+export class ScreenshotsHelperParent extends JSWindowActorParent {
+ receiveMessage(message) {
+ switch (message.name) {
+ case "ScreenshotsHelper:GetElementRectFromPoint":
+ let cxt = BrowsingContext.get(message.data.bcId);
+ return cxt.currentWindowGlobal
+ .getActor("ScreenshotsHelper")
+ .sendQuery("ScreenshotsHelper:GetElementRectFromPoint", message.data);
+ }
+ return null;
+ }
+}
+
+export const UIPhases = {
+ CLOSED: 0, // nothing showing
+ INITIAL: 1, // panel and overlay showing
+ OVERLAYSELECTION: 2, // something selected in the overlay
+ PREVIEW: 3, // preview dialog showing
+};
+
+export var ScreenshotsUtils = {
+ browserToScreenshotsState: new WeakMap(),
+ initialized: false,
+ methodsUsed: {},
+
+ /**
+ * Figures out which of various states the screenshots UI is in, for the given browser.
+ * @param browser The selected browser
+ * @returns One of the `UIPhases` constants
+ */
+ getUIPhase(browser) {
+ let perBrowserState = this.browserToScreenshotsState.get(browser);
+ if (perBrowserState?.previewDialog) {
+ return UIPhases.PREVIEW;
+ }
+ const buttonsPanel = this.panelForBrowser(browser);
+ if (buttonsPanel && !buttonsPanel.hidden) {
+ return UIPhases.INITIAL;
+ }
+ if (perBrowserState?.hasOverlaySelection) {
+ return UIPhases.OVERLAYSELECTION;
+ }
+ return UIPhases.CLOSED;
+ },
+
+ resetMethodsUsed() {
+ this.methodsUsed = { fullpage: 0, visible: 0 };
+ },
+
+ initialize() {
+ if (!this.initialized) {
+ if (
+ !Services.prefs.getBoolPref(
+ "screenshots.browser.component.enabled",
+ false
+ )
+ ) {
+ return;
+ }
+ this.resetMethodsUsed();
+ Services.telemetry.setEventRecordingEnabled("screenshots", true);
+ Services.obs.addObserver(this, "menuitem-screenshot");
+ this.initialized = true;
+ if (Cu.isInAutomation) {
+ Services.obs.notifyObservers(null, "screenshots-component-initialized");
+ }
+ }
+ },
+
+ uninitialize() {
+ if (this.initialized) {
+ Services.obs.removeObserver(this, "menuitem-screenshot");
+ this.initialized = false;
+ }
+ },
+
+ handleEvent(event) {
+ // Escape should cancel and exit
+ if (event.type === "keydown" && event.key === "Escape") {
+ let browser = event.view.gBrowser.selectedBrowser;
+ this.cancel(browser, "escape");
+ }
+ },
+
+ observe(subj, topic, data) {
+ let { gBrowser } = subj;
+ let browser = gBrowser.selectedBrowser;
+
+ switch (topic) {
+ case "menuitem-screenshot": {
+ const uiPhase = this.getUIPhase(browser);
+ if (uiPhase !== UIPhases.CLOSED) {
+ // toggle from already-open to closed
+ this.cancel(browser, data);
+ return;
+ }
+ this.start(browser, data);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Notify screenshots when screenshot command is used.
+ * @param window The current window the screenshot command was used.
+ * @param type The type of screenshot taken. Used for telemetry.
+ */
+ notify(window, type) {
+ if (Services.prefs.getBoolPref("screenshots.browser.component.enabled")) {
+ Services.obs.notifyObservers(
+ window.event.currentTarget.ownerGlobal,
+ "menuitem-screenshot",
+ type
+ );
+ } else {
+ Services.obs.notifyObservers(null, "menuitem-screenshot-extension", type);
+ }
+ },
+
+ /**
+ * Creates/gets and returns a Screenshots actor.
+ *
+ * @param browser The current browser.
+ * @returns JSWindowActor The screenshot actor.
+ */
+ getActor(browser) {
+ let actor = browser.browsingContext.currentWindowGlobal.getActor(
+ "ScreenshotsComponent"
+ );
+ return actor;
+ },
+
+ /**
+ * Show the Screenshots UI and start the capture flow
+ * @param browser The current browser.
+ * @param reason [string] Optional reason string passed along when recording telemetry events
+ */
+ start(browser, reason = "") {
+ const uiPhase = this.getUIPhase(browser);
+ switch (uiPhase) {
+ case UIPhases.CLOSED:
+ this.captureFocusedElement(browser, "previousFocusRef");
+ this.showPanelAndOverlay(browser, reason);
+ break;
+ case UIPhases.INITIAL:
+ // nothing to do, panel & overlay are already open
+ break;
+ case UIPhases.PREVIEW: {
+ this.closeDialogBox(browser);
+ this.showPanelAndOverlay(browser, reason);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Exit the Screenshots UI for the given browser
+ * Closes any of open UI elements (preview dialog, panel, overlay) and cleans up internal state.
+ * @param browser The current browser.
+ */
+ exit(browser) {
+ this.captureFocusedElement(browser, "currentFocusRef");
+ this.closeDialogBox(browser);
+ this.closePanel(browser);
+ this.closeOverlay(browser);
+ this.resetMethodsUsed();
+ this.attemptToRestoreFocus(browser);
+
+ this.browserToScreenshotsState.delete(browser);
+ if (Cu.isInAutomation) {
+ Services.obs.notifyObservers(null, "screenshots-exit");
+ }
+ },
+
+ /**
+ * Cancel/abort the screenshots operation for the given browser
+ *
+ * @param browser The current browser.
+ */
+ cancel(browser, reason) {
+ this.recordTelemetryEvent("canceled", reason, {});
+ this.exit(browser);
+ },
+
+ /**
+ * Update internal UI state associated with the given browser
+ *
+ * @param browser The current browser.
+ * @param nameValues {object} An object with one or more named property values
+ */
+ setPerBrowserState(browser, nameValues = {}) {
+ if (!this.browserToScreenshotsState.has(browser)) {
+ // we should really have this state already, created when the preview dialog was opened
+ this.browserToScreenshotsState.set(browser, {});
+ }
+ let perBrowserState = this.browserToScreenshotsState.get(browser);
+ Object.assign(perBrowserState, nameValues);
+ },
+
+ /**
+ * Attempt to place focus on the element that had focus before screenshots UI was shown
+ *
+ * @param browser The current browser.
+ */
+ attemptToRestoreFocus(browser) {
+ const document = browser.ownerDocument;
+ const window = browser.ownerGlobal;
+
+ const doFocus = () => {
+ // Move focus it back to where it was previously.
+ prevFocus.setAttribute("refocused-by-panel", true);
+ try {
+ let fm = Services.focus;
+ fm.setFocus(prevFocus, fm.FLAG_NOSCROLL);
+ } catch (e) {
+ prevFocus.focus();
+ }
+ prevFocus.removeAttribute("refocused-by-panel");
+ let focusedElement;
+ try {
+ focusedElement = document.commandDispatcher.focusedElement;
+ if (!focusedElement) {
+ focusedElement = document.activeElement;
+ }
+ } catch (ex) {
+ focusedElement = document.activeElement;
+ }
+ };
+
+ let perBrowserState = this.browserToScreenshotsState.get(browser) || {};
+ let prevFocus = perBrowserState.previousFocusRef?.get();
+ let currentFocus = perBrowserState.currentFocusRef?.get();
+ delete perBrowserState.currentFocusRef;
+
+ // Avoid changing focus if focus changed during exit - perhaps exit was caused
+ // by a user action which resulted in focus moving
+ let nowFocus;
+ try {
+ nowFocus = document.commandDispatcher.focusedElement;
+ } catch (e) {
+ nowFocus = document.activeElement;
+ }
+ if (nowFocus && nowFocus != currentFocus) {
+ return;
+ }
+
+ let dialog = this.getDialog(browser);
+ let panel = this.panelForBrowser(browser);
+
+ if (prevFocus) {
+ // Try to restore focus
+ try {
+ if (document.commandDispatcher.focusedWindow != window) {
+ // Focus has already been set to a different window
+ return;
+ }
+ } catch (ex) {}
+
+ if (!currentFocus) {
+ doFocus();
+ return;
+ }
+ while (currentFocus) {
+ if (
+ (dialog && currentFocus == dialog) ||
+ (panel && currentFocus == panel) ||
+ currentFocus == browser
+ ) {
+ doFocus();
+ return;
+ }
+ currentFocus = currentFocus.parentNode;
+ if (
+ currentFocus &&
+ currentFocus.nodeType == currentFocus.DOCUMENT_FRAGMENT_NODE &&
+ currentFocus.host
+ ) {
+ // focus was in a shadowRoot, we'll try the host",
+ currentFocus = currentFocus.host;
+ }
+ }
+ doFocus();
+ }
+ },
+
+ /**
+ * Set a flag so we don't try to exit when preview dialog next closes.
+ *
+ * @param browser The current browser.
+ * @param reason [string] Optional reason string passed along when recording telemetry events
+ */
+ scheduleRetry(browser, reason) {
+ let perBrowserState = this.browserToScreenshotsState.get(browser);
+ if (!perBrowserState?.closedPromise) {
+ console.warn(
+ "Expected perBrowserState with a closedPromise for the preview dialog"
+ );
+ return;
+ }
+ this.setPerBrowserState(browser, { exitOnPreviewClose: false });
+ perBrowserState?.closedPromise.then(() => {
+ this.start(browser, reason);
+ });
+ },
+
+ /**
+ * Open the tab dialog for preview
+ *
+ * @param browser The current browser
+ */
+ async openPreviewDialog(browser) {
+ let dialogBox = browser.ownerGlobal.gBrowser.getTabDialogBox(browser);
+ let { dialog, closedPromise } = await dialogBox.open(
+ `chrome://browser/content/screenshots/screenshots.html?browsingContextId=${browser.browsingContext.id}`,
+ {
+ features: "resizable=no",
+ sizeTo: "available",
+ allowDuplicateDialogs: false,
+ },
+ browser
+ );
+
+ this.setPerBrowserState(browser, {
+ previewDialog: dialog,
+ exitOnPreviewClose: true,
+ closedPromise: closedPromise.finally(() => {
+ this.onDialogClose(browser);
+ }),
+ });
+ return dialog;
+ },
+
+ /**
+ * Take a weak-reference to whatever element currently has focus and associate it with
+ * the UI state for this browser.
+ *
+ * @param browser The current browser.
+ * @param {string} stateRefName The property name for this element reference.
+ */
+ captureFocusedElement(browser, stateRefName) {
+ let document = browser.ownerDocument;
+ let focusedElement;
+ try {
+ focusedElement = document.commandDispatcher.focusedElement;
+ if (!focusedElement) {
+ focusedElement = document.activeElement;
+ }
+ } catch (ex) {
+ focusedElement = document.activeElement;
+ }
+ this.setPerBrowserState(browser, {
+ [stateRefName]: Cu.getWeakReference(focusedElement),
+ });
+ },
+
+ /**
+ * Returns the buttons panel for the given browser
+ * @param browser The current browser
+ * @returns The buttons panel
+ */
+ panelForBrowser(browser) {
+ return browser.ownerDocument.getElementById("screenshotsPagePanel");
+ },
+
+ /**
+ * Create the buttons container from its template, for this browser
+ * @param browser The current browser
+ * @returns The buttons panel
+ */
+ createPanelForBrowser(browser) {
+ let buttonsPanel = this.panelForBrowser(browser);
+ if (!buttonsPanel) {
+ let doc = browser.ownerDocument;
+ let template = doc.getElementById("screenshotsPagePanelTemplate");
+ let fragmentClone = template.content.cloneNode(true);
+ buttonsPanel = fragmentClone.firstElementChild;
+ template.replaceWith(buttonsPanel);
+
+ let anchor = browser.ownerDocument.querySelector("#navigator-toolbox");
+ anchor.appendChild(buttonsPanel);
+ }
+
+ return this.panelForBrowser(browser);
+ },
+
+ /**
+ * Open the buttons panel.
+ * @param browser The current browser
+ */
+ openPanel(browser) {
+ let buttonsPanel = this.panelForBrowser(browser);
+ if (!buttonsPanel.hidden) {
+ return;
+ }
+ buttonsPanel.hidden = false;
+ buttonsPanel.ownerDocument.addEventListener("keydown", this);
+
+ buttonsPanel
+ .querySelector("screenshots-buttons")
+ .focusButton(lazy.SCREENSHOTS_LAST_SCREENSHOT_METHOD);
+ },
+
+ /**
+ * Close the panel
+ * @param browser The current browser
+ */
+ closePanel(browser) {
+ let buttonsPanel = this.panelForBrowser(browser);
+ if (!buttonsPanel) {
+ return;
+ }
+ buttonsPanel.hidden = true;
+ buttonsPanel.ownerDocument.removeEventListener("keydown", this);
+ },
+
+ /**
+ * If the buttons panel exists and is open we will hide both the panel
+ * and the overlay. If the overlay is showing, we will hide the overlay.
+ * Otherwise create or display the buttons.
+ * @param browser The current browser.
+ */
+ async showPanelAndOverlay(browser, data) {
+ let actor = this.getActor(browser);
+ actor.sendAsyncMessage("Screenshots:ShowOverlay");
+ this.createPanelForBrowser(browser);
+ this.recordTelemetryEvent("started", data, {});
+ this.openPanel(browser);
+ },
+
+ /**
+ * Close the overlay UI, and clear out internal state if there was an overlay selection
+ * The overlay lives in the child document; so although closing is actually async, we assume success.
+ * @param browser The current browser.
+ */
+ closeOverlay(browser, options = {}) {
+ let actor = this.getActor(browser);
+ actor?.sendAsyncMessage("Screenshots:HideOverlay", options);
+
+ if (this.browserToScreenshotsState.has(browser)) {
+ this.setPerBrowserState(browser, {
+ hasOverlaySelection: false,
+ });
+ }
+ },
+
+ /**
+ * Gets the screenshots dialog box
+ * @param browser The selected browser
+ * @returns Screenshots dialog box if it exists otherwise null
+ */
+ getDialog(browser) {
+ let currTabDialogBox = browser.tabDialogBox;
+ let browserContextId = browser.browsingContext.id;
+ if (currTabDialogBox) {
+ currTabDialogBox.getTabDialogManager();
+ let manager = currTabDialogBox.getTabDialogManager();
+ let dialogs = manager.hasDialogs && manager.dialogs;
+ if (dialogs.length) {
+ for (let dialog of dialogs) {
+ if (
+ dialog._openedURL.endsWith(
+ `browsingContextId=${browserContextId}`
+ ) &&
+ dialog._openedURL.includes("screenshots.html")
+ ) {
+ return dialog;
+ }
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Closes the dialog box it it exists
+ * @param browser The selected browser
+ */
+ closeDialogBox(browser) {
+ let perBrowserState = this.browserToScreenshotsState.get(browser);
+ if (perBrowserState?.previewDialog) {
+ perBrowserState.previewDialog.close();
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Callback fired when the preview dialog window closes
+ * Will exit the screenshots UI if the `exitOnPreviewClose` flag is set for this browser
+ * @param browser The associated browser
+ */
+ onDialogClose(browser) {
+ let perBrowserState = this.browserToScreenshotsState.get(browser);
+ if (!perBrowserState) {
+ return;
+ }
+ delete perBrowserState.previewDialog;
+ if (perBrowserState?.exitOnPreviewClose) {
+ this.exit(browser);
+ }
+ },
+
+ /**
+ * Gets the screenshots button if it is visible, otherwise it will get the
+ * element that the screenshots button is nested under. If the screenshots
+ * button doesn't exist then we will default to the navigator toolbox.
+ * @param browser The selected browser
+ * @returns The anchor element for the ConfirmationHint
+ */
+ getWidgetAnchor(browser) {
+ let window = browser.ownerGlobal;
+ let widgetGroup = window.CustomizableUI.getWidget("screenshot-button");
+ let widget = widgetGroup?.forWindow(window);
+ let anchor = widget?.anchor;
+
+ // Check if the anchor exists and is visible
+ if (
+ !anchor ||
+ !anchor.isConnected ||
+ !window.isElementVisible(anchor.parentNode)
+ ) {
+ // Use the hamburger button if the screenshots button isn't available
+ anchor = browser.ownerDocument.getElementById("PanelUI-menu-button");
+ }
+ return anchor;
+ },
+
+ /**
+ * Indicate that the screenshot has been copied via ConfirmationHint.
+ * @param browser The selected browser
+ */
+ showCopiedConfirmationHint(browser) {
+ let anchor = this.getWidgetAnchor(browser);
+
+ browser.ownerGlobal.ConfirmationHint.show(
+ anchor,
+ "confirmation-hint-screenshot-copied"
+ );
+ },
+
+ /**
+ * Gets the full page bounds from the screenshots child actor.
+ * @param browser The current browser.
+ * @returns { object }
+ * Contains the full page bounds from the screenshots child actor.
+ */
+ fetchFullPageBounds(browser) {
+ let actor = this.getActor(browser);
+ return actor.sendQuery("Screenshots:getFullPageBounds");
+ },
+
+ /**
+ * Gets the visible bounds from the screenshots child actor.
+ * @param browser The current browser.
+ * @returns { object }
+ * Contains the visible bounds from the screenshots child actor.
+ */
+ fetchVisibleBounds(browser) {
+ let actor = this.getActor(browser);
+ return actor.sendQuery("Screenshots:getVisibleBounds");
+ },
+
+ showAlertMessage(title, message) {
+ lazy.AlertsService.showAlertNotification(null, title, message);
+ },
+
+ /**
+ * The max dimension of any side of a canvas is 32767 and the max canvas area is
+ * 124925329. If the width or height is greater or equal to 32766 we will crop the
+ * screenshot to the max width. If the area is still too large for the canvas
+ * we will adjust the height so we can successfully capture the screenshot.
+ * @param {Object} rect The dimensions of the screenshot. The rect will be
+ * modified in place
+ */
+ cropScreenshotRectIfNeeded(rect) {
+ let cropped = false;
+ let width = rect.width * rect.devicePixelRatio;
+ let height = rect.height * rect.devicePixelRatio;
+
+ if (width > MAX_CAPTURE_DIMENSION) {
+ width = MAX_CAPTURE_DIMENSION;
+ cropped = true;
+ }
+ if (height > MAX_CAPTURE_DIMENSION) {
+ height = MAX_CAPTURE_DIMENSION;
+ cropped = true;
+ }
+ if (width * height > MAX_CAPTURE_AREA) {
+ height = Math.floor(MAX_CAPTURE_AREA / width);
+ cropped = true;
+ }
+
+ rect.width = Math.floor(width / rect.devicePixelRatio);
+ rect.height = Math.floor(height / rect.devicePixelRatio);
+ rect.right = rect.left + rect.width;
+ rect.bottom = rect.top + rect.height;
+
+ if (cropped) {
+ let [errorTitle, errorMessage] =
+ lazy.screenshotsLocalization.formatMessagesSync([
+ { id: "screenshots-too-large-error-title" },
+ { id: "screenshots-too-large-error-details" },
+ ]);
+ this.showAlertMessage(errorTitle.value, errorMessage.value);
+ this.recordTelemetryEvent("failed", "screenshot_too_large", null);
+ }
+ },
+
+ /**
+ * Open and add screenshot-ui to the dialog box and then take the screenshot
+ * @param browser The current browser.
+ * @param type The type of screenshot taken.
+ */
+ async doScreenshot(browser, type) {
+ this.closePanel(browser);
+ this.closeOverlay(browser, { doNotResetMethods: true });
+
+ let dialog = await this.openPreviewDialog(browser);
+ await dialog._dialogReady;
+ let screenshotsUI =
+ dialog._frame.contentDocument.createElement("screenshots-ui");
+ dialog._frame.contentDocument.body.appendChild(screenshotsUI);
+
+ screenshotsUI.focusButton(lazy.SCREENSHOTS_LAST_SAVED_METHOD);
+
+ let rect;
+ let lastUsedMethod;
+ if (type === "full_page") {
+ rect = await this.fetchFullPageBounds(browser);
+ lastUsedMethod = "fullpage";
+ } else {
+ rect = await this.fetchVisibleBounds(browser);
+ lastUsedMethod = "visible";
+ }
+
+ Services.prefs.setStringPref(
+ SCREENSHOTS_LAST_SCREENSHOT_METHOD_PREF,
+ lastUsedMethod
+ );
+ this.methodsUsed[lastUsedMethod] += 1;
+ this.recordTelemetryEvent("selected", type, {});
+ return this.takeScreenshot(browser, dialog, rect);
+ },
+
+ /**
+ * Take the screenshot and add the image to the dialog box
+ * @param browser The current browser.
+ * @param dialog The dialog box to show the screenshot preview.
+ * @param rect DOMRect containing bounds of the screenshot.
+ */
+ async takeScreenshot(browser, dialog, rect) {
+ let canvas = await this.createCanvas(rect, browser);
+
+ let newImg = dialog._frame.contentDocument.createElement("img");
+ let url = canvas.toDataURL();
+
+ newImg.id = "placeholder-image";
+
+ newImg.src = url;
+ dialog._frame.contentDocument
+ .getElementById("preview-image-div")
+ .appendChild(newImg);
+
+ if (Cu.isInAutomation) {
+ Services.obs.notifyObservers(null, "screenshots-preview-ready");
+ }
+ },
+
+ /**
+ * Creates a canvas and draws a snapshot of the screenshot on the canvas
+ * @param region The bounds of screenshots
+ * @param browser The current browser
+ * @returns The canvas
+ */
+ async createCanvas(region, browser) {
+ region.left = Math.round(region.left);
+ region.right = Math.round(region.right);
+ region.top = Math.round(region.top);
+ region.bottom = Math.round(region.bottom);
+ region.width = Math.round(region.right - region.left);
+ region.height = Math.round(region.bottom - region.top);
+
+ this.cropScreenshotRectIfNeeded(region);
+
+ let { devicePixelRatio } = region;
+
+ let browsingContext = BrowsingContext.get(browser.browsingContext.id);
+
+ let canvas = browser.ownerDocument.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "html:canvas"
+ );
+ let context = canvas.getContext("2d");
+
+ canvas.width = region.width * devicePixelRatio;
+ canvas.height = region.height * devicePixelRatio;
+
+ for (
+ let startLeft = region.left;
+ startLeft < region.right;
+ startLeft += MAX_SNAPSHOT_DIMENSION
+ ) {
+ for (
+ let startTop = region.top;
+ startTop < region.bottom;
+ startTop += MAX_SNAPSHOT_DIMENSION
+ ) {
+ let height =
+ startTop + MAX_SNAPSHOT_DIMENSION > region.bottom
+ ? region.bottom - startTop
+ : MAX_SNAPSHOT_DIMENSION;
+ let width =
+ startLeft + MAX_SNAPSHOT_DIMENSION > region.right
+ ? region.right - startLeft
+ : MAX_SNAPSHOT_DIMENSION;
+ let rect = new DOMRect(startLeft, startTop, width, height);
+
+ let snapshot = await browsingContext.currentWindowGlobal.drawSnapshot(
+ rect,
+ devicePixelRatio,
+ "rgb(255,255,255)"
+ );
+
+ context.drawImage(
+ snapshot,
+ (startLeft - region.left) * devicePixelRatio,
+ (startTop - region.top) * devicePixelRatio,
+ width * devicePixelRatio,
+ height * devicePixelRatio
+ );
+
+ snapshot.close();
+ }
+ }
+
+ return canvas;
+ },
+
+ /**
+ * Copy the screenshot
+ * @param region The bounds of the screenshots
+ * @param browser The current browser
+ */
+ async copyScreenshotFromRegion(region, browser) {
+ let canvas = await this.createCanvas(region, browser);
+ let url = canvas.toDataURL();
+
+ await this.copyScreenshot(url, browser, {
+ object: "overlay_copy",
+ });
+ },
+
+ /**
+ * Copy the image to the clipboard
+ * This is called from the preview dialog
+ * @param dataUrl The image data
+ * @param browser The current browser
+ * @param data Telemetry data
+ */
+ async copyScreenshot(dataUrl, browser, data) {
+ // Guard against missing image data.
+ if (!dataUrl) {
+ return;
+ }
+
+ const imageTools = Cc["@mozilla.org/image/tools;1"].getService(
+ Ci.imgITools
+ );
+
+ const base64Data = dataUrl.replace("data:image/png;base64,", "");
+
+ const image = atob(base64Data);
+ const imgDecoded = imageTools.decodeImageFromBuffer(
+ image,
+ image.length,
+ "image/png"
+ );
+
+ const transferable = Cc[
+ "@mozilla.org/widget/transferable;1"
+ ].createInstance(Ci.nsITransferable);
+ transferable.init(null);
+ transferable.addDataFlavor("image/png");
+ transferable.setTransferData("image/png", imgDecoded);
+
+ Services.clipboard.setData(
+ transferable,
+ null,
+ Services.clipboard.kGlobalClipboard
+ );
+
+ this.showCopiedConfirmationHint(browser);
+
+ let extra = await this.getActor(browser).sendQuery(
+ "Screenshots:GetMethodsUsed"
+ );
+ this.recordTelemetryEvent("copy", data.object, {
+ ...extra,
+ ...this.methodsUsed,
+ });
+ this.resetMethodsUsed();
+
+ Services.prefs.setStringPref(SCREENSHOTS_LAST_SAVED_METHOD_PREF, "copy");
+ },
+
+ /**
+ * Download the screenshot
+ * @param title The title of the current page
+ * @param region The bounds of the screenshot
+ * @param browser The current browser
+ */
+ async downloadScreenshotFromRegion(title, region, browser) {
+ let canvas = await this.createCanvas(region, browser);
+ let dataUrl = canvas.toDataURL();
+
+ await this.downloadScreenshot(title, dataUrl, browser, {
+ object: "overlay_download",
+ });
+ },
+
+ /**
+ * Download the screenshot
+ * This is called from the preview dialog
+ * @param title The title of the current page or null and getFilename will get the title
+ * @param dataUrl The image data
+ * @param browser The current browser
+ * @param data Telemetry data
+ */
+ async downloadScreenshot(title, dataUrl, browser, data) {
+ // Guard against missing image data.
+ if (!dataUrl) {
+ return;
+ }
+
+ let filename = await getFilename(title, browser);
+
+ const targetFile = new lazy.FileUtils.File(filename);
+
+ // Create download and track its progress.
+ try {
+ const download = await lazy.Downloads.createDownload({
+ source: dataUrl,
+ target: targetFile,
+ });
+
+ let isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(
+ browser.ownerGlobal
+ );
+ const list = await lazy.Downloads.getList(
+ isPrivate ? lazy.Downloads.PRIVATE : lazy.Downloads.PUBLIC
+ );
+ // add the download to the download list in the Downloads list in the Browser UI
+ list.add(download);
+
+ // Await successful completion of the save via the download manager
+ await download.start();
+ } catch (ex) {}
+
+ let extra = await this.getActor(browser).sendQuery(
+ "Screenshots:GetMethodsUsed"
+ );
+ this.recordTelemetryEvent("download", data.object, {
+ ...extra,
+ ...this.methodsUsed,
+ });
+ this.resetMethodsUsed();
+
+ Services.prefs.setStringPref(
+ SCREENSHOTS_LAST_SAVED_METHOD_PREF,
+ "download"
+ );
+ },
+
+ recordTelemetryEvent(type, object, args) {
+ if (args) {
+ for (let key of Object.keys(args)) {
+ args[key] = args[key].toString();
+ }
+ }
+ Services.telemetry.recordEvent("screenshots", type, object, null, args);
+ },
+};
diff --git a/browser/components/screenshots/content/cancel.svg b/browser/components/screenshots/content/cancel.svg
new file mode 100644
index 0000000000..0c176be25f
--- /dev/null
+++ b/browser/components/screenshots/content/cancel.svg
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M10.5 8.7L5.2 3.3c-.5-.5-1.3-.5-1.8 0s-.5 1.3 0 1.8l5.3 5.3-5.3 5.3c-.5.5-.5 1.3 0 1.8s1.3.5 1.8 0l5.3-5.3 5.3 5.3c.5.5 1.3.5 1.8 0s.5-1.3 0-1.8l-5.3-5.3 5.3-5.3c.5-.5.5-1.3 0-1.8s-1.3-.5-1.8 0l-5.3 5.4z" fill="context-fill #3e3d40"/></svg>
diff --git a/browser/components/screenshots/content/copied-notification.svg b/browser/components/screenshots/content/copied-notification.svg
new file mode 100644
index 0000000000..2310b41aef
--- /dev/null
+++ b/browser/components/screenshots/content/copied-notification.svg
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><path fill="context-fill" d="M44.121 24.879l-9-9A3 3 0 0 0 33 15h-3v-3a3 3 0 0 0-.879-2.121l-9-9A3 3 0 0 0 18 0H9a6 6 0 0 0-6 6v21a6 6 0 0 0 6 6h9v9a6 6 0 0 0 6 6h15a6 6 0 0 0 6-6V27a3 3 0 0 0-.879-2.121zM37.758 27H33v-4.758zm-15-15H18V7.242zM18 21v6H9V6h6v7.5a1.5 1.5 0 0 0 1.5 1.5H24a6 6 0 0 0-6 6zm6 21V21h6v7.5a1.5 1.5 0 0 0 1.5 1.5H39v12z"/></svg> \ No newline at end of file
diff --git a/browser/components/screenshots/content/copy.svg b/browser/components/screenshots/content/copy.svg
new file mode 100644
index 0000000000..3e3d49122c
--- /dev/null
+++ b/browser/components/screenshots/content/copy.svg
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="context-fill #3e3d40" d="M14.707 8.293l-3-3A1 1 0 0 0 11 5h-1V4a1 1 0 0 0-.293-.707l-3-3A1 1 0 0 0 6 0H3a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3v3a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V9a1 1 0 0 0-.293-.707zM12.586 9H11V7.414zm-5-5H6V2.414zM6 7v2H3V2h2v2.5a.5.5 0 0 0 .5.5H8a2 2 0 0 0-2 2zm2 7V7h2v2.5a.5.5 0 0 0 .5.5H13v4z"/></svg>
diff --git a/browser/components/screenshots/content/download-white.svg b/browser/components/screenshots/content/download-white.svg
new file mode 100644
index 0000000000..bb6a7de845
--- /dev/null
+++ b/browser/components/screenshots/content/download-white.svg
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.1 12L4.9 7.9c-.5-.5-1.3-.5-1.8 0s-.5 1.3 0 1.8l6.2 6.2c.5.5 1.3.5 1.8 0l6.2-6.2c.5-.5.5-1.3 0-1.8s-1.3-.5-1.8 0L11.6 12V1.2C11.6.6 11 0 10.3 0c-.7 0-1.2.6-1.2 1.2V12zM4 20c-.7 0-1.2-.6-1.2-1.2s.6-1.2 1.2-1.2h12.5c.7 0 1.2.6 1.2 1.2s-.5 1.2-1.2 1.2H4z" fill="#fff"/></svg> \ No newline at end of file
diff --git a/browser/components/screenshots/content/download.svg b/browser/components/screenshots/content/download.svg
new file mode 100644
index 0000000000..a85f745937
--- /dev/null
+++ b/browser/components/screenshots/content/download.svg
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.1 12L4.9 7.9c-.5-.5-1.3-.5-1.8 0s-.5 1.3 0 1.8l6.2 6.2c.5.5 1.3.5 1.8 0l6.2-6.2c.5-.5.5-1.3 0-1.8s-1.3-.5-1.8 0L11.6 12V1.2C11.6.6 11 0 10.3 0c-.7 0-1.2.6-1.2 1.2V12zM4 20c-.7 0-1.2-.6-1.2-1.2s.6-1.2 1.2-1.2h12.5c.7 0 1.2.6 1.2 1.2s-.5 1.2-1.2 1.2H4z" fill="context-fill #3e3d40"/></svg>
diff --git a/browser/components/screenshots/content/icon-welcome-face-without-eyes.svg b/browser/components/screenshots/content/icon-welcome-face-without-eyes.svg
new file mode 100644
index 0000000000..138308af57
--- /dev/null
+++ b/browser/components/screenshots/content/icon-welcome-face-without-eyes.svg
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><style>.st0{fill:#fff}</style><g id="Visual-design"><g id="_x31_.2-Div-selection" transform="translate(-575 -503)"><g id="Introduction" transform="translate(250 503)"><g id="icon-welcomeface" transform="translate(325)"><g id="Layer_1_1_"><path id="Shape" class="st0" d="M11.4.9v2.9h-6c-.9 0-1.5.8-1.5 1.5v6H.8V3.8C.8 2.1 2.2.7 3.9.7h7.6v.2h-.1z"/><path id="Shape_1_" class="st0" d="M63.2 11.4h-3.1v-6c0-.8-.6-1.5-1.5-1.5h-6v-3h7.6c1.7 0 3.1 1.4 3.1 3.1l-.1 7.4z"/><path id="Shape_2_" class="st0" d="M52.6 63.2v-3.1h6c.9 0 1.5-.6 1.5-1.5v-6h3.1v7.6c0 1.7-1.4 3.1-3.1 3.1l-7.5-.1z"/><path id="Shape_3_" class="st0" d="M.8 52.7h3.1v6c0 .9.6 1.5 1.5 1.5h6v3.1H3.8c-1.7 0-3.1-1.4-3.1-3.1l.1-7.5z"/><path id="Shape_6_" class="st0" d="M33.3 49.2H33c-4.6-.1-7.8-3.6-7.9-3.8-.6-.8-.6-2 .1-2.7.8-.8 1.9-.6 2.6.1 0 0 2.3 2.6 5.2 2.6 1.8 0 3.6-.9 5.2-2.6.8-.8 1.9-.8 2.7 0s.8 1.9 0 2.7c-2.2 2.4-4.9 3.7-7.6 3.7z"/></g></g></g></g></g></svg> \ No newline at end of file
diff --git a/browser/components/screenshots/content/menu-fullpage.svg b/browser/components/screenshots/content/menu-fullpage.svg
new file mode 100644
index 0000000000..6552ef8cdd
--- /dev/null
+++ b/browser/components/screenshots/content/menu-fullpage.svg
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 46 46"><path id="bg" d="M7 42h32V5.1H7z" fill="context-stroke #00fdff"/><g id="frame" transform="translate(0 6)"><path d="M40 5c.5 0 1 .4 1 1v24c0 .5-.5 1-1 1H6c-.6 0-1-.5-1-1V6c0-.6.4-1 1-1h34zM7 29h32V7H7v22z" fill="context-fill"/><path id="Fill-4" fill="context-fill" d="M7 7h32V5H7z"/><path id="Fill-6" fill="context-fill" d="M7 31h32v-2H7z"/></g><path id="dash" d="M38 11h1V9h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm-1 1h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-2-3H7v3h2v-1H8v-2zm-1-1h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1V9H7v2zm2-6H7v3h1V6h1V5zm1 1h2V5h-2v1zm3 0h2V5h-2v1zm3 0h2V5h-2v1zm3 0h2V5h-2v1zm3 0h2V5h-2v1zm3 0h2V5h-2v1zm3 0h2V5h-2v1zm3 0h2V5h-2v1zm3 0h2V5h-2v1zm5-1h-2v1h1v2h1V5z" fill="context-fill" opacity="0.5"/></svg> \ No newline at end of file
diff --git a/browser/components/screenshots/content/menu-visible.svg b/browser/components/screenshots/content/menu-visible.svg
new file mode 100644
index 0000000000..98cb1bfd3e
--- /dev/null
+++ b/browser/components/screenshots/content/menu-visible.svg
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 46 46"><path d="M5 12c0-.6.5-1 1-1h34c.6 0 1 .5 1 1v24c0 .6-.5 1-1 1H6c-.6 0-1-.5-1-1V12zm2 23V13h32v22H7z" fill="context-fill"/><path d="M7 35h32V13H7z" fill="context-stroke #00fdff"/><path id="dash" d="M38 19h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm0 3h1v-2h-1v2zm-1 1h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-3 0h2v-1h-2v1zm-2-3H7v3h2v-1H8v-2zm-1-1h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm0-3h1v-2H7v2zm2-6H7v3h1v-2h1v-1zm1 1h2v-1h-2v1zm3 0h2v-1h-2v1zm3 0h2v-1h-2v1zm3 0h2v-1h-2v1zm3 0h2v-1h-2v1zm3 0h2v-1h-2v1zm3 0h2v-1h-2v1zm3 0h2v-1h-2v1zm3 0h2v-1h-2v1zm5-1h-2v1h1v2h1v-3z" fill="context-fill" opacity="0.5"/></svg> \ No newline at end of file
diff --git a/browser/components/screenshots/content/screenshots.css b/browser/components/screenshots/content/screenshots.css
new file mode 100644
index 0000000000..506f3658c9
--- /dev/null
+++ b/browser/components/screenshots/content/screenshots.css
@@ -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/. */
+
+html,
+body {
+ height: 100vh;
+ width: 100vw;
+}
+
+.image-view {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.preview-buttons {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ width: 100%;
+ border: 0;
+ box-sizing: border-box;
+ margin: 4px 0;
+ margin-inline-start: calc(-2% + 4px);
+}
+
+.preview-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ text-align: center;
+ user-select: none;
+ white-space: nowrap;
+ min-height: 36px;
+ font-size: 15px;
+ min-width: 36px;
+}
+
+.preview-button > img {
+ -moz-context-properties: fill;
+ fill: currentColor;
+ width: 16px;
+ height: 16px;
+}
+
+#download > img,
+#copy > img {
+ margin-inline-end: 5px;
+}
+
+.preview-image {
+ height: 100%;
+ width: 100%;
+ overflow: auto;
+}
+
+#preview-image-div {
+ margin: 2%;
+ margin-top: 0;
+}
+
+#placeholder-image {
+ width: 100%;
+ height: 100%;
+}
diff --git a/browser/components/screenshots/content/screenshots.html b/browser/components/screenshots/content/screenshots.html
new file mode 100644
index 0000000000..88c71fb4fe
--- /dev/null
+++ b/browser/components/screenshots/content/screenshots.html
@@ -0,0 +1,68 @@
+<!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></title>
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src chrome:;img-src data:; object-src 'none'"
+ />
+
+ <link rel="localization" href="browser/screenshots.ftl" />
+
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/screenshots/screenshots.css"
+ />
+ <script
+ defer
+ src="chrome://browser/content/screenshots/screenshots.js"
+ ></script>
+ </head>
+
+ <body>
+ <template id="screenshots-dialog-template">
+ <div class="image-view">
+ <div class="preview-buttons">
+ <button
+ id="retry"
+ class="preview-button"
+ data-l10n-id="screenshots-retry-button-title"
+ >
+ <img src="chrome://global/skin/icons/reload.svg" />
+ </button>
+ <button
+ id="cancel"
+ class="preview-button"
+ data-l10n-id="screenshots-cancel-button-title"
+ >
+ <img src="chrome://global/skin/icons/close.svg" />
+ </button>
+ <button
+ id="copy"
+ class="preview-button"
+ data-l10n-id="screenshots-copy-button-title"
+ >
+ <img src="chrome://global/skin/icons/edit-copy.svg" />
+ <span data-l10n-id="screenshots-copy-button" />
+ </button>
+ <button
+ id="download"
+ class="preview-button primary"
+ data-l10n-id="screenshots-download-button-title"
+ >
+ <img src="chrome://browser/skin/downloads/downloads.svg" />
+ <span data-l10n-id="screenshots-download-button" />
+ </button>
+ </div>
+ <div class="preview-image">
+ <div id="preview-image-div"></div>
+ </div>
+ </div>
+ </template>
+ </body>
+</html>
diff --git a/browser/components/screenshots/content/screenshots.js b/browser/components/screenshots/content/screenshots.js
new file mode 100644
index 0000000000..9e47570e07
--- /dev/null
+++ b/browser/components/screenshots/content/screenshots.js
@@ -0,0 +1,105 @@
+/* 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/. */
+/* eslint-env mozilla/browser-window */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
+});
+
+class ScreenshotsUI extends HTMLElement {
+ constructor() {
+ super();
+ // we get passed the <browser> as a param via TabDialogBox.open()
+ this.openerBrowser = window.arguments[0];
+ }
+ async connectedCallback() {
+ this.initialize();
+ }
+
+ initialize() {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+ let template = this.ownerDocument.getElementById(
+ "screenshots-dialog-template"
+ );
+ let templateContent = template.content;
+ this.appendChild(templateContent.cloneNode(true));
+
+ this._retryButton = this.querySelector("#retry");
+ this._retryButton.addEventListener("click", this);
+ this._cancelButton = this.querySelector("#cancel");
+ this._cancelButton.addEventListener("click", this);
+ this._copyButton = this.querySelector("#copy");
+ this._copyButton.addEventListener("click", this);
+ this._downloadButton = this.querySelector("#download");
+ this._downloadButton.addEventListener("click", this);
+ }
+
+ close() {
+ URL.revokeObjectURL(document.getElementById("placeholder-image").src);
+ window.close();
+ }
+
+ async handleEvent(event) {
+ if (event.type == "click" && event.currentTarget == this._cancelButton) {
+ this.close();
+ ScreenshotsUtils.recordTelemetryEvent("canceled", "preview_cancel", {});
+ } else if (
+ event.type == "click" &&
+ event.currentTarget == this._copyButton
+ ) {
+ this.saveToClipboard(
+ this.ownerDocument.getElementById("placeholder-image").src
+ );
+ } else if (
+ event.type == "click" &&
+ event.currentTarget == this._downloadButton
+ ) {
+ await this.saveToFile(
+ this.ownerDocument.getElementById("placeholder-image").src
+ );
+ } else if (
+ event.type == "click" &&
+ event.currentTarget == this._retryButton
+ ) {
+ ScreenshotsUtils.scheduleRetry(this.openerBrowser, "preview_retry");
+ this.close();
+ }
+ }
+
+ async saveToFile(dataUrl) {
+ await ScreenshotsUtils.downloadScreenshot(
+ null,
+ dataUrl,
+ this.openerBrowser,
+ { object: "preview_download" }
+ );
+ this.close();
+ }
+
+ async saveToClipboard(dataUrl) {
+ await ScreenshotsUtils.copyScreenshot(dataUrl, this.openerBrowser, {
+ object: "preview_copy",
+ });
+ this.close();
+ }
+
+ /**
+ * Set the focus to the most recent saved method.
+ * This will default to the download button.
+ * @param {String} buttonToFocus
+ */
+ focusButton(buttonToFocus) {
+ if (buttonToFocus === "copy") {
+ this._copyButton.focus({ focusVisible: true });
+ } else {
+ this._downloadButton.focus({ focusVisible: true });
+ }
+ }
+}
+customElements.define("screenshots-ui", ScreenshotsUI);
diff --git a/browser/components/screenshots/fileHelpers.mjs b/browser/components/screenshots/fileHelpers.mjs
new file mode 100644
index 0000000000..42cb868bea
--- /dev/null
+++ b/browser/components/screenshots/fileHelpers.mjs
@@ -0,0 +1,269 @@
+/* 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 { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ DownloadLastDir: "resource://gre/modules/DownloadLastDir.sys.mjs",
+ DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
+});
+
+/**
+ * Gets the filename automatically or by a file picker depending on "browser.download.useDownloadDir"
+ * @param filenameTitle The title of the current page
+ * @param browser The current browser
+ * @returns Path of the chosen filename
+ */
+export async function getFilename(filenameTitle, browser) {
+ if (filenameTitle === null) {
+ filenameTitle = await lazy.ScreenshotsUtils.getActor(browser).sendQuery(
+ "Screenshots:getDocumentTitle"
+ );
+ }
+ const date = new Date();
+ /* eslint-disable no-control-regex */
+ filenameTitle = filenameTitle
+ .replace(/[\\/]/g, "_")
+ .replace(/[\u200e\u200f\u202a-\u202e]/g, "")
+ .replace(/[\x00-\x1f\x7f-\x9f:*?|"<>;,+=\[\]]+/g, " ")
+ .replace(/^[\s\u180e.]+|[\s\u180e.]+$/g, "");
+ /* eslint-enable no-control-regex */
+ filenameTitle = filenameTitle.replace(/\s{1,4000}/g, " ");
+ const currentDateTime = new Date(
+ date.getTime() - date.getTimezoneOffset() * 60 * 1000
+ ).toISOString();
+ const filenameDate = currentDateTime.substring(0, 10);
+ const filenameTime = currentDateTime.substring(11, 19).replace(/:/g, "-");
+ let clipFilename = `Screenshot ${filenameDate} at ${filenameTime} ${filenameTitle}`;
+
+ // Crop the filename size at less than 246 bytes, so as to leave
+ // room for the extension and an ellipsis [...]. Note that JS
+ // strings are UTF16 but the filename will be converted to UTF8
+ // when saving which could take up more space, and we want a
+ // maximum of 255 bytes (not characters). Here, we iterate
+ // and crop at shorter and shorter points until we fit into
+ // 255 bytes.
+ let suffix = "";
+ for (let cropSize = 246; cropSize >= 0; cropSize -= 32) {
+ if (new Blob([clipFilename]).size > 246) {
+ clipFilename = clipFilename.substring(0, cropSize);
+ suffix = "[...]";
+ } else {
+ break;
+ }
+ }
+
+ clipFilename += suffix;
+
+ let extension = ".png";
+ let filename = clipFilename + extension;
+
+ let useDownloadDir = Services.prefs.getBoolPref(
+ "browser.download.useDownloadDir"
+ );
+ if (useDownloadDir) {
+ const downloadsDir = await lazy.Downloads.getPreferredDownloadsDirectory();
+ const downloadsDirExists = await IOUtils.exists(downloadsDir);
+ if (downloadsDirExists) {
+ // If filename is absolute, it will override the downloads directory and
+ // still be applied as expected.
+ filename = PathUtils.join(downloadsDir, filename);
+ }
+ } else {
+ let fileInfo = new FileInfo(filename);
+ let file;
+
+ let fpParams = {
+ fpTitleKey: "SaveImageTitle",
+ fileInfo,
+ contentType: "image/png",
+ saveAsType: 0,
+ file,
+ };
+
+ let accepted = await promiseTargetFile(fpParams, browser.ownerGlobal);
+ if (!accepted) {
+ return null;
+ }
+
+ filename = fpParams.file.path;
+ }
+
+ return filename;
+}
+
+// The below functions are a modified copy from toolkit/content/contentAreaUtils.js
+/**
+ * Structure for holding info about a URL and the target filename it should be
+ * saved to.
+ * @param aFileName The target filename
+ */
+class FileInfo {
+ constructor(aFileName) {
+ this.fileName = aFileName;
+ this.fileBaseName = aFileName.replace(".png", "");
+ this.fileExt = "png";
+ }
+}
+
+const ContentAreaUtils = {
+ get stringBundle() {
+ delete this.stringBundle;
+ return (this.stringBundle = Services.strings.createBundle(
+ "chrome://global/locale/contentAreaCommands.properties"
+ ));
+ },
+};
+
+function makeFilePicker() {
+ const fpContractID = "@mozilla.org/filepicker;1";
+ const fpIID = Ci.nsIFilePicker;
+ return Cc[fpContractID].createInstance(fpIID);
+}
+
+function getMIMEService() {
+ const mimeSvcContractID = "@mozilla.org/mime;1";
+ const mimeSvcIID = Ci.nsIMIMEService;
+ const mimeSvc = Cc[mimeSvcContractID].getService(mimeSvcIID);
+ return mimeSvc;
+}
+
+function getMIMEInfoForType(aMIMEType, aExtension) {
+ if (aMIMEType || aExtension) {
+ try {
+ return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension);
+ } catch (e) {}
+ }
+ return null;
+}
+
+// This is only used after the user has entered a filename.
+function validateFileName(aFileName) {
+ let processed =
+ lazy.DownloadPaths.sanitize(aFileName, {
+ compressWhitespaces: false,
+ allowInvalidFilenames: true,
+ }) || "_";
+ if (AppConstants.platform == "android") {
+ // If a large part of the filename has been sanitized, then we
+ // will use a default filename instead
+ if (processed.replace(/_/g, "").length <= processed.length / 2) {
+ // We purposefully do not use a localized default filename,
+ // which we could have done using
+ // ContentAreaUtils.stringBundle.GetStringFromName("UntitledSaveFileName")
+ // since it may contain invalid characters.
+ let original = processed;
+ processed = "download";
+
+ // Preserve a suffix, if there is one
+ if (original.includes(".")) {
+ let suffix = original.split(".").pop();
+ if (suffix && !suffix.includes("_")) {
+ processed += "." + suffix;
+ }
+ }
+ }
+ }
+ return processed;
+}
+
+function appendFiltersForContentType(
+ aFilePicker,
+ aContentType,
+ aFileExtension
+) {
+ let mimeInfo = getMIMEInfoForType(aContentType, aFileExtension);
+ if (mimeInfo) {
+ let extString = "";
+ for (let extension of mimeInfo.getFileExtensions()) {
+ if (extString) {
+ extString += "; ";
+ } // If adding more than one extension,
+ // separate by semi-colon
+ extString += "*." + extension;
+ }
+
+ if (extString) {
+ aFilePicker.appendFilter(mimeInfo.description, extString);
+ }
+ }
+
+ // Always append the all files (*) filter
+ aFilePicker.appendFilters(Ci.nsIFilePicker.filterAll);
+}
+
+/**
+ * Given the Filepicker Parameters (aFpP), show the file picker dialog,
+ * prompting the user to confirm (or change) the fileName.
+ * @param aFpP
+ * A structure (see definition in internalSave(...) method)
+ * containing all the data used within this method.
+ * @param win
+ * The window used for opening the file picker
+ * @return Promise
+ * @resolve a boolean. When true, it indicates that the file picker dialog
+ * is accepted.
+ */
+function promiseTargetFile(aFpP, win) {
+ return (async function () {
+ let downloadLastDir = new lazy.DownloadLastDir(win);
+
+ // Default to the user's default downloads directory configured
+ // through download prefs.
+ let dirPath = await lazy.Downloads.getPreferredDownloadsDirectory();
+ let dirExists = await IOUtils.exists(dirPath);
+ let dir = new lazy.FileUtils.File(dirPath);
+
+ // We must prompt for the file name explicitly.
+ // If we must prompt because we were asked to...
+ let file = await downloadLastDir.getFileAsync(null);
+ if (file && (await IOUtils.exists(file.path))) {
+ dir = file;
+ dirExists = true;
+ }
+
+ if (!dirExists) {
+ // Default to desktop.
+ dir = Services.dirsvc.get("Desk", Ci.nsIFile);
+ }
+
+ let fp = makeFilePicker();
+ let titleKey = aFpP.fpTitleKey;
+ fp.init(
+ win,
+ ContentAreaUtils.stringBundle.GetStringFromName(titleKey),
+ Ci.nsIFilePicker.modeSave
+ );
+
+ fp.displayDirectory = dir;
+ fp.defaultExtension = aFpP.fileInfo.fileExt;
+ fp.defaultString = aFpP.fileInfo.fileName;
+ appendFiltersForContentType(fp, aFpP.contentType, aFpP.fileInfo.fileExt);
+
+ let result = await new Promise(resolve => {
+ fp.open(function (aResult) {
+ resolve(aResult);
+ });
+ });
+ if (result == Ci.nsIFilePicker.returnCancel || !fp.file) {
+ return false;
+ }
+
+ // Do not store the last save directory as a pref inside the private browsing mode
+ downloadLastDir.setFile(null, fp.file.parent);
+
+ aFpP.saveAsType = fp.filterIndex;
+ aFpP.file = fp.file;
+ aFpP.file.leafName = validateFileName(aFpP.file.leafName);
+
+ return true;
+ })();
+}
diff --git a/browser/components/screenshots/jar.mn b/browser/components/screenshots/jar.mn
new file mode 100644
index 0000000000..7a4e2ed73a
--- /dev/null
+++ b/browser/components/screenshots/jar.mn
@@ -0,0 +1,24 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+browser.jar:
+ content/browser/screenshots/cancel.svg (content/cancel.svg)
+ content/browser/screenshots/copied-notification.svg (content/copied-notification.svg)
+ content/browser/screenshots/copy.svg (content/copy.svg)
+ content/browser/screenshots/download-white.svg (content/download-white.svg)
+ content/browser/screenshots/download.svg (content/download.svg)
+ content/browser/screenshots/fileHelpers.mjs
+ content/browser/screenshots/icon-welcome-face-without-eyes.svg (content/icon-welcome-face-without-eyes.svg)
+ content/browser/screenshots/menu-fullpage.svg (content/menu-fullpage.svg)
+ content/browser/screenshots/menu-visible.svg (content/menu-visible.svg)
+ content/browser/screenshots/screenshots.js (content/screenshots.js)
+ content/browser/screenshots/screenshots-buttons.js (screenshots-buttons.js)
+ content/browser/screenshots/screenshots-buttons.css (screenshots-buttons.css)
+ content/browser/screenshots/screenshots.css (content/screenshots.css)
+ content/browser/screenshots/screenshots.html (content/screenshots.html)
+ content/browser/screenshots/overlay/ (overlay/**)
+ content/browser/screenshots/overlayHelpers.mjs
+
+
+% content screenshots-overlay %overlay/
diff --git a/browser/components/screenshots/moz.build b/browser/components/screenshots/moz.build
new file mode 100644
index 0000000000..c66eed8f49
--- /dev/null
+++ b/browser/components/screenshots/moz.build
@@ -0,0 +1,20 @@
+# -*- 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/.
+
+EXTRA_JS_MODULES += [
+ "ScreenshotsHelperChild.sys.mjs",
+ "ScreenshotsOverlayChild.sys.mjs",
+ "ScreenshotsUtils.sys.mjs",
+]
+
+JAR_MANIFESTS += ["jar.mn"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Screenshots")
+
+BROWSER_CHROME_MANIFESTS += [
+ "tests/browser/browser.toml",
+]
diff --git a/browser/components/screenshots/overlay/overlay.css b/browser/components/screenshots/overlay/overlay.css
new file mode 100644
index 0000000000..6eeda8b44c
--- /dev/null
+++ b/browser/components/screenshots/overlay/overlay.css
@@ -0,0 +1,349 @@
+/* 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 url("chrome://global/skin/in-content/common.css");
+
+:host {
+ display: contents;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+#screenshots-component {
+ position: absolute;
+ inset: 0;
+ font: message-box;
+ user-select: none;
+ touch-action: none;
+ pointer-events: auto;
+ cursor: crosshair;
+
+ &[dragging] {
+ cursor: grabbing;
+ }
+
+ &[resizing] {
+ position: fixed;
+ width: 100% !important;
+ height: 100% !important;
+ }
+}
+
+#selection-container {
+ overflow: clip;
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+}
+
+#preview-container {
+ overflow: clip;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ position: sticky;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background-color: rgba(0, 0, 0, 0.7);
+}
+
+#buttons-container {
+ position: absolute;
+ margin: 10px 0;
+ cursor: auto;
+}
+
+#selection-size,
+#buttons-container {
+ padding: 4px;
+ background-color: var(--in-content-page-background);
+ color: var(--in-content-text-color);
+ border-radius: 4px;
+}
+
+.buttons-wrapper,
+#selection-size-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.screenshots-button {
+ display: inline-flex;
+ align-items: center;
+ cursor: pointer;
+ text-align: center;
+ user-select: none;
+ white-space: nowrap;
+ z-index: 6;
+ min-width: 32px;
+ margin-inline: 4px;
+}
+
+#selection-size-container {
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+}
+
+#screenshots-cancel-button {
+ margin-top: 20px;
+ border-color: #fff;
+ color: #fff;
+
+ @media (prefers-contrast) {
+ background-color: var(--in-content-button-background);
+ color: var(--in-content-button-text-color);
+ border-color: var(--in-content-button-border-color);
+ }
+}
+
+#screenshots-cancel-button:hover {
+ background-color: #fff;
+ color: #000;
+
+ @media (prefers-contrast) {
+ background-color: var(--in-content-button-background-hover);
+ color: var(--in-content-button-text-color-hover);
+ border-color: var(--in-content-button-border-color-hover);
+ }
+}
+
+.screenshots-button > img {
+ -moz-context-properties: fill;
+ fill: currentColor;
+ width: 16px;
+ height: 16px;
+ pointer-events: none;
+}
+
+#cancel > img {
+ content: url("chrome://global/skin/icons/close.svg");
+}
+
+#copy > img {
+ content: url("chrome://global/skin/icons/edit-copy.svg");
+}
+
+#download > img {
+ content: url("chrome://browser/skin/downloads/downloads.svg");
+}
+
+#download > img,
+#copy > img {
+ margin-inline-end: 5px;
+}
+
+.face-container {
+ position: relative;
+ width: 64px;
+ height: 64px;
+}
+
+.face {
+ width: 62px;
+ height: 62px;
+ display: block;
+ background-image: url("chrome://browser/content/screenshots/icon-welcome-face-without-eyes.svg");
+}
+
+.eye {
+ background-color: #fff;
+ width: 10px;
+ height: 14px;
+ position: absolute;
+ border-radius: 100%;
+ overflow: hidden;
+ inset-inline-start: 16px;
+ top: 19px;
+}
+
+.eyeball {
+ position: absolute;
+ width: 6px;
+ height: 6px;
+ background-color: #000;
+ border-radius: 50%;
+ inset-inline-start: 2px;
+ top: 4px;
+ z-index: 10;
+}
+
+.left {
+ margin-inline-start: 0;
+}
+
+.right {
+ margin-inline-start: 20px;
+}
+
+.preview-instructions {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: pulse 125ms cubic-bezier(0.07, 0.95, 0, 1);
+ color: #fff;
+ font-size: 24px;
+ line-height: 32px;
+ text-align: center;
+ padding: 20px;
+ width: 400px;
+
+ @media (prefers-contrast) {
+ color: CanvasText;
+ background-color: Canvas;
+ }
+}
+
+#hover-highlight {
+ animation: fade-in 125ms forwards cubic-bezier(0.07, 0.95, 0, 1);
+ background: rgba(255, 255, 255, 0.2);
+ border: 2px dashed rgba(255, 255, 255, 0.4);
+ border-radius: 1px;
+ box-sizing: border-box;
+ pointer-events: none;
+ position: absolute;
+ z-index: 11;
+}
+
+#top-background {
+ top: 0;
+ left: 0;
+ width: 100%;
+}
+
+#left-background {
+ left: 0;
+}
+
+#bottom-background {
+ left: 0;
+ width: 100%;
+}
+
+.bghighlight {
+ background-color: rgba(0, 0, 0, 0.7);
+ position: absolute;
+ overflow: clip;
+ pointer-events: none;
+ /* FIXME(bug 1859421): This shouldn't be needed */
+ z-index: -1;
+}
+
+.highlight {
+ border: 2px dashed rgba(255, 255, 255, 0.8);
+ box-sizing: border-box;
+ cursor: move;
+ position: absolute;
+ pointer-events: auto;
+ z-index: 2;
+ outline-offset: 8px;
+}
+
+.mover-target {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ z-index: 5;
+ pointer-events: auto;
+ outline-offset: -15px;
+}
+
+.mover-target.direction-topLeft {
+ cursor: nwse-resize;
+ height: 60px;
+ left: -30px;
+ top: -30px;
+ width: 60px;
+}
+
+.mover-target.direction-top {
+ cursor: ns-resize;
+ height: 60px;
+ inset-inline-start: 0;
+ top: -30px;
+ width: 100%;
+ z-index: 4;
+}
+
+.mover-target.direction-topRight {
+ cursor: nesw-resize;
+ height: 60px;
+ right: -30px;
+ top: -30px;
+ width: 60px;
+}
+
+.mover-target.direction-left {
+ cursor: ew-resize;
+ height: 100%;
+ left: -30px;
+ top: 0;
+ width: 60px;
+ z-index: 4;
+}
+
+.mover-target.direction-right {
+ cursor: ew-resize;
+ height: 100%;
+ right: -30px;
+ top: 0;
+ width: 60px;
+ z-index: 4;
+}
+
+.mover-target.direction-bottomLeft {
+ bottom: -30px;
+ cursor: nesw-resize;
+ height: 60px;
+ left: -30px;
+ width: 60px;
+}
+
+.mover-target.direction-bottom {
+ bottom: -30px;
+ cursor: ns-resize;
+ height: 60px;
+ inset-inline-start: 0;
+ width: 100%;
+ z-index: 4;
+}
+
+.mover-target.direction-bottomRight {
+ bottom: -30px;
+ cursor: nwse-resize;
+ height: 60px;
+ right: -30px;
+ width: 60px;
+}
+
+.mover-target:hover .mover {
+ transform: scale(1.05);
+}
+
+.mover {
+ background-color: #fff;
+ border-radius: 50%;
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
+ transition: transform 125ms cubic-bezier(0.07, 0.95, 0, 1);
+ position: relative;
+ height: 16px;
+ width: 16px;
+ pointer-events: none;
+
+ @media (prefers-contrast) {
+ background-color: ButtonText;
+ }
+}
+
+.small-selection .mover {
+ height: 10px;
+ width: 10px;
+}
diff --git a/browser/components/screenshots/overlayHelpers.mjs b/browser/components/screenshots/overlayHelpers.mjs
new file mode 100644
index 0000000000..70a1bd86d0
--- /dev/null
+++ b/browser/components/screenshots/overlayHelpers.mjs
@@ -0,0 +1,497 @@
+/* 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/. */
+
+// An autoselection smaller than these will be ignored entirely:
+const MIN_DETECT_ABSOLUTE_HEIGHT = 10;
+const MIN_DETECT_ABSOLUTE_WIDTH = 30;
+// An autoselection smaller than these will not be preferred:
+const MIN_DETECT_HEIGHT = 30;
+const MIN_DETECT_WIDTH = 100;
+// An autoselection bigger than either of these will be ignored:
+let MAX_DETECT_HEIGHT = 700;
+let MAX_DETECT_WIDTH = 1000;
+
+const doNotAutoselectTags = {
+ H1: true,
+ H2: true,
+ H3: true,
+ H4: true,
+ H5: true,
+ H6: true,
+};
+
+/**
+ * Gets the rect for an element if getBoundingClientRect exists
+ * @param ele The element to get the rect from
+ * @returns The bounding client rect of the element or null
+ */
+function getBoundingClientRect(ele) {
+ if (!ele.getBoundingClientRect) {
+ return null;
+ }
+
+ return ele.getBoundingClientRect();
+}
+
+export function setMaxDetectHeight(maxHeight) {
+ MAX_DETECT_HEIGHT = maxHeight;
+}
+
+export function setMaxDetectWidth(maxWidth) {
+ MAX_DETECT_WIDTH = maxWidth;
+}
+
+/**
+ * This function will try to get an element from a given point in the doc.
+ * This function is recursive because when sending a message to the
+ * ScreenshotsHelper, the ScreenshotsHelper will call into this function.
+ * This only occurs when the element at the given point is an iframe.
+ *
+ * If the element is an iframe, we will send a message to the ScreenshotsHelper
+ * actor in the correct context to get the element at the given point.
+ * The message will return the "getBestRectForElement" for the element at the
+ * given point.
+ *
+ * If the element is not an iframe, then we will just return the element.
+ *
+ * @param {Number} x The x coordinate
+ * @param {Number} y The y coordinate
+ * @param {Document} doc The document
+ * @returns {Object}
+ * ele: The element for a given point (x, y)
+ * rect: The rect for the given point if ele is an iframe
+ * otherwise null
+ */
+export async function getElementFromPoint(x, y, doc) {
+ let ele = null;
+ let rect = null;
+ try {
+ ele = doc.elementFromPoint(x, y);
+ // if the element is an iframe, we need to send a message to that browsing context
+ // to get the coordinates of the element in the iframe
+ if (doc.defaultView.HTMLIFrameElement.isInstance(ele)) {
+ let actor =
+ ele.browsingContext.parentWindowContext.windowGlobalChild.getActor(
+ "ScreenshotsHelper"
+ );
+ rect = await actor.sendQuery(
+ "ScreenshotsHelper:GetElementRectFromPoint",
+ {
+ x: x + ele.ownerGlobal.mozInnerScreenX,
+ y: y + ele.ownerGlobal.mozInnerScreenY,
+ bcId: ele.browsingContext.id,
+ }
+ );
+
+ if (rect) {
+ rect = {
+ left: rect.left - ele.ownerGlobal.mozInnerScreenX,
+ right: rect.right - ele.ownerGlobal.mozInnerScreenX,
+ top: rect.top - ele.ownerGlobal.mozInnerScreenY,
+ bottom: rect.bottom - ele.ownerGlobal.mozInnerScreenY,
+ };
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ }
+
+ return { ele, rect };
+}
+
+/**
+ * This function takes an element and finds a suitable rect to draw the hover box on
+ * @param {Element} ele The element to find a suitale rect of
+ * @param {Document} doc The current document
+ * @returns A suitable rect or null
+ */
+export function getBestRectForElement(ele, doc) {
+ let lastRect;
+ let lastNode;
+ let rect;
+ let attemptExtend = false;
+ let node = ele;
+ while (node) {
+ rect = getBoundingClientRect(node);
+ if (!rect) {
+ rect = lastRect;
+ break;
+ }
+ if (rect.width < MIN_DETECT_WIDTH || rect.height < MIN_DETECT_HEIGHT) {
+ // Avoid infinite loop for elements with zero or nearly zero height,
+ // like non-clearfixed float parents with or without borders.
+ break;
+ }
+ if (rect.width > MAX_DETECT_WIDTH || rect.height > MAX_DETECT_HEIGHT) {
+ // Then the last rectangle is better
+ rect = lastRect;
+ attemptExtend = true;
+ break;
+ }
+ if (rect.width >= MIN_DETECT_WIDTH && rect.height >= MIN_DETECT_HEIGHT) {
+ if (!doNotAutoselectTags[node.tagName]) {
+ break;
+ }
+ }
+ lastRect = rect;
+ lastNode = node;
+ node = node.parentNode;
+ }
+ if (rect && node) {
+ const evenBetter = evenBetterElement(node, doc);
+ if (evenBetter) {
+ node = lastNode = evenBetter;
+ rect = getBoundingClientRect(evenBetter);
+ attemptExtend = false;
+ }
+ }
+ if (rect && attemptExtend) {
+ let extendNode = lastNode.nextSibling;
+ while (extendNode) {
+ if (extendNode.nodeType === doc.ELEMENT_NODE) {
+ break;
+ }
+ extendNode = extendNode.nextSibling;
+ if (!extendNode) {
+ const parentNode = lastNode.parentNode;
+ for (let i = 0; i < parentNode.childNodes.length; i++) {
+ if (parentNode.childNodes[i] === lastNode) {
+ extendNode = parentNode.childNodes[i + 1];
+ }
+ }
+ }
+ }
+ if (extendNode) {
+ const extendRect = getBoundingClientRect(extendNode);
+ let x = Math.min(rect.x, extendRect.x);
+ let y = Math.min(rect.y, extendRect.y);
+ let width = Math.max(rect.right, extendRect.right) - x;
+ let height = Math.max(rect.bottom, extendRect.bottom) - y;
+ const combinedRect = new DOMRect(x, y, width, height);
+ if (
+ combinedRect.width <= MAX_DETECT_WIDTH &&
+ combinedRect.height <= MAX_DETECT_HEIGHT
+ ) {
+ rect = combinedRect;
+ }
+ }
+ }
+
+ if (
+ rect &&
+ (rect.width < MIN_DETECT_ABSOLUTE_WIDTH ||
+ rect.height < MIN_DETECT_ABSOLUTE_HEIGHT)
+ ) {
+ rect = null;
+ }
+
+ return rect;
+}
+
+/**
+ * This finds a better element by looking for elements with role article
+ * @param {Element} node The currently hovered node
+ * @param {Document} doc The current document
+ * @returns A better node or null
+ */
+function evenBetterElement(node, doc) {
+ let el = node.parentNode;
+ const ELEMENT_NODE = doc.ELEMENT_NODE;
+ while (el && el.nodeType === ELEMENT_NODE) {
+ if (!el.getAttribute) {
+ return null;
+ }
+ if (el.getAttribute("role") === "article") {
+ const rect = getBoundingClientRect(el);
+ if (!rect) {
+ return null;
+ }
+ if (rect.width <= MAX_DETECT_WIDTH && rect.height <= MAX_DETECT_HEIGHT) {
+ return el;
+ }
+ return null;
+ }
+ el = el.parentNode;
+ }
+ return null;
+}
+
+export class Region {
+ #x1;
+ #x2;
+ #y1;
+ #y2;
+ #xOffset;
+ #yOffset;
+ #windowDimensions;
+
+ constructor(windowDimensions) {
+ this.resetDimensions();
+ this.#windowDimensions = windowDimensions;
+ }
+
+ /**
+ * Sets the dimensions if the given dimension is defined.
+ * Otherwise will reset the dimensions
+ * @param {Object} dims The new region dimensions
+ * {
+ * left: new left dimension value or undefined
+ * top: new top dimension value or undefined
+ * right: new right dimension value or undefined
+ * bottom: new bottom dimension value or undefined
+ * }
+ */
+ set dimensions(dims) {
+ if (dims == null) {
+ this.resetDimensions();
+ return;
+ }
+
+ if (dims.left != null) {
+ this.left = dims.left;
+ }
+ if (dims.top != null) {
+ this.top = dims.top;
+ }
+ if (dims.right != null) {
+ this.right = dims.right;
+ }
+ if (dims.bottom != null) {
+ this.bottom = dims.bottom;
+ }
+ }
+
+ get dimensions() {
+ return {
+ left: this.left,
+ top: this.top,
+ right: this.right,
+ bottom: this.bottom,
+ width: this.width,
+ height: this.height,
+ };
+ }
+
+ get isRegionValid() {
+ return this.#x1 + this.#x2 + this.#y1 + this.#y2 > 0;
+ }
+
+ resetDimensions() {
+ this.#x1 = 0;
+ this.#x2 = 0;
+ this.#y1 = 0;
+ this.#y2 = 0;
+ this.#xOffset = 0;
+ this.#yOffset = 0;
+ }
+
+ /**
+ * Sort the coordinates so x1 < x2 and y1 < y2
+ */
+ sortCoords() {
+ if (this.#x1 > this.#x2) {
+ [this.#x1, this.#x2] = [this.#x2, this.#x1];
+ }
+ if (this.#y1 > this.#y2) {
+ [this.#y1, this.#y2] = [this.#y2, this.#y1];
+ }
+ }
+
+ /**
+ * The region should never appear outside the document so the region will
+ * be shifted if the region is outside the page's width or height.
+ */
+ shift() {
+ let didShift = false;
+ let xDiff = this.right - this.#windowDimensions.scrollWidth;
+ if (xDiff > 0) {
+ this.left -= xDiff;
+ this.right -= xDiff;
+
+ didShift = true;
+ }
+
+ let yDiff = this.bottom - this.#windowDimensions.scrollHeight;
+ if (yDiff > 0) {
+ this.top -= yDiff;
+ this.bottom -= yDiff;
+
+ didShift = true;
+ }
+
+ return didShift;
+ }
+
+ /**
+ * The diagonal distance of the region
+ */
+ get distance() {
+ return Math.sqrt(Math.pow(this.width, 2) + Math.pow(this.height, 2));
+ }
+
+ get xOffset() {
+ return this.#xOffset;
+ }
+ set xOffset(val) {
+ this.#xOffset = val;
+ }
+
+ get yOffset() {
+ return this.#yOffset;
+ }
+ set yOffset(val) {
+ this.#yOffset = val;
+ }
+
+ get top() {
+ return Math.min(this.#y1, this.#y2);
+ }
+ set top(val) {
+ this.#y1 = Math.min(this.#windowDimensions.scrollHeight, Math.max(0, val));
+ }
+
+ get left() {
+ return Math.min(this.#x1, this.#x2);
+ }
+ set left(val) {
+ this.#x1 = Math.min(this.#windowDimensions.scrollWidth, Math.max(0, val));
+ }
+
+ get right() {
+ return Math.max(this.#x1, this.#x2);
+ }
+ set right(val) {
+ this.#x2 = Math.min(this.#windowDimensions.scrollWidth, Math.max(0, val));
+ }
+
+ get bottom() {
+ return Math.max(this.#y1, this.#y2);
+ }
+ set bottom(val) {
+ this.#y2 = Math.min(this.#windowDimensions.scrollHeight, Math.max(0, val));
+ }
+
+ get width() {
+ return Math.abs(this.#x2 - this.#x1);
+ }
+ get height() {
+ return Math.abs(this.#y2 - this.#y1);
+ }
+
+ get x1() {
+ return this.#x1;
+ }
+ get x2() {
+ return this.#x2;
+ }
+ get y1() {
+ return this.#y1;
+ }
+ get y2() {
+ return this.#y2;
+ }
+}
+
+export class WindowDimensions {
+ #clientHeight = null;
+ #clientWidth = null;
+ #scrollHeight = null;
+ #scrollWidth = null;
+ #scrollX = null;
+ #scrollY = null;
+ #scrollMinX = null;
+ #scrollMinY = null;
+ #devicePixelRatio = null;
+
+ set dimensions(dimensions) {
+ if (dimensions.clientHeight != null) {
+ this.#clientHeight = dimensions.clientHeight;
+ }
+ if (dimensions.clientWidth != null) {
+ this.#clientWidth = dimensions.clientWidth;
+ }
+ if (dimensions.scrollHeight != null) {
+ this.#scrollHeight = dimensions.scrollHeight;
+ }
+ if (dimensions.scrollWidth != null) {
+ this.#scrollWidth = dimensions.scrollWidth;
+ }
+ if (dimensions.scrollX != null) {
+ this.#scrollX = dimensions.scrollX;
+ }
+ if (dimensions.scrollY != null) {
+ this.#scrollY = dimensions.scrollY;
+ }
+ if (dimensions.scrollMinX != null) {
+ this.#scrollMinX = dimensions.scrollMinX;
+ }
+ if (dimensions.scrollMinY != null) {
+ this.#scrollMinY = dimensions.scrollMinY;
+ }
+ if (dimensions.devicePixelRatio != null) {
+ this.#devicePixelRatio = dimensions.devicePixelRatio;
+ }
+ }
+
+ get dimensions() {
+ return {
+ clientHeight: this.#clientHeight,
+ clientWidth: this.#clientWidth,
+ scrollHeight: this.#scrollHeight,
+ scrollWidth: this.#scrollWidth,
+ scrollX: this.#scrollX,
+ scrollY: this.#scrollY,
+ scrollMinX: this.#scrollMinX,
+ scrollMinY: this.#scrollMinY,
+ devicePixelRatio: this.#devicePixelRatio,
+ };
+ }
+
+ get clientWidth() {
+ return this.#clientWidth;
+ }
+
+ get clientHeight() {
+ return this.#clientHeight;
+ }
+
+ get scrollWidth() {
+ return this.#scrollWidth;
+ }
+
+ get scrollHeight() {
+ return this.#scrollHeight;
+ }
+
+ get scrollX() {
+ return this.#scrollX;
+ }
+
+ get scrollY() {
+ return this.#scrollY;
+ }
+
+ get scrollMinX() {
+ return this.#scrollMinX;
+ }
+
+ get scrollMinY() {
+ return this.#scrollMinY;
+ }
+
+ get devicePixelRatio() {
+ return this.#devicePixelRatio;
+ }
+
+ reset() {
+ this.#clientHeight = 0;
+ this.#clientWidth = 0;
+ this.#scrollHeight = 0;
+ this.#scrollWidth = 0;
+ this.#scrollX = 0;
+ this.#scrollY = 0;
+ this.#scrollMinX = 0;
+ this.#scrollMinY = 0;
+ }
+}
diff --git a/browser/components/screenshots/screenshots-buttons.css b/browser/components/screenshots/screenshots-buttons.css
new file mode 100644
index 0000000000..82b075bccb
--- /dev/null
+++ b/browser/components/screenshots/screenshots-buttons.css
@@ -0,0 +1,35 @@
+/* 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/. */
+
+:host {
+ position: absolute;
+ /* position about 4px above the container ensuring overlap with toolbar/chrome */
+ top: -8px;
+ inset-inline-end: 10px;
+ z-index: 3;
+ padding: 12px;
+ background-color: var(--arrowpanel-background);
+ color: var(--arrowpanel-color);
+ border-radius: var(--arrowpanel-border-radius);
+}
+
+.full-page {
+ background-image: url("chrome://browser/content/screenshots/menu-fullpage.svg");
+}
+
+.visible-page {
+ background-image: url("chrome://browser/content/screenshots/menu-visible.svg");
+}
+
+.full-page, .visible-page {
+ -moz-context-properties: fill, stroke;
+ fill: currentColor;
+ stroke: var(--color-accent-primary);
+ background-position: center top;
+ background-repeat: no-repeat;
+ background-size: 46px 46px;
+ min-width: 90px;
+ padding: 46px 5px 5px;
+ margin: 2px;
+}
diff --git a/browser/components/screenshots/screenshots-buttons.js b/browser/components/screenshots/screenshots-buttons.js
new file mode 100644
index 0000000000..864505ae2f
--- /dev/null
+++ b/browser/components/screenshots/screenshots-buttons.js
@@ -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/. */
+/* eslint-env mozilla/browser-window */
+
+"use strict";
+
+// This is loaded into chrome windows with the subscript loader. Wrap in
+// a block to prevent accidentally leaking globals onto `window`.
+{
+ ChromeUtils.defineESModuleGetters(this, {
+ ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
+ });
+
+ class ScreenshotsButtons extends MozXULElement {
+ static get markup() {
+ return `
+ <html:link rel="stylesheet" href="chrome://global/skin/global.css"/>
+ <html:link rel="stylesheet" href="chrome://browser/content/screenshots/screenshots-buttons.css"/>
+ <html:button class="visible-page footer-button" data-l10n-id="screenshots-save-visible-button"></html:button>
+ <html:button class="full-page footer-button" data-l10n-id="screenshots-save-page-button"></html:button>
+ `;
+ }
+
+ connectedCallback() {
+ const shadowRoot = this.attachShadow({ mode: "open" });
+ document.l10n.connectRoot(shadowRoot);
+
+ let fragment = MozXULElement.parseXULToFragment(this.constructor.markup);
+ this.shadowRoot.append(fragment);
+
+ let visibleButton = shadowRoot.querySelector(".visible-page");
+ visibleButton.onclick = function () {
+ ScreenshotsUtils.doScreenshot(gBrowser.selectedBrowser, "visible");
+ };
+
+ let fullpageButton = shadowRoot.querySelector(".full-page");
+ fullpageButton.onclick = function () {
+ ScreenshotsUtils.doScreenshot(gBrowser.selectedBrowser, "full_page");
+ };
+ }
+
+ disconnectedCallback() {
+ document.l10n.disconnectRoot(this.shadowRoot);
+ }
+
+ /**
+ * Focus the last used button.
+ * This will default to the visible page button.
+ * @param {String} buttonToFocus
+ */
+ focusButton(buttonToFocus) {
+ if (buttonToFocus === "fullpage") {
+ this.shadowRoot
+ .querySelector(".full-page")
+ .focus({ focusVisible: true });
+ } else {
+ this.shadowRoot
+ .querySelector(".visible-page")
+ .focus({ focusVisible: true });
+ }
+ }
+ }
+
+ customElements.define("screenshots-buttons", ScreenshotsButtons, {
+ extends: "toolbar",
+ });
+}
diff --git a/browser/components/screenshots/tests/browser/browser.toml b/browser/components/screenshots/tests/browser/browser.toml
new file mode 100644
index 0000000000..b363c14732
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser.toml
@@ -0,0 +1,55 @@
+[DEFAULT]
+support-files = [
+ "head.js",
+ "iframe-test-page.html",
+ "first-iframe.html",
+ "second-iframe.html",
+ "test-page.html",
+ "short-test-page.html",
+ "large-test-page.html",
+ "test-page-resize.html",
+]
+
+prefs = [
+ "extensions.screenshots.disabled=false",
+ "screenshots.browser.component.enabled=true",
+]
+
+["browser_iframe_test.js"]
+
+["browser_overlay_keyboard_test.js"]
+
+["browser_screenshots_drag_scroll_test.js"]
+
+["browser_screenshots_drag_test.js"]
+
+["browser_screenshots_focus_test.js"]
+
+["browser_screenshots_overlay_panel_sync.js"]
+
+["browser_screenshots_page_unload.js"]
+
+["browser_screenshots_short_page_test.js"]
+
+["browser_screenshots_telemetry_tests.js"]
+
+["browser_screenshots_test_downloads.js"]
+
+["browser_screenshots_test_escape.js"]
+
+["browser_screenshots_test_full_page.js"]
+
+["browser_screenshots_test_page_crash.js"]
+skip-if = ["!crashreporter"]
+
+["browser_screenshots_test_screenshot_too_big.js"]
+
+["browser_screenshots_test_toggle_pref.js"]
+
+["browser_screenshots_test_toolbar_button.js"]
+
+["browser_screenshots_test_visible.js"]
+
+["browser_test_element_picker.js"]
+
+["browser_test_resize.js"]
diff --git a/browser/components/screenshots/tests/browser/browser_iframe_test.js b/browser/components/screenshots/tests/browser/browser_iframe_test.js
new file mode 100644
index 0000000000..bb853fbe28
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_iframe_test.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_selectingElementsInIframes() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: IFRAME_TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ helper.triggerUIFromToolbar();
+
+ // There are two iframes in the test page. One iframe is nested in the
+ // other so we SpecialPowers.spawn into the iframes to get the
+ // dimension/position of the elements within each iframe.
+ let elementDimensions = await SpecialPowers.spawn(
+ browser,
+ [],
+ async () => {
+ let divDims = content.document
+ .querySelector("div")
+ .getBoundingClientRect();
+
+ let iframe = content.document.querySelector("iframe");
+ let iframesDivsDimArr = await SpecialPowers.spawn(
+ iframe,
+ [],
+ async () => {
+ let iframeDivDims = content.document
+ .querySelector("div")
+ .getBoundingClientRect();
+
+ // Element within the first iframe
+ iframeDivDims = {
+ left: iframeDivDims.left + content.window.mozInnerScreenX,
+ top: iframeDivDims.top + content.window.mozInnerScreenY,
+ width: iframeDivDims.width,
+ height: iframeDivDims.height,
+ };
+
+ let nestedIframe = content.document.querySelector("iframe");
+ let nestedIframeDivDims = await SpecialPowers.spawn(
+ nestedIframe,
+ [],
+ async () => {
+ let secondIframeDivDims = content.document
+ .querySelector("div")
+ .getBoundingClientRect();
+
+ // Element within the nested iframe
+ secondIframeDivDims = {
+ left:
+ secondIframeDivDims.left +
+ content.document.defaultView.mozInnerScreenX,
+ top:
+ secondIframeDivDims.top +
+ content.document.defaultView.mozInnerScreenY,
+ width: secondIframeDivDims.width,
+ height: secondIframeDivDims.height,
+ };
+
+ return secondIframeDivDims;
+ }
+ );
+
+ return [iframeDivDims, nestedIframeDivDims];
+ }
+ );
+
+ // Offset each element position for the browser window
+ for (let dims of iframesDivsDimArr) {
+ dims.left -= content.window.mozInnerScreenX;
+ dims.top -= content.window.mozInnerScreenY;
+ }
+
+ return [divDims].concat(iframesDivsDimArr);
+ }
+ );
+
+ info(JSON.stringify(elementDimensions, null, 2));
+
+ for (let el of elementDimensions) {
+ let x = el.left + el.width / 2;
+ let y = el.top + el.height / 2;
+
+ mouse.move(x, y);
+ await helper.waitForHoverElementRect(el.width, el.height);
+ mouse.click(x, y);
+
+ await helper.waitForStateChange("selected");
+
+ let dimensions = await helper.getSelectionRegionDimensions();
+
+ is(
+ dimensions.left,
+ el.left,
+ "The region left position matches the elements left position"
+ );
+ is(
+ dimensions.top,
+ el.top,
+ "The region top position matches the elements top position"
+ );
+ is(
+ dimensions.width,
+ el.width,
+ "The region width matches the elements width"
+ );
+ is(
+ dimensions.height,
+ el.height,
+ "The region height matches the elements height"
+ );
+
+ mouse.click(500, 500);
+ await helper.waitForStateChange("crosshairs");
+ }
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js b/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js
new file mode 100644
index 0000000000..592587a67d
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js
@@ -0,0 +1,748 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const KEY_TO_EXPECTED_POSITION_ARRAY = [
+ [
+ "ArrowRight",
+ {
+ top: 10,
+ left: 20,
+ bottom: 20,
+ right: 30,
+ },
+ ],
+ [
+ "ArrowDown",
+ {
+ top: 20,
+ left: 20,
+ bottom: 30,
+ right: 30,
+ },
+ ],
+ [
+ "ArrowLeft",
+ {
+ top: 20,
+ left: 10,
+ bottom: 30,
+ right: 20,
+ },
+ ],
+ [
+ "ArrowUp",
+ {
+ top: 10,
+ left: 10,
+ bottom: 20,
+ right: 20,
+ },
+ ],
+ ["ArrowDown", { top: 20, left: 10, bottom: 30, right: 20 }],
+ [
+ "ArrowRight",
+ {
+ top: 20,
+ left: 20,
+ bottom: 30,
+ right: 30,
+ },
+ ],
+ [
+ "ArrowUp",
+ {
+ top: 10,
+ left: 20,
+ bottom: 20,
+ right: 30,
+ },
+ ],
+ [
+ "ArrowLeft",
+ {
+ top: 10,
+ left: 10,
+ bottom: 20,
+ right: 20,
+ },
+ ],
+];
+
+const SHIFT_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY = [
+ [
+ "ArrowRight",
+ {
+ top: 100,
+ left: 200,
+ bottom: 200,
+ right: 300,
+ },
+ ],
+ [
+ "ArrowDown",
+ {
+ top: 200,
+ left: 200,
+ bottom: 300,
+ right: 300,
+ },
+ ],
+ [
+ "ArrowLeft",
+ {
+ top: 200,
+ left: 100,
+ bottom: 300,
+ right: 200,
+ },
+ ],
+ [
+ "ArrowUp",
+ {
+ top: 100,
+ left: 100,
+ bottom: 200,
+ right: 200,
+ },
+ ],
+ ["ArrowDown", { top: 200, left: 100, bottom: 300, right: 200 }],
+ [
+ "ArrowRight",
+ {
+ top: 200,
+ left: 200,
+ bottom: 300,
+ right: 300,
+ },
+ ],
+ [
+ "ArrowUp",
+ {
+ top: 100,
+ left: 200,
+ bottom: 200,
+ right: 300,
+ },
+ ],
+ [
+ "ArrowLeft",
+ {
+ top: 100,
+ left: 100,
+ bottom: 200,
+ right: 200,
+ },
+ ],
+];
+
+/**
+ *
+ */
+add_task(async function test_moveRegionWithKeyboard() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ // Because the screenshots state won't go from draggingReady to
+ // dragging until the diagonal distance is 40px, we have to resize
+ // it to get the region to 10px x 10px
+ await helper.dragOverlay(10, 10, 100, 100);
+ mouse.down(100, 100);
+ await helper.assertStateChange("resizing");
+ mouse.move(20, 20);
+ mouse.up(20, 20);
+ await helper.assertStateChange("selected");
+
+ await SpecialPowers.spawn(
+ browser,
+ [KEY_TO_EXPECTED_POSITION_ARRAY],
+ async keyToExpectedPositionArray => {
+ function assertSelectionRegionDimensions(
+ actualDimensions,
+ expectedDimensions
+ ) {
+ is(
+ actualDimensions.top,
+ expectedDimensions.top,
+ "Top dimension is correct"
+ );
+ is(
+ actualDimensions.left,
+ expectedDimensions.left,
+ "Left dimension is correct"
+ );
+ is(
+ actualDimensions.bottom,
+ expectedDimensions.bottom,
+ "Bottom dimension is correct"
+ );
+ is(
+ actualDimensions.right,
+ expectedDimensions.right,
+ "Right dimension is correct"
+ );
+ }
+
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+
+ // Test moving each corner of the region
+ screenshotsChild.overlay.topLeftMover.focus();
+
+ // Check that initial position is correct
+ assertSelectionRegionDimensions(
+ screenshotsChild.overlay.selectionRegion.dimensions,
+ {
+ top: 10,
+ left: 10,
+ bottom: 20,
+ right: 20,
+ }
+ );
+
+ for (let [key, expectedDimensions] of keyToExpectedPositionArray) {
+ EventUtils.synthesizeKey(key, { repeat: 20 }, content);
+ assertSelectionRegionDimensions(
+ screenshotsChild.overlay.selectionRegion.dimensions,
+ expectedDimensions
+ );
+ }
+
+ // Test moving the highlight element
+ screenshotsChild.overlay.highlightEl.focus();
+
+ // Check that initial position is correct
+ assertSelectionRegionDimensions(
+ screenshotsChild.overlay.selectionRegion.dimensions,
+ {
+ top: 10,
+ left: 10,
+ bottom: 20,
+ right: 20,
+ }
+ );
+
+ for (let [key, expectedDimensions] of keyToExpectedPositionArray) {
+ EventUtils.synthesizeKey(key, { repeat: 10 }, content);
+ assertSelectionRegionDimensions(
+ screenshotsChild.overlay.selectionRegion.dimensions,
+ expectedDimensions
+ );
+ }
+ }
+ );
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlayClosed();
+ }
+ );
+});
+
+add_task(async function test_moveRegionWithKeyboardWithShift() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ await helper.dragOverlay(100, 100, 200, 200);
+
+ await SpecialPowers.spawn(
+ browser,
+ [SHIFT_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY],
+ async shiftPlusKeyToExpectedPositionArray => {
+ function assertSelectionRegionDimensions(
+ actualDimensions,
+ expectedDimensions
+ ) {
+ is(
+ actualDimensions.top,
+ expectedDimensions.top,
+ "Top dimension is correct"
+ );
+ is(
+ actualDimensions.left,
+ expectedDimensions.left,
+ "Left dimension is correct"
+ );
+ is(
+ actualDimensions.bottom,
+ expectedDimensions.bottom,
+ "Bottom dimension is correct"
+ );
+ is(
+ actualDimensions.right,
+ expectedDimensions.right,
+ "Right dimension is correct"
+ );
+ }
+
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+
+ // Test moving each corner of the region
+ screenshotsChild.overlay.topLeftMover.focus();
+
+ // Check that initial position is correct
+ assertSelectionRegionDimensions(
+ screenshotsChild.overlay.selectionRegion.dimensions,
+ {
+ top: 100,
+ left: 100,
+ bottom: 200,
+ right: 200,
+ }
+ );
+
+ for (let [
+ key,
+ expectedDimensions,
+ ] of shiftPlusKeyToExpectedPositionArray) {
+ EventUtils.synthesizeKey(
+ key,
+ { repeat: 20, shiftKey: true },
+ content
+ );
+ assertSelectionRegionDimensions(
+ screenshotsChild.overlay.selectionRegion.dimensions,
+ expectedDimensions
+ );
+ }
+
+ // Test moving the highlight element
+ screenshotsChild.overlay.highlightEl.focus();
+
+ // Check that initial position is correct
+ assertSelectionRegionDimensions(
+ screenshotsChild.overlay.selectionRegion.dimensions,
+ {
+ top: 100,
+ left: 100,
+ bottom: 200,
+ right: 200,
+ }
+ );
+
+ for (let [
+ key,
+ expectedDimensions,
+ ] of shiftPlusKeyToExpectedPositionArray) {
+ EventUtils.synthesizeKey(
+ key,
+ { repeat: 10, shiftKey: true },
+ content
+ );
+ assertSelectionRegionDimensions(
+ screenshotsChild.overlay.selectionRegion.dimensions,
+ expectedDimensions
+ );
+ }
+ }
+ );
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlayClosed();
+ }
+ );
+});
+
+add_task(async function test_moveRegionWithKeyboardWithAccelKey() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ await helper.scrollContentWindow(100, 100);
+
+ let contentWindowDimensions = await helper.getContentDimensions();
+ ok(contentWindowDimensions, "Got dimensions back from the content");
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ await helper.dragOverlay(100, 100, 200, 200);
+
+ info("Test moving the the highlight element");
+ await SpecialPowers.spawn(
+ browser,
+ [contentWindowDimensions],
+ async contentDimensions => {
+ function assertSelectionRegionDimensions(
+ actualDimensions,
+ expectedDimensions
+ ) {
+ is(
+ actualDimensions.top,
+ expectedDimensions.top,
+ "Top dimension is correct"
+ );
+ is(
+ actualDimensions.left,
+ expectedDimensions.left,
+ "Left dimension is correct"
+ );
+ is(
+ actualDimensions.bottom,
+ expectedDimensions.bottom,
+ "Bottom dimension is correct"
+ );
+ is(
+ actualDimensions.right,
+ expectedDimensions.right,
+ "Right dimension is correct"
+ );
+ }
+
+ let { scrollX, scrollY, clientHeight, clientWidth } =
+ contentDimensions;
+
+ const HIGHLIGHT_CONTROL_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY = [
+ [
+ "ArrowRight",
+ {
+ top: scrollY,
+ left: scrollX + clientWidth - 100,
+ bottom: scrollY + 100,
+ right: scrollX + clientWidth,
+ },
+ ],
+ [
+ "ArrowDown",
+ {
+ top: scrollY + clientHeight - 100,
+ left: scrollX + clientWidth - 100,
+ bottom: scrollY + clientHeight,
+ right: scrollX + clientWidth,
+ },
+ ],
+ [
+ "ArrowLeft",
+ {
+ top: scrollY + clientHeight - 100,
+ left: scrollX,
+ bottom: scrollY + clientHeight,
+ right: scrollX + 100,
+ },
+ ],
+ [
+ "ArrowUp",
+ {
+ top: scrollY,
+ left: scrollX,
+ bottom: scrollY + 100,
+ right: scrollX + 100,
+ },
+ ],
+ [
+ "ArrowDown",
+ {
+ top: scrollY + clientHeight - 100,
+ left: scrollX,
+ bottom: scrollY + clientHeight,
+ right: scrollX + 100,
+ },
+ ],
+ [
+ "ArrowRight",
+ {
+ top: scrollY + clientHeight - 100,
+ left: scrollX + clientWidth - 100,
+ bottom: scrollY + clientHeight,
+ right: scrollX + clientWidth,
+ },
+ ],
+ [
+ "ArrowUp",
+ {
+ top: scrollY,
+ left: scrollX + clientWidth - 100,
+ bottom: scrollY + 100,
+ right: scrollX + clientWidth,
+ },
+ ],
+ [
+ "ArrowLeft",
+ {
+ top: scrollY,
+ left: scrollX,
+ bottom: scrollY + 100,
+ right: scrollX + 100,
+ },
+ ],
+ ];
+
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ screenshotsChild.overlay.highlightEl.focus();
+
+ // Move the region around in a clockwise direction
+ // Check that original position is correct
+ assertSelectionRegionDimensions(
+ screenshotsChild.overlay.selectionRegion.dimensions,
+ {
+ top: 100,
+ left: 100,
+ bottom: 200,
+ right: 200,
+ }
+ );
+
+ for (let [
+ key,
+ expectedDimensions,
+ ] of HIGHLIGHT_CONTROL_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY) {
+ EventUtils.synthesizeKey(key, { accelKey: true }, content);
+ assertSelectionRegionDimensions(
+ screenshotsChild.overlay.selectionRegion.dimensions,
+ expectedDimensions
+ );
+ }
+ }
+ );
+
+ mouse.click(300, 300);
+ await helper.assertStateChange("crosshairs");
+
+ await helper.dragOverlay(200, 200, 300, 300);
+
+ info("Test moving the corners clockwise");
+ await SpecialPowers.spawn(
+ browser,
+ [contentWindowDimensions],
+ async contentDimensions => {
+ function assertSelectionRegionDimensions(
+ actualDimensions,
+ expectedDimensions
+ ) {
+ is(
+ actualDimensions.top,
+ expectedDimensions.top,
+ "Top dimension is correct"
+ );
+ is(
+ actualDimensions.left,
+ expectedDimensions.left,
+ "Left dimension is correct"
+ );
+ is(
+ actualDimensions.bottom,
+ expectedDimensions.bottom,
+ "Bottom dimension is correct"
+ );
+ is(
+ actualDimensions.right,
+ expectedDimensions.right,
+ "Right dimension is correct"
+ );
+ }
+
+ let { scrollX, scrollY, clientHeight, clientWidth } =
+ contentDimensions;
+
+ const CONTROL_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY = [
+ [
+ "ArrowRight",
+ {
+ top: scrollY + 100,
+ left: scrollX + 100 + 100,
+ bottom: scrollY + 100 + 100,
+ right: scrollX + clientWidth,
+ },
+ ],
+ [
+ "ArrowDown",
+ {
+ top: scrollY + 100 + 100,
+ left: scrollX + 100 + 100,
+ bottom: scrollY + clientHeight,
+ right: scrollX + clientWidth,
+ },
+ ],
+ [
+ "ArrowLeft",
+ {
+ top: scrollY + 100 + 100,
+ left: scrollX,
+ bottom: scrollY + clientHeight,
+ right: scrollX + 100 + 100,
+ },
+ ],
+ [
+ "ArrowUp",
+ {
+ top: scrollY,
+ left: scrollX,
+ bottom: scrollY + 100 + 100,
+ right: scrollX + 100 + 100,
+ },
+ ],
+ ];
+
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ screenshotsChild.overlay.topLeftMover.focus();
+
+ // Move the region around in a clockwise direction
+ // Check that original position is correct
+ assertSelectionRegionDimensions(
+ screenshotsChild.overlay.selectionRegion.dimensions,
+ {
+ top: 200,
+ left: 200,
+ bottom: 300,
+ right: 300,
+ }
+ );
+
+ for (let [
+ key,
+ expectedDimensions,
+ ] of CONTROL_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY) {
+ EventUtils.synthesizeKey(key, { accelKey: true }, content);
+ assertSelectionRegionDimensions(
+ screenshotsChild.overlay.selectionRegion.dimensions,
+ expectedDimensions
+ );
+ }
+ }
+ );
+
+ mouse.click(400, 400);
+ await helper.assertStateChange("crosshairs");
+
+ await helper.dragOverlay(200, 200, 300, 300);
+
+ info("Test moving the corners counter clockwise");
+ await SpecialPowers.spawn(
+ browser,
+ [contentWindowDimensions],
+ async contentDimensions => {
+ function assertSelectionRegionDimensions(
+ actualDimensions,
+ expectedDimensions
+ ) {
+ is(
+ actualDimensions.top,
+ expectedDimensions.top,
+ "Top dimension is correct"
+ );
+ is(
+ actualDimensions.left,
+ expectedDimensions.left,
+ "Left dimension is correct"
+ );
+ is(
+ actualDimensions.bottom,
+ expectedDimensions.bottom,
+ "Bottom dimension is correct"
+ );
+ is(
+ actualDimensions.right,
+ expectedDimensions.right,
+ "Right dimension is correct"
+ );
+ }
+
+ let { scrollX, scrollY, clientHeight, clientWidth } =
+ contentDimensions;
+
+ const CONTROL_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY = [
+ [
+ "ArrowDown",
+ {
+ top: scrollY + 100 + 100,
+ left: scrollX + 100,
+ bottom: scrollY + clientHeight,
+ right: scrollX + 100 + 100,
+ },
+ ],
+ [
+ "ArrowRight",
+ {
+ top: scrollY + 100 + 100,
+ left: scrollX + 100 + 100,
+ bottom: scrollY + clientHeight,
+ right: scrollX + clientWidth,
+ },
+ ],
+ [
+ "ArrowUp",
+ {
+ top: scrollY,
+ left: scrollX + 100 + 100,
+ bottom: scrollY + 100 + 100,
+ right: scrollX + clientWidth,
+ },
+ ],
+ [
+ "ArrowLeft",
+ {
+ top: scrollY,
+ left: scrollX,
+ bottom: scrollY + 100 + 100,
+ right: scrollX + 100 + 100,
+ },
+ ],
+ ];
+
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ screenshotsChild.overlay.topLeftMover.focus();
+
+ // Move the region around in a clockwise direction
+ // Check that original position is correct
+ assertSelectionRegionDimensions(
+ screenshotsChild.overlay.selectionRegion.dimensions,
+ {
+ top: 200,
+ left: 200,
+ bottom: 300,
+ right: 300,
+ }
+ );
+
+ for (let [
+ key,
+ expectedDimensions,
+ ] of CONTROL_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY) {
+ EventUtils.synthesizeKey(key, { accelKey: true }, content);
+ assertSelectionRegionDimensions(
+ screenshotsChild.overlay.selectionRegion.dimensions,
+ expectedDimensions
+ );
+ }
+ }
+ );
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_drag_scroll_test.js b/browser/components/screenshots/tests/browser/browser_screenshots_drag_scroll_test.js
new file mode 100644
index 0000000000..7fdb084ca6
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_drag_scroll_test.js
@@ -0,0 +1,465 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that screenshots overlay covers the entire page
+ */
+add_task(async function test_overlayCoversEntirePage() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ let contentInfo = await helper.getContentDimensions();
+ info(JSON.stringify(contentInfo, null, 2));
+ ok(contentInfo, "Got dimensions back from the content");
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+ await helper.dragOverlay(10, 10, 500, 500);
+
+ let { scrollWidth, scrollHeight } =
+ await helper.getScreenshotsOverlayDimensions();
+
+ is(
+ scrollWidth,
+ contentInfo.scrollWidth,
+ "The overlay spans the entire width of the page"
+ );
+
+ is(
+ scrollHeight,
+ contentInfo.scrollHeight,
+ "The overlay spans the entire height of the page"
+ );
+ }
+ );
+});
+
+/**
+ * Test dragging screenshots box off top left of screen
+ */
+add_task(async function test_draggingBoxOffTopLeft() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ let startX = 10;
+ let startY = 10;
+ let endX = 500;
+ let endY = 500;
+ await helper.dragOverlay(startX, startY, endX, endY);
+
+ mouse.down(
+ startX + Math.floor((endX - startX) / 2),
+ startY + Math.floor((endY - startY) / 2)
+ );
+
+ await helper.assertStateChange("resizing");
+
+ mouse.move(10, 10);
+
+ // We moved the box to the edge of the screen so we need to wait until the box size is updated
+ await helper.waitForSelectionRegionSizeChange(490);
+
+ let dimensions = await helper.getSelectionRegionDimensions();
+
+ is(dimensions.left, 0, "The box x1 position is now 0");
+ is(dimensions.top, 0, "The box y1 position is now 0");
+ is(dimensions.width, 255, "The box width is now 255");
+ is(dimensions.height, 255, "The box height is now 255");
+
+ mouse.move(
+ startX + Math.floor((endX - startX) / 2),
+ startY + Math.floor((endY - startY) / 2)
+ );
+
+ mouse.up(
+ startX + Math.floor((endX - startX) / 2),
+ startY + Math.floor((endY - startY) / 2)
+ );
+
+ // We moved the box off the edge of the screen so we need to wait until the box size is updated
+ await helper.waitForSelectionRegionSizeChange(255);
+
+ dimensions = await helper.getSelectionRegionDimensions();
+
+ is(dimensions.left, 10, "The box x1 position is now 10 again");
+ is(dimensions.top, 10, "The box y1 position is now 10 again");
+ is(dimensions.width, 490, "The box width is now 490 again");
+ is(dimensions.height, 490, "The box height is now 490 again");
+ }
+ );
+});
+
+/**
+ * Test dragging screenshots box off bottom right of screen
+ */
+add_task(async function test_draggingBoxOffBottomRight() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ let contentInfo = await helper.getContentDimensions();
+ info(JSON.stringify(contentInfo));
+ ok(contentInfo, "Got dimensions back from the content");
+
+ await helper.scrollContentWindow(
+ contentInfo.scrollWidth - contentInfo.clientWidth,
+ contentInfo.scrollHeight - contentInfo.clientHeight
+ );
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ let startX = contentInfo.scrollWidth - 500;
+ let startY = contentInfo.scrollHeight - 500;
+ let endX = contentInfo.scrollWidth - 20;
+ let endY = contentInfo.scrollHeight - 20;
+
+ await helper.dragOverlay(startX, startY, endX, endY);
+
+ // move box off the bottom right of the screen
+ mouse.down(
+ startX + Math.floor((endX - startX) / 2),
+ startY + Math.floor((endY - startY) / 2)
+ );
+ mouse.move(
+ startX + 50 + Math.floor((endX - startX) / 2),
+ startY + 50 + Math.floor((endY - startY) / 2)
+ );
+
+ await helper.assertStateChange("resizing");
+
+ mouse.move(endX, endY);
+
+ // We moved the box to the edge of the screen so we need to wait until the box size is updated
+ await helper.waitForSelectionRegionSizeChange(480);
+
+ let dimensions = await helper.getSelectionRegionDimensions();
+
+ is(dimensions.left, startX + 240, "The box x1 position is now 3748");
+ is(dimensions.top, startY + 240, "The box y1 position is now 3756");
+ is(dimensions.width, 260, "The box width is now 260");
+ is(dimensions.height, 260, "The box height is now 260");
+
+ mouse.move(
+ startX + Math.floor((endX - startX) / 2),
+ startY + Math.floor((endY - startY) / 2)
+ );
+
+ mouse.up(
+ startX + Math.floor((endX - startX) / 2),
+ startY + Math.floor((endY - startY) / 2)
+ );
+
+ // We moved the box off the edge of the screen so we need to wait until the box size is updated
+ await helper.waitForSelectionRegionSizeChange(252);
+
+ dimensions = await helper.getSelectionRegionDimensions();
+
+ is(dimensions.left, startX, "The box x1 position is now 3508 again");
+ is(dimensions.top, startY, "The box y1 position is now 3516 again");
+ is(dimensions.width, 480, "The box width is now 480 again");
+ is(dimensions.height, 480, "The box height is now 480 again");
+ }
+ );
+});
+
+/**
+ * test scrolling while screenshots is open
+ */
+add_task(async function test_scrollingScreenshotsOpen() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ let contentInfo = await helper.getContentDimensions();
+ info(JSON.stringify(contentInfo));
+ ok(contentInfo, "Got dimensions back from the content");
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ let startX = 10;
+ let startY = 10;
+ let endX = 100;
+ let endY = 100;
+
+ await helper.dragOverlay(startX, startY, endX, endY);
+
+ let scrollX = 1000;
+ let scrollY = 1000;
+
+ await helper.scrollContentWindow(scrollX, scrollY);
+
+ let dimensions = await helper.getSelectionRegionDimensions();
+
+ is(dimensions.left, startX, "The box x1 position is 10");
+ is(dimensions.top, startY, "The box y1 position is 10");
+ is(dimensions.width, endX - startX, "The box width is now 90");
+ is(dimensions.height, endY - startY, "The box height is now 90");
+
+ // reset screenshots box
+ await helper.escapeKeyInContent();
+ await helper.assertStateChange("crosshairs");
+
+ await helper.dragOverlay(
+ scrollX + startX,
+ scrollY + startY,
+ scrollX + endX,
+ scrollY + endY
+ );
+
+ await helper.scrollContentWindow(0, 0);
+
+ dimensions = await helper.getSelectionRegionDimensions();
+
+ is(dimensions.left, scrollX + startX, "The box x1 position is 1010");
+ is(dimensions.top, scrollY + startY, "The box y1 position is 1010");
+ is(dimensions.width, endX - startX, "The box width is now 90");
+ is(dimensions.height, endY - startY, "The box height is now 90");
+
+ // reset screenshots box
+ await helper.escapeKeyInContent();
+ await helper.assertStateChange("crosshairs");
+
+ await helper.dragOverlay(
+ startX,
+ startY,
+ contentInfo.clientWidth - 10,
+ contentInfo.clientHeight - 10
+ );
+
+ await helper.scrollContentWindow(
+ contentInfo.clientWidth - 20,
+ contentInfo.clientHeight - 20
+ );
+
+ mouse.down(contentInfo.clientWidth - 10, contentInfo.clientHeight - 10);
+
+ await helper.assertStateChange("resizing");
+
+ mouse.move(
+ contentInfo.clientWidth * 2 - 30,
+ contentInfo.clientHeight * 2 - 30
+ );
+
+ mouse.up(
+ contentInfo.clientWidth * 2 - 30,
+ contentInfo.clientHeight * 2 - 30
+ );
+
+ await helper.assertStateChange("selected");
+
+ let { left, top, right, bottom, width, height } =
+ await helper.getSelectionRegionDimensions();
+ let { scrollWidth, scrollHeight } =
+ await helper.getScreenshotsOverlayDimensions();
+
+ is(left, startX, "The box left is 10");
+ is(top, startY, "The box top is 10");
+ is(
+ right,
+ contentInfo.clientWidth * 2 - 30,
+ "The box right is 2 x clientWidth - 30"
+ );
+ is(
+ bottom,
+ contentInfo.clientHeight * 2 - 30,
+ "The box right is 2 x clientHeight - 30"
+ );
+ is(
+ width,
+ contentInfo.clientWidth * 2 - 40,
+ "The box right is 2 x clientWidth - 40"
+ );
+ is(
+ height,
+ contentInfo.clientHeight * 2 - 40,
+ "The box right is 2 x clientHeight - 40"
+ );
+ is(
+ scrollWidth,
+ contentInfo.scrollWidth,
+ "The overlay spans the entire width of the page"
+ );
+ is(
+ scrollHeight,
+ contentInfo.scrollHeight,
+ "The overlay spans the entire height of the page"
+ );
+ }
+ );
+});
+
+/**
+ * test scroll if by edge
+ */
+add_task(async function test_scrollIfByEdge() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ let windowX = 1000;
+ let windowY = 1000;
+
+ await helper.scrollContentWindow(windowX, windowY);
+
+ await TestUtils.waitForTick();
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let { scrollX, scrollY } = await helper.getContentDimensions();
+
+ is(scrollX, windowX, "Window x position is 1000");
+ is(scrollY, windowY, "Window y position is 1000");
+
+ let startX = 1100;
+ let startY = 1100;
+ let endX = 1010;
+ let endY = 1010;
+
+ // The window won't scroll if the state is draggingReady so we move to
+ // get into the dragging state and then move again to scroll the window
+ mouse.down(startX, startY);
+ await helper.assertStateChange("draggingReady");
+ mouse.move(1050, 1050);
+ await helper.assertStateChange("dragging");
+ mouse.move(endX, endY);
+ mouse.up(endX, endY);
+ await helper.assertStateChange("selected");
+
+ windowX = 990;
+ windowY = 990;
+ await helper.waitForScrollTo(windowX, windowY);
+
+ ({ scrollX, scrollY } = await helper.getContentDimensions());
+
+ is(scrollX, windowX, "Window x position is 990");
+ is(scrollY, windowY, "Window y position is 990");
+
+ let contentInfo = await helper.getContentDimensions();
+
+ endX = windowX + contentInfo.clientWidth - 10;
+ endY = windowY + contentInfo.clientHeight - 10;
+
+ info(
+ `starting to drag overlay to ${endX}, ${endY} in test\nclientInfo: ${JSON.stringify(
+ contentInfo
+ )}\n`
+ );
+ await helper.dragOverlay(startX, startY, endX, endY, "selected");
+
+ windowX = 1000;
+ windowY = 1000;
+ await helper.waitForScrollTo(windowX, windowY);
+
+ ({ scrollX, scrollY } = await helper.getContentDimensions());
+
+ is(scrollX, windowX, "Window x position is 1000");
+ is(scrollY, windowY, "Window y position is 1000");
+ }
+ );
+});
+
+add_task(async function test_scrollIfByEdgeWithKeyboard() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ let windowX = 1000;
+ let windowY = 1000;
+
+ await helper.scrollContentWindow(windowX, windowY);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let { scrollX, scrollY, clientWidth, clientHeight } =
+ await helper.getContentDimensions();
+
+ is(scrollX, windowX, "Window x position is 1000");
+ is(scrollY, windowY, "Window y position is 1000");
+
+ await helper.dragOverlay(1020, 1020, 1120, 1120);
+
+ await helper.moveOverlayViaKeyboard("highlight", [
+ { key: "ArrowLeft", options: { shiftKey: true } },
+ { key: "ArrowLeft", options: {} },
+ { key: "ArrowUp", options: { shiftKey: true } },
+ { key: "ArrowUp", options: {} },
+ ]);
+
+ windowX = 989;
+ windowY = 989;
+ await helper.waitForScrollTo(windowX, windowY);
+
+ ({ scrollX, scrollY, clientWidth, clientHeight } =
+ await helper.getContentDimensions());
+
+ is(scrollX, windowX, "Window x position is 989");
+ is(scrollY, windowY, "Window y position is 989");
+
+ mouse.click(1200, 1200);
+ await helper.assertStateChange("crosshairs");
+ await helper.dragOverlay(
+ scrollX + clientWidth - 100 - 20,
+ scrollY + clientHeight - 100 - 20,
+ scrollX + clientWidth - 20,
+ scrollY + clientHeight - 20
+ );
+
+ await helper.moveOverlayViaKeyboard("highlight", [
+ { key: "ArrowRight", options: { shiftKey: true } },
+ { key: "ArrowRight", options: {} },
+ { key: "ArrowDown", options: { shiftKey: true } },
+ { key: "ArrowDown", options: {} },
+ ]);
+
+ windowX = 1000;
+ windowY = 1000;
+ await helper.waitForScrollTo(windowX, windowY);
+
+ ({ scrollX, scrollY } = await helper.getContentDimensions());
+
+ is(scrollX, windowX, "Window x position is 1000");
+ is(scrollY, windowY, "Window y position is 1000");
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_drag_test.js b/browser/components/screenshots/tests/browser/browser_screenshots_drag_test.js
new file mode 100644
index 0000000000..605e0ae75c
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_drag_test.js
@@ -0,0 +1,488 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This function drags to a 490x490 area and copies to the clipboard
+ */
+add_task(async function dragTest() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+ let expected = Math.floor(
+ 490 * (await getContentDevicePixelRatio(browser))
+ );
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ await helper.dragOverlay(10, 10, 500, 500);
+
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ expected,
+ expected
+ );
+
+ await helper.clickCopyButton();
+
+ info("Waiting for clipboard change");
+ let result = await clipboardChanged;
+
+ info("result: " + JSON.stringify(result, null, 2));
+
+ Assert.equal(
+ result.width,
+ expected,
+ `The copied image from the overlay is ${expected}px in width`
+ );
+ Assert.equal(
+ result.height,
+ expected,
+ `The copied image from the overlay is ${expected}px in height`
+ );
+ }
+ );
+});
+
+/**
+ * This function drags a 1.5 zoomed browser to a 490x490 area and copies to the clipboard
+ */
+add_task(async function dragTest1Point5Zoom() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ const zoom = 1.5;
+ let helper = new ScreenshotsHelper(browser);
+ helper.zoomBrowser(zoom);
+
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+ let expected = Math.floor(
+ 50 * (await getContentDevicePixelRatio(browser))
+ );
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ await helper.dragOverlay(300, 100, 350, 150);
+
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ expected,
+ expected
+ );
+
+ await helper.clickCopyButton();
+
+ info("Waiting for clipboard change");
+ let result = await clipboardChanged;
+
+ result.zoom = zoom;
+ result.devicePixelRatio = window.devicePixelRatio;
+ result.contentDevicePixelRatio = await getContentDevicePixelRatio(
+ browser
+ );
+
+ info("result: " + JSON.stringify(result, null, 2));
+
+ Assert.equal(
+ result.width,
+ expected,
+ `The copied image from the overlay is ${expected}px in width`
+ );
+ Assert.equal(
+ result.height,
+ expected,
+ `The copied image from the overlay is ${expected}px in height`
+ );
+ }
+ );
+});
+
+/**
+ * This function drags an area and clicks elsewhere
+ * on the overlay to go back to the crosshairs state
+ */
+add_task(async function clickOverlayResetState() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ await helper.dragOverlay(10, 10, 100, 100);
+
+ // click outside overlay
+ mouse.click(200, 200);
+
+ await helper.assertStateChange("crosshairs");
+ }
+ );
+});
+
+/**
+ * This function drags an area and clicks the
+ * cancel button to restart the overlay
+ */
+add_task(async function overlayCancelButton() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ await helper.dragOverlay(10, 10, 300, 300);
+
+ await helper.clickCancelButton();
+
+ await helper.assertStateChange("crosshairs");
+ }
+ );
+});
+
+/**
+ * This function drags a 490x490 area and moves it along the edges
+ * and back to the center to confirm that the original size is preserved
+ */
+add_task(async function preserveBoxSizeWhenMovingOutOfWindowBounds() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: SHORT_TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+ let expected = Math.floor(
+ 490 * (await getContentDevicePixelRatio(browser))
+ );
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ await helper.dragOverlay(10, 10, 500, 500);
+
+ let startX = 10;
+ let startY = 10;
+ let endX = 500;
+ let endY = 500;
+
+ mouse.down(
+ Math.floor((endX - startX) / 2),
+ Math.floor((endY - startY) / 2)
+ );
+
+ await helper.assertStateChange("resizing");
+
+ mouse.move(10, 10);
+
+ mouse.move(contentInfo.clientWidth - 10, contentInfo.clientHeight - 10);
+
+ mouse.up(
+ Math.floor((endX - startX) / 2),
+ Math.floor((endY - startY) / 2)
+ );
+
+ await helper.assertStateChange("selected");
+
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ expected,
+ expected
+ );
+
+ await helper.clickCopyButton();
+
+ info("Waiting for clipboard change");
+ let result = await clipboardChanged;
+
+ info("result: " + JSON.stringify(result, null, 2));
+
+ Assert.equal(
+ result.width,
+ expected,
+ `The copied image from the overlay is ${expected}px in width`
+ );
+ Assert.equal(
+ result.height,
+ expected,
+ `The copied image from the overlay is ${expected}px in height`
+ );
+ }
+ );
+});
+
+/**
+ * This function drags a 490x490 area and resizes it to a 300x300 area
+ * with the 4 sides of the box
+ */
+add_task(async function resizeAllEdges() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+ let expected = Math.floor(
+ 300 * (await getContentDevicePixelRatio(browser))
+ );
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ await helper.dragOverlay(10, 10, 500, 500);
+
+ let startX = 10;
+ let startY = 10;
+ let endX = 500;
+ let endY = 500;
+
+ let x = Math.floor((endX - startX) / 2);
+
+ // drag top
+ mouse.down(x, 10);
+
+ await helper.assertStateChange("resizing");
+
+ mouse.move(x, 100);
+ mouse.up(x, 100);
+
+ await helper.assertStateChange("selected");
+
+ // drag bottom
+ mouse.down(x, 500);
+
+ await helper.assertStateChange("resizing");
+
+ mouse.move(x, 400);
+ mouse.up(x, 400);
+
+ await helper.assertStateChange("selected");
+
+ // drag right
+ let y = Math.floor((endY - startY) / 2);
+ mouse.down(500, y);
+
+ await helper.assertStateChange("resizing");
+
+ mouse.move(400, y);
+ mouse.up(400, y);
+
+ await helper.assertStateChange("selected");
+
+ // drag left
+ mouse.down(10, y);
+
+ await helper.assertStateChange("resizing");
+
+ mouse.move(100, y);
+ mouse.up(100, y);
+
+ await helper.assertStateChange("selected");
+
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ expected,
+ expected
+ );
+
+ helper.endX = 400;
+ helper.endY = 400;
+
+ await helper.clickCopyButton();
+
+ info("Waiting for clipboard change");
+ let result = await clipboardChanged;
+
+ info("result: " + JSON.stringify(result, null, 2));
+
+ Assert.equal(
+ result.width,
+ expected,
+ `The copied image from the overlay is ${expected}px in width`
+ );
+ Assert.equal(
+ result.height,
+ expected,
+ `The copied image from the overlay is ${expected}px in height`
+ );
+ }
+ );
+});
+
+/**
+ * This function drags a 490x490 area and resizes it to a 300x300 area
+ * with the 4 corners of the box
+ */
+add_task(async function resizeAllCorners() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+ let expected = Math.floor(
+ 300 * (await getContentDevicePixelRatio(browser))
+ );
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ await helper.dragOverlay(10, 10, 500, 500);
+
+ // drag topright
+ mouse.down(500, 10);
+
+ await helper.assertStateChange("resizing");
+
+ mouse.move(450, 50);
+ mouse.up(450, 50);
+
+ await helper.assertStateChange("selected");
+
+ // drag bottomright
+ mouse.down(450, 500);
+
+ await helper.assertStateChange("resizing");
+
+ mouse.move(400, 450);
+ mouse.up(400, 450);
+
+ await helper.assertStateChange("selected");
+
+ // drag bottomleft
+ mouse.down(10, 450);
+
+ await helper.assertStateChange("resizing");
+
+ mouse.move(50, 400);
+ mouse.up(50, 400);
+
+ await helper.assertStateChange("selected");
+
+ // drag topleft
+ mouse.down(50, 50);
+
+ await helper.assertStateChange("resizing");
+
+ mouse.move(100, 100);
+ mouse.up(100, 100);
+
+ await helper.assertStateChange("selected");
+
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ expected,
+ expected
+ );
+
+ helper.endX = 400;
+ helper.endY = 400;
+
+ await helper.clickCopyButton();
+
+ info("Waiting for clipboard change");
+ let result = await clipboardChanged;
+
+ info("result: " + JSON.stringify(result, null, 2));
+
+ Assert.equal(
+ result.width,
+ expected,
+ `The copied image from the overlay is ${expected}px in width`
+ );
+ Assert.equal(
+ result.height,
+ expected,
+ `The copied image from the overlay is ${expected}px in height`
+ );
+ }
+ );
+});
+
+/**
+ * This function tests clicking the overlay with the different mouse buttons
+ */
+add_task(async function test_otherMouseButtons() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ await helper.dragOverlay(10, 10, 100, 100);
+
+ // click outside overlay
+ mouse.click(200, 200, { button: 1 });
+ mouse.click(200, 200, { button: 2 });
+
+ await TestUtils.waitForTick();
+
+ await helper.assertStateChange("selected");
+
+ mouse.click(200, 200);
+
+ await helper.assertStateChange("crosshairs");
+
+ mouse.down(10, 10, { button: 1 });
+ mouse.move(100, 100, { button: 1 });
+ mouse.up(100, 100, { button: 1 });
+
+ await TestUtils.waitForTick();
+
+ await helper.assertStateChange("crosshairs");
+
+ mouse.down(10, 10, { button: 2 });
+ mouse.move(100, 100, { button: 2 });
+ mouse.up(100, 100, { button: 2 });
+
+ await TestUtils.waitForTick();
+
+ await helper.assertStateChange("crosshairs");
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js b/browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js
new file mode 100644
index 0000000000..367f62205e
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js
@@ -0,0 +1,384 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const SCREENSHOTS_EVENTS = [
+ { category: "screenshots", method: "started", object: "toolbar_button" },
+ { category: "screenshots", method: "canceled", object: "escape" },
+];
+
+const SCREENSHOTS_LAST_SCREENSHOT_METHOD_PREF =
+ "screenshots.browser.component.last-screenshot-method";
+const SCREENSHOTS_LAST_SAVED_METHOD_PREF =
+ "screenshots.browser.component.last-saved-method";
+
+async function restoreFocusOnEscape(initialFocusElem, helper) {
+ info(
+ `restoreFocusOnEscape, focusedElement: ${Services.focus.focusedElement.localName}#${Services.focus.focusedElement.id}`
+ );
+ is(
+ window,
+ BrowserWindowTracker.getTopWindow(),
+ "Our window is the top window"
+ );
+
+ let gotFocus;
+ if (Services.focus.focusedElement !== initialFocusElem) {
+ gotFocus = BrowserTestUtils.waitForEvent(initialFocusElem, "focus");
+ await SimpleTest.promiseFocus(initialFocusElem.ownerGlobal);
+ Services.focus.setFocus(initialFocusElem, Services.focus.FLAG_BYKEY);
+ info(
+ `Waiting to place focus on initialFocusElem: ${initialFocusElem.localName}#${initialFocusElem.id}`
+ );
+ await gotFocus;
+ }
+ is(
+ Services.focus.focusedElement,
+ initialFocusElem,
+ `The initial element #${initialFocusElem.id} has focus`
+ );
+
+ helper.assertPanelNotVisible();
+ // open Screenshots with the keyboard shortcut
+ info(
+ "Triggering screenshots UI with the ctrl+shift+s and waiting for the panel"
+ );
+ EventUtils.synthesizeKey("s", { shiftKey: true, accelKey: true });
+
+ let button = await helper.getPanelButton(".visible-page");
+ info("Panel is now visible, got button: " + button.className);
+ info(
+ `focusedElement: ${Services.focus.focusedElement.localName}.${Services.focus.focusedElement.className}`
+ );
+
+ await BrowserTestUtils.waitForCondition(async () => {
+ return button.getRootNode().activeElement === button;
+ }, "The first button in the panel should have focus");
+
+ info(
+ "Sending Escape to dismiss the screenshots UI and for the panel to be closed"
+ );
+
+ let exitObserved = TestUtils.topicObserved("screenshots-exit");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await helper.waitForPanelClosed();
+ await exitObserved;
+ info("Waiting for the initialFocusElem to be the focusedElement");
+ await BrowserTestUtils.waitForCondition(async () => {
+ return Services.focus.focusedElement === initialFocusElem;
+ }, "The initially focused element should have focus");
+
+ info(
+ `Screenshots did exit, focusedElement: ${Services.focus.focusedElement.localName}#${Services.focus.focusedElement.id}`
+ );
+ helper.assertPanelNotVisible();
+}
+
+add_task(async function testPanelFocused() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await clearAllTelemetryEvents();
+ let helper = new ScreenshotsHelper(browser);
+
+ info("Opening Screenshots and waiting for the panel");
+ helper.triggerUIFromToolbar();
+
+ let button = await helper.getPanelButton(".visible-page");
+ info("Panel is now visible, got button: " + button.className);
+ info(
+ `focusedElement: ${Services.focus.focusedElement.localName}.${Services.focus.focusedElement.className}`
+ );
+
+ info("Waiting for the button to be the activeElement");
+ await BrowserTestUtils.waitForCondition(() => {
+ return button.getRootNode().activeElement === button;
+ }, "The first button in the panel should have focus");
+
+ info("Sending Escape to close Screenshots");
+ let exitObserved = TestUtils.topicObserved("screenshots-exit");
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ info("waiting for the panel to be closed");
+ await helper.waitForPanelClosed();
+ info("waiting for the overlay to be closed");
+ await helper.waitForOverlayClosed();
+ await exitObserved;
+
+ info("Checking telemetry");
+ await assertScreenshotsEvents(SCREENSHOTS_EVENTS);
+ helper.assertPanelNotVisible();
+ }
+ );
+});
+
+add_task(async function testRestoreFocusToChromeOnEscape() {
+ for (let focusSelector of [
+ "#urlbar-input", // A focusable HTML chrome element
+ "tab[selected='true']", // The selected tab element
+ ]) {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ helper.assertPanelNotVisible();
+ let initialFocusElem = document.querySelector(focusSelector);
+ await SimpleTest.promiseFocus(window);
+ await restoreFocusOnEscape(initialFocusElem, helper);
+ }
+ );
+ }
+});
+
+add_task(async function testRestoreFocusToToolbarbuttonOnEscape() {
+ const focusId = "PanelUI-menu-button"; // a toolbarbutton
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ helper.assertPanelNotVisible();
+ let initialFocusElem = document.getElementById(focusId);
+ await SimpleTest.promiseFocus(window);
+ await restoreFocusOnEscape(initialFocusElem, helper);
+ }
+ );
+}).skip(); // Bug 1867687
+
+add_task(async function testRestoreFocusToContentOnEscape() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: 'data:text/html;charset=utf-8,%3Cinput type%3D"text" id%3D"field"%3E',
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ await SimpleTest.promiseFocus(browser);
+ await BrowserTestUtils.synthesizeMouse("#field", 2, 2, {}, browser);
+
+ let initialFocusElem = Services.focus.focusedElement;
+ await restoreFocusOnEscape(initialFocusElem, helper);
+
+ is(
+ initialFocusElem,
+ document.activeElement,
+ "The browser element has focus"
+ );
+ let focusId = await SpecialPowers.spawn(browser, [], () => {
+ return content.document.activeElement.id;
+ });
+ is(focusId, "field", "The button in the content document has focus");
+ }
+ );
+});
+
+add_task(async function test_focusLastUsedMethod() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [SCREENSHOTS_LAST_SCREENSHOT_METHOD_PREF, ""],
+ [SCREENSHOTS_LAST_SAVED_METHOD_PREF, ""],
+ ["browser.download.useDownloadDir", true],
+ ],
+ });
+
+ let publicDownloads = await Downloads.getList(Downloads.PUBLIC);
+ // First ensure we catch the download finishing.
+ let downloadFinishedPromise = new Promise(resolve => {
+ publicDownloads.addView({
+ onDownloadChanged(download) {
+ info("Download changed!");
+ if (download.succeeded || download.error) {
+ info("Download succeeded or errored");
+ publicDownloads.removeView(this);
+ resolve(download);
+ }
+ },
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: SHORT_TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let expectedFocusedButton = await helper.getPanelButton(".visible-page");
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return (
+ expectedFocusedButton.getRootNode().activeElement ===
+ expectedFocusedButton
+ );
+ }, "The visible button in the panel should have focus");
+
+ is(
+ Services.focus.focusedElement,
+ expectedFocusedButton,
+ "The visible button in the panel should have focus"
+ );
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+ let fullpageButton = await helper.getPanelButton(".full-page");
+ fullpageButton.click();
+ await screenshotReady;
+
+ let dialog = helper.getDialog();
+ let retryButton = dialog._frame.contentDocument.getElementById("retry");
+ retryButton.click();
+
+ await helper.waitForOverlay();
+
+ expectedFocusedButton = await helper.getPanelButton(".full-page");
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return (
+ expectedFocusedButton.getRootNode().activeElement ===
+ expectedFocusedButton
+ );
+ }, "The full page button in the panel should have focus");
+
+ is(
+ Services.focus.focusedElement,
+ expectedFocusedButton,
+ "The full button in the panel should have focus"
+ );
+
+ screenshotReady = TestUtils.topicObserved("screenshots-preview-ready");
+ let visiblepageButton = await helper.getPanelButton(".visible-page");
+ visiblepageButton.click();
+ await screenshotReady;
+
+ dialog = helper.getDialog();
+ retryButton = dialog._frame.contentDocument.getElementById("retry");
+ retryButton.click();
+
+ await helper.waitForOverlay();
+
+ expectedFocusedButton = await helper.getPanelButton(".visible-page");
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return (
+ expectedFocusedButton.getRootNode().activeElement ===
+ expectedFocusedButton
+ );
+ }, "The visible button in the panel should have focus");
+
+ is(
+ Services.focus.focusedElement,
+ expectedFocusedButton,
+ "The visible button in the panel should have focus"
+ );
+
+ screenshotReady = TestUtils.topicObserved("screenshots-preview-ready");
+ expectedFocusedButton.click();
+ await screenshotReady;
+
+ dialog = helper.getDialog();
+
+ expectedFocusedButton =
+ dialog._frame.contentDocument.getElementById("download");
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return (
+ expectedFocusedButton.getRootNode().activeElement ===
+ expectedFocusedButton
+ );
+ }, "The download button in the preview dialog should have focus");
+
+ is(
+ Services.focus.focusedElement,
+ expectedFocusedButton,
+ "The download button in the preview dialog should have focus"
+ );
+
+ let screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ copyButton.click();
+ await screenshotExit;
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let visibleButton = await helper.getPanelButton(".visible-page");
+
+ screenshotReady = TestUtils.topicObserved("screenshots-preview-ready");
+ visibleButton.click();
+ await screenshotReady;
+
+ dialog = helper.getDialog();
+
+ expectedFocusedButton =
+ dialog._frame.contentDocument.getElementById("copy");
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return (
+ expectedFocusedButton.getRootNode().activeElement ===
+ expectedFocusedButton
+ );
+ }, "The copy button in the preview dialog should have focus");
+
+ is(
+ Services.focus.focusedElement,
+ expectedFocusedButton,
+ "The copy button in the preview dialog should have focus"
+ );
+
+ screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ let downloadButton =
+ dialog._frame.contentDocument.getElementById("download");
+ downloadButton.click();
+
+ await Promise.all([screenshotExit, downloadFinishedPromise]);
+
+ await publicDownloads.removeFinished();
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ visibleButton = await helper.getPanelButton(".visible-page");
+
+ screenshotReady = TestUtils.topicObserved("screenshots-preview-ready");
+ visibleButton.click();
+ await screenshotReady;
+
+ dialog = helper.getDialog();
+
+ expectedFocusedButton =
+ dialog._frame.contentDocument.getElementById("download");
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return (
+ expectedFocusedButton.getRootNode().activeElement ===
+ expectedFocusedButton
+ );
+ }, "The download button in the preview dialog should have focus");
+
+ is(
+ Services.focus.focusedElement,
+ expectedFocusedButton,
+ "The download button in the preview dialog should have focus"
+ );
+
+ screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ helper.triggerUIFromToolbar();
+ await screenshotExit;
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_overlay_panel_sync.js b/browser/components/screenshots/tests/browser/browser_screenshots_overlay_panel_sync.js
new file mode 100644
index 0000000000..0f54255dc0
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_overlay_panel_sync.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function waitOnTabSwitch() {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 300));
+}
+
+add_task(async function test_overlay_and_panel_state() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+
+ let screenshotsTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE
+ );
+ let browser = screenshotsTab.linkedBrowser;
+
+ let helper = new ScreenshotsHelper(browser);
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ helper.assertPanelVisible();
+
+ await helper.dragOverlay(10, 10, 500, 500);
+
+ await helper.assertStateChange("selected");
+
+ helper.assertPanelNotVisible();
+
+ mouse.click(600, 600);
+
+ await helper.assertStateChange("crosshairs");
+
+ await helper.waitForOverlay();
+
+ helper.assertPanelVisible();
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await BrowserTestUtils.switchTab(gBrowser, screenshotsTab);
+
+ await helper.waitForOverlayClosed();
+
+ Assert.ok(!(await helper.isOverlayInitialized()), "Overlay is closed");
+ helper.assertPanelNotVisible();
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ helper.assertPanelVisible();
+
+ await helper.dragOverlay(10, 10, 500, 500);
+
+ await helper.assertStateChange("selected");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await BrowserTestUtils.switchTab(gBrowser, screenshotsTab);
+
+ Assert.ok(await helper.isOverlayInitialized(), "Overlay is open");
+ helper.assertPanelNotVisible();
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlayClosed();
+
+ Assert.ok(!(await helper.isOverlayInitialized()), "Overlay is closed");
+ helper.assertPanelNotVisible();
+
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(screenshotsTab);
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_page_unload.js b/browser/components/screenshots/tests/browser/browser_screenshots_page_unload.js
new file mode 100644
index 0000000000..fbb5788a5a
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_page_unload.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const SCREENSHOTS_EVENTS = [
+ { category: "screenshots", method: "started", object: "toolbar_button" },
+ { category: "screenshots", method: "canceled", object: "navigation" },
+];
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: SHORT_TEST_PAGE,
+ },
+ async browser => {
+ await clearAllTelemetryEvents();
+ await SpecialPowers.spawn(browser, [SHORT_TEST_PAGE], url => {
+ let a = content.document.createElement("a");
+ a.id = "clickMe";
+ a.href = url;
+ a.textContent = "Click me to unload page";
+ content.document.querySelector("body").appendChild(a);
+ });
+
+ let helper = new ScreenshotsHelper(browser);
+
+ // click toolbar button so panel shows
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("#clickMe").click();
+ });
+
+ await helper.waitForOverlayClosed();
+
+ await assertScreenshotsEvents(SCREENSHOTS_EVENTS);
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_short_page_test.js b/browser/components/screenshots/tests/browser/browser_screenshots_short_page_test.js
new file mode 100644
index 0000000000..bebc70e915
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_short_page_test.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test ensures the overlay is covering the entire window event thought
+ * the body is smaller than the viewport
+ */
+add_task(async function test_overlay() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: SHORT_TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ await helper.dragOverlay(10, 10, 500, 500);
+
+ let { scrollWidth, scrollHeight } =
+ await helper.getScreenshotsOverlayDimensions();
+ Assert.equal(
+ scrollWidth,
+ contentInfo.clientWidth,
+ "The overlay spans the width of the window"
+ );
+
+ Assert.equal(
+ scrollHeight,
+ contentInfo.clientHeight,
+ "The overlay spans the height of the window"
+ );
+ }
+ );
+});
+
+add_task(async function test_window_resize() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: SHORT_TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+
+ const originalWindowWidth = window.outerWidth;
+ const originalWindowHeight = window.outerHeight;
+
+ const BIG_WINDOW_SIZE = 920;
+ const SMALL_WINDOW_SIZE = 620;
+
+ await helper.resizeContentWindow(SMALL_WINDOW_SIZE, SMALL_WINDOW_SIZE);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+ await helper.dragOverlay(10, 10, 100, 100);
+
+ let dimensions = await helper.getScreenshotsOverlayDimensions();
+ let oldWidth = dimensions.scrollWidth;
+ let oldHeight = dimensions.scrollHeight;
+
+ await helper.resizeContentWindow(BIG_WINDOW_SIZE, BIG_WINDOW_SIZE);
+ await helper.waitForSelectionLayerDimensionChange(oldWidth, oldHeight);
+
+ contentInfo = await helper.getContentDimensions();
+ dimensions = await helper.getScreenshotsOverlayDimensions();
+ Assert.equal(
+ dimensions.scrollWidth,
+ contentInfo.clientWidth,
+ "The overlay spans the width of the window"
+ );
+ Assert.equal(
+ dimensions.scrollHeight,
+ contentInfo.clientHeight,
+ "The overlay spans the height of the window"
+ );
+
+ oldWidth = dimensions.scrollWidth;
+ oldHeight = dimensions.scrollHeight;
+
+ await helper.resizeContentWindow(SMALL_WINDOW_SIZE, SMALL_WINDOW_SIZE);
+ await helper.waitForSelectionLayerDimensionChange(oldWidth, oldHeight);
+
+ contentInfo = await helper.getContentDimensions();
+ dimensions = await helper.getScreenshotsOverlayDimensions();
+ Assert.equal(
+ dimensions.scrollWidth,
+ contentInfo.clientWidth,
+ "The overlay spans the width of the window"
+ );
+ Assert.equal(
+ dimensions.scrollHeight,
+ contentInfo.clientHeight,
+ "The overlay spans the height of the window"
+ );
+
+ Assert.less(
+ dimensions.scrollWidth,
+ BIG_WINDOW_SIZE,
+ "Screenshots overlay is smaller than the big window width"
+ );
+ Assert.less(
+ dimensions.scrollHeight,
+ BIG_WINDOW_SIZE,
+ "Screenshots overlay is smaller than the big window height"
+ );
+
+ await helper.resizeContentWindow(
+ originalWindowWidth,
+ originalWindowHeight
+ );
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js b/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js
new file mode 100644
index 0000000000..782ffa3fd3
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js
@@ -0,0 +1,466 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const STARTED_AND_CANCELED_EVENTS = [
+ { category: "screenshots", method: "started", object: "toolbar_button" },
+ { category: "screenshots", method: "canceled", object: "toolbar_button" },
+ { category: "screenshots", method: "started", object: "shortcut" },
+ { category: "screenshots", method: "canceled", object: "shortcut" },
+ { category: "screenshots", method: "started", object: "context_menu" },
+ { category: "screenshots", method: "canceled", object: "context_menu" },
+ { category: "screenshots", method: "started", object: "quick_actions" },
+ { category: "screenshots", method: "canceled", object: "quick_actions" },
+];
+
+const STARTED_RETRY_EVENTS = [
+ { category: "screenshots", method: "started", object: "toolbar_button" },
+ { category: "screenshots", method: "selected", object: "visible" },
+ { category: "screenshots", method: "started", object: "preview_retry" },
+];
+
+const CANCEL_EVENTS = [
+ { category: "screenshots", method: "started", object: "toolbar_button" },
+ { category: "screenshots", method: "selected", object: "full_page" },
+ { category: "screenshots", method: "canceled", object: "preview_cancel" },
+ { category: "screenshots", method: "started", object: "toolbar_button" },
+ { category: "screenshots", method: "canceled", object: "overlay_cancel" },
+];
+
+const COPY_EVENTS = [
+ { category: "screenshots", method: "started", object: "toolbar_button" },
+ { category: "screenshots", method: "selected", object: "visible" },
+ { category: "screenshots", method: "copy", object: "preview_copy" },
+ { category: "screenshots", method: "started", object: "toolbar_button" },
+ { category: "screenshots", method: "copy", object: "overlay_copy" },
+];
+
+const CONTENT_EVENTS = [
+ { category: "screenshots", method: "selected", object: "region_selection" },
+ { category: "screenshots", method: "selected", object: "region_selection" },
+ { category: "screenshots", method: "started", object: "overlay_retry" },
+ { category: "screenshots", method: "selected", object: "element" },
+];
+
+const EXTRA_OVERLAY_EVENTS = [
+ { category: "screenshots", method: "started", object: "toolbar_button" },
+ {
+ category: "screenshots",
+ method: "copy",
+ object: "overlay_copy",
+ extra: {
+ element: "1",
+ region: "1",
+ move: "1",
+ resize: "1",
+ fullpage: "0",
+ visible: "0",
+ },
+ },
+];
+
+const EXTRA_EVENTS = [
+ { category: "screenshots", method: "started", object: "toolbar_button" },
+ { category: "screenshots", method: "selected", object: "visible" },
+ { category: "screenshots", method: "started", object: "preview_retry" },
+ { category: "screenshots", method: "selected", object: "full_page" },
+ {
+ category: "screenshots",
+ method: "copy",
+ object: "preview_copy",
+ extra: {
+ element: "0",
+ region: "0",
+ move: "0",
+ resize: "0",
+ fullpage: "1",
+ visible: "1",
+ },
+ },
+];
+
+add_task(async function test_started_and_canceled_events() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.quickactions.enabled", true],
+ ["browser.urlbar.suggest.quickactions", true],
+ ["browser.urlbar.shortcuts.quickactions", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: SHORT_TEST_PAGE,
+ },
+ async browser => {
+ await clearAllTelemetryEvents();
+ let helper = new ScreenshotsHelper(browser);
+ let screenshotExit;
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlayClosed();
+ await screenshotExit;
+
+ EventUtils.synthesizeKey("s", { shiftKey: true, accelKey: true });
+ await helper.waitForOverlay();
+
+ screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ EventUtils.synthesizeKey("s", { shiftKey: true, accelKey: true });
+ await helper.waitForOverlayClosed();
+ await screenshotExit;
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtPoint(
+ 50,
+ 50,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ browser
+ );
+ await popupShownPromise;
+
+ contextMenu.activateItem(
+ contextMenu.querySelector("#context-take-screenshot")
+ );
+ await helper.waitForOverlay();
+
+ popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtPoint(
+ 50,
+ 50,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ browser
+ );
+ await popupShownPromise;
+
+ screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ contextMenu.activateItem(
+ contextMenu.querySelector("#context-take-screenshot")
+ );
+ await helper.waitForOverlayClosed();
+ await screenshotExit;
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "screenshot",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+ let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC);
+ Assert.equal(result.providerName, "quickactions");
+
+ info("Trigger the screenshot mode");
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+ await helper.waitForOverlay();
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "screenshot",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+ ({ result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1));
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC);
+ Assert.equal(result.providerName, "quickactions");
+
+ info("Trigger the screenshot mode");
+ screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+ await helper.waitForOverlayClosed();
+ await screenshotExit;
+
+ await assertScreenshotsEvents(STARTED_AND_CANCELED_EVENTS);
+ }
+ );
+});
+
+add_task(async function test_started_retry() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await clearAllTelemetryEvents();
+ let helper = new ScreenshotsHelper(browser);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let panel = await helper.waitForPanel();
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ // click the visible page button in panel
+ let visiblePageButton = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector(".visible-page");
+ visiblePageButton.click();
+ await screenshotReady;
+
+ let dialog = helper.getDialog();
+ let retryButton = dialog._frame.contentDocument.getElementById("retry");
+ ok(retryButton, "Got the retry button");
+ retryButton.click();
+
+ await helper.waitForOverlay();
+
+ await assertScreenshotsEvents(STARTED_RETRY_EVENTS);
+ }
+ );
+});
+
+add_task(async function test_canceled() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await clearAllTelemetryEvents();
+ let helper = new ScreenshotsHelper(browser);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let panel = await helper.waitForPanel();
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ // click the full page button in panel
+ let fullPageButton = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector(".full-page");
+ fullPageButton.click();
+ await screenshotReady;
+
+ let dialog = helper.getDialog();
+ let cancelButton = dialog._frame.contentDocument.getElementById("cancel");
+ ok(cancelButton, "Got the cancel button");
+
+ let screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ cancelButton.click();
+
+ await helper.waitForOverlayClosed();
+ await screenshotExit;
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ await helper.clickPreviewCancelButton();
+
+ await helper.waitForOverlayClosed();
+ await screenshotExit;
+
+ await assertScreenshotsEvents(CANCEL_EVENTS);
+ }
+ );
+});
+
+add_task(async function test_copy() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await clearAllTelemetryEvents();
+ let helper = new ScreenshotsHelper(browser);
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+ let devicePixelRatio = await getContentDevicePixelRatio(browser);
+
+ let expectedWidth = Math.floor(
+ devicePixelRatio * contentInfo.clientWidth
+ );
+ let expectedHeight = Math.floor(
+ devicePixelRatio * contentInfo.clientHeight
+ );
+
+ helper.triggerUIFromToolbar();
+ info("waiting for overlay");
+ await helper.waitForOverlay();
+
+ info("waiting for panel");
+ let panel = await helper.waitForPanel();
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ // click the visible page button in panel
+ let visiblePageButton = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector(".visible-page");
+ visiblePageButton.click();
+ info("clicked visible page, waiting for screenshots-preview-ready");
+ await screenshotReady;
+
+ let dialog = helper.getDialog();
+ let copyButton = dialog._frame.contentDocument.getElementById("copy");
+
+ let screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ expectedWidth,
+ expectedHeight
+ );
+
+ // click copy button on dialog box
+ info("clicking the copy button");
+ copyButton.click();
+
+ info("Waiting for clipboard change");
+ await clipboardChanged;
+ info("waiting for screenshot exit");
+ await screenshotExit;
+
+ helper.triggerUIFromToolbar();
+ info("waiting for overlay again");
+ await helper.waitForOverlay();
+
+ await helper.dragOverlay(50, 50, 300, 300);
+
+ clipboardChanged = helper.waitForRawClipboardChange(
+ devicePixelRatio * 250,
+ devicePixelRatio * 250
+ );
+
+ screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ await helper.clickCopyButton();
+
+ info("Waiting for clipboard change");
+ await clipboardChanged;
+ info("Waiting for exit again");
+ await screenshotExit;
+
+ info("Waiting for assertScreenshotsEvents");
+ await assertScreenshotsEvents(COPY_EVENTS);
+ }
+ );
+});
+
+add_task(async function test_content_events() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await clearAllTelemetryEvents();
+ let helper = new ScreenshotsHelper(browser);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ await helper.dragOverlay(50, 50, 300, 300);
+
+ await helper.dragOverlay(300, 300, 333, 333, "selected");
+
+ await helper.dragOverlay(150, 150, 200, 200, "selected");
+
+ mouse.click(11, 11);
+ await helper.assertStateChange("crosshairs");
+
+ await helper.clickTestPageElement();
+
+ let screenshotExit = TestUtils.topicObserved("screenshots-exit");
+
+ await helper.clickCopyButton();
+
+ info("Waiting for exit");
+ await screenshotExit;
+
+ await assertScreenshotsEvents(CONTENT_EVENTS, "content", false);
+ await assertScreenshotsEvents(EXTRA_OVERLAY_EVENTS);
+ }
+ );
+});
+
+add_task(async function test_extra_telemetry() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await clearAllTelemetryEvents();
+ let helper = new ScreenshotsHelper(browser);
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+
+ helper.triggerUIFromToolbar();
+ info("waiting for overlay");
+ await helper.waitForOverlay();
+
+ info("waiting for panel");
+ let panel = await helper.waitForPanel();
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ // click the visible page button in panel
+ let visiblePageButton = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector(".visible-page");
+ visiblePageButton.click();
+ info("clicked visible page, waiting for screenshots-preview-ready");
+ await screenshotReady;
+
+ let dialog = helper.getDialog();
+ let retryButton = dialog._frame.contentDocument.getElementById("retry");
+ retryButton.click();
+
+ info("waiting for panel");
+ panel = await helper.waitForPanel();
+
+ screenshotReady = TestUtils.topicObserved("screenshots-preview-ready");
+
+ // click the full page button in panel
+ let fullPageButton = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector(".full-page");
+ fullPageButton.click();
+ await screenshotReady;
+
+ let screenshotExit = TestUtils.topicObserved("screenshots-exit");
+
+ dialog = helper.getDialog();
+ let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ retryButton.click();
+ // click copy button on dialog box
+ info("clicking the copy button");
+ copyButton.click();
+
+ info("waiting for screenshot exit");
+ await screenshotExit;
+
+ info("Waiting for assertScreenshotsEvents");
+ await assertScreenshotsEvents(EXTRA_EVENTS);
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js
new file mode 100644
index 0000000000..770a7ae06b
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js
@@ -0,0 +1,186 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const SCREENSHOTS_EVENTS = [
+ { category: "screenshots", method: "started", object: "toolbar_button" },
+ { category: "screenshots", method: "download", object: "overlay_download" },
+ { category: "screenshots", method: "started", object: "toolbar_button" },
+ { category: "screenshots", method: "selected", object: "visible" },
+ { category: "screenshots", method: "download", object: "preview_download" },
+];
+
+const MockFilePicker = SpecialPowers.MockFilePicker;
+
+add_setup(async function () {
+ let tmpDir = PathUtils.join(
+ PathUtils.tempDir,
+ "testsavedir" + Math.floor(Math.random() * 2 ** 32)
+ );
+ // Create this dir if it doesn't exist (ignores existing dirs)
+ await IOUtils.makeDirectory(tmpDir);
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.start_downloads_in_tmp_dir", true],
+ ["browser.helperApps.deleteTempFileOnExit", true],
+ ["browser.download.folderList", 2],
+ ["browser.download.dir", tmpDir],
+ ],
+ });
+
+ MockFilePicker.init(window);
+ MockFilePicker.useAnyFile();
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.download.dir");
+ Services.prefs.clearUserPref("browser.download.folderList");
+
+ MockFilePicker.cleanup();
+ });
+});
+
+function waitForFilePicker() {
+ return new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ MockFilePicker.showCallback = null;
+ ok(true, "Saw the file picker");
+ resolve();
+ };
+ });
+}
+
+add_task(async function test_download_without_filepicker() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.useDownloadDir", true]],
+ });
+
+ let publicDownloads = await Downloads.getList(Downloads.PUBLIC);
+ // First ensure we catch the download finishing.
+ let downloadFinishedPromise = new Promise(resolve => {
+ publicDownloads.addView({
+ onDownloadChanged(download) {
+ info("Download changed!");
+ if (download.succeeded || download.error) {
+ info("Download succeeded or errored");
+ publicDownloads.removeView(this);
+ resolve(download);
+ }
+ },
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await clearAllTelemetryEvents();
+ let helper = new ScreenshotsHelper(browser);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+ await helper.dragOverlay(10, 10, 500, 500);
+
+ await helper.clickDownloadButton();
+
+ info("wait for download to finish");
+ let download = await downloadFinishedPromise;
+
+ ok(download.succeeded, "Download should succeed");
+
+ await publicDownloads.removeFinished();
+
+ await waitForScreenshotsEventCount(2);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ let panel = gBrowser.selectedBrowser.ownerDocument.querySelector(
+ "#screenshotsPagePanel"
+ );
+
+ // click the visible page button in panel
+ let visiblePageButton = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector(".visible-page");
+ visiblePageButton.click();
+
+ let dialog = helper.getDialog();
+
+ await screenshotReady;
+
+ let downloadButton =
+ dialog._frame.contentDocument.getElementById("download");
+ ok(downloadButton, "Got the download button");
+
+ let screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ // click download button on dialog box
+ downloadButton.click();
+
+ info("wait for download to finish");
+ download = await downloadFinishedPromise;
+
+ ok(download.succeeded, "Download should succeed");
+
+ await publicDownloads.removeFinished();
+ await screenshotExit;
+ await assertScreenshotsEvents(SCREENSHOTS_EVENTS);
+ }
+ );
+});
+
+add_task(async function test_download_with_filepicker() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.useDownloadDir", false]],
+ });
+
+ let publicDownloads = await Downloads.getList(Downloads.PUBLIC);
+ // First ensure we catch the download finishing.
+ let downloadFinishedPromise = new Promise(resolve => {
+ publicDownloads.addView({
+ onDownloadChanged(download) {
+ info("Download changed!");
+ if (download.succeeded || download.error) {
+ info("Download succeeded or errored");
+ publicDownloads.removeView(this);
+ resolve(download);
+ }
+ },
+ });
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+ await helper.dragOverlay(10, 10, 500, 500);
+
+ let filePicker = waitForFilePicker();
+ let screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ await helper.clickDownloadButton();
+
+ await filePicker;
+ ok(true, "Export file picker opened");
+
+ info("wait for download to finish");
+ let download = await downloadFinishedPromise;
+
+ ok(download.succeeded, "Download should succeed");
+ await publicDownloads.removeFinished();
+ await screenshotExit;
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_escape.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_escape.js
new file mode 100644
index 0000000000..73db436ff1
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_escape.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const SCREENSHOTS_EVENTS = [
+ { category: "screenshots", method: "started", object: "toolbar_button" },
+ { category: "screenshots", method: "canceled", object: "escape" },
+];
+
+add_task(async function test_fullpageScreenshot() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await clearAllTelemetryEvents();
+ let helper = new ScreenshotsHelper(browser);
+
+ // click toolbar button so panel shows
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ EventUtils.synthesizeKey("KEY_F6", { shiftKey: true });
+
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ await helper.waitForOverlayClosed();
+
+ await assertScreenshotsEvents(SCREENSHOTS_EVENTS);
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js
new file mode 100644
index 0000000000..51cda963d9
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js
@@ -0,0 +1,175 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function assertRange(lhs, rhsMin, rhsMax, msg) {
+ Assert.ok(lhs >= rhsMin && lhs <= rhsMax, msg);
+}
+
+add_task(async function test_fullpageScreenshot() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+ let devicePixelRatio = await getContentDevicePixelRatio(browser);
+
+ let expectedWidth = Math.floor(
+ devicePixelRatio * contentInfo.scrollWidth
+ );
+ let expectedHeight = Math.floor(
+ devicePixelRatio * contentInfo.scrollHeight
+ );
+
+ // click toolbar button so panel shows
+ helper.triggerUIFromToolbar();
+
+ let panel = await helper.waitForPanel();
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ // click the full page button in panel
+ let visiblePage = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector(".full-page");
+ visiblePage.click();
+
+ let dialog = helper.getDialog();
+
+ await screenshotReady;
+
+ let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ ok(copyButton, "Got the copy button");
+
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ expectedWidth,
+ expectedHeight
+ );
+
+ // click copy button on dialog box
+ copyButton.click();
+
+ info("Waiting for clipboard change");
+ let result = await clipboardChanged;
+
+ info("result: " + JSON.stringify(result, null, 2));
+ info("contentInfo: " + JSON.stringify(contentInfo, null, 2));
+
+ Assert.equal(result.width, expectedWidth, "Widths should be equal");
+ Assert.equal(result.height, expectedHeight, "Heights should be equal");
+
+ // top left
+ assertRange(result.color.topLeft[0], 110, 111, "R color value");
+ assertRange(result.color.topLeft[1], 110, 111, "G color value");
+ assertRange(result.color.topLeft[2], 110, 111, "B color value");
+
+ // top right
+ assertRange(result.color.topRight[0], 55, 56, "R color value");
+ assertRange(result.color.topRight[1], 155, 156, "G color value");
+ assertRange(result.color.topRight[2], 155, 156, "B color value");
+
+ // bottom left
+ assertRange(result.color.bottomLeft[0], 105, 106, "R color value");
+ assertRange(result.color.bottomLeft[1], 55, 56, "G color value");
+ assertRange(result.color.bottomLeft[2], 105, 106, "B color value");
+
+ // bottom right
+ assertRange(result.color.bottomRight[0], 52, 53, "R color value");
+ assertRange(result.color.bottomRight[1], 127, 128, "G color value");
+ assertRange(result.color.bottomRight[2], 152, 153, "B color value");
+ }
+ );
+});
+
+add_task(async function test_fullpageScreenshotScrolled() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.scrollTo(0, 2008);
+ });
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+ let devicePixelRatio = await getContentDevicePixelRatio(browser);
+
+ let expectedWidth = Math.floor(
+ devicePixelRatio * contentInfo.scrollWidth
+ );
+ let expectedHeight = Math.floor(
+ devicePixelRatio * contentInfo.scrollHeight
+ );
+
+ // click toolbar button so panel shows
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let panel = await helper.waitForPanel();
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ // click the full page button in panel
+ let visiblePage = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector(".full-page");
+ visiblePage.click();
+
+ let dialog = helper.getDialog();
+
+ await screenshotReady;
+
+ let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ ok(copyButton, "Got the copy button");
+
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ expectedWidth,
+ expectedHeight
+ );
+
+ // click copy button on dialog box
+ copyButton.click();
+
+ info("Waiting for clipboard change");
+ let result = await clipboardChanged;
+
+ info("result: " + JSON.stringify(result, null, 2));
+ info("contentInfo: " + JSON.stringify(contentInfo, null, 2));
+
+ Assert.equal(result.width, expectedWidth, "Widths should be equal");
+ Assert.equal(result.height, expectedHeight, "Heights should be equal");
+
+ // top left
+ assertRange(result.color.topLeft[0], 110, 111, "R color value");
+ assertRange(result.color.topLeft[1], 110, 111, "G color value");
+ assertRange(result.color.topLeft[2], 110, 111, "B color value");
+
+ // top right
+ assertRange(result.color.topRight[0], 55, 56, "R color value");
+ assertRange(result.color.topRight[1], 155, 156, "G color value");
+ assertRange(result.color.topRight[2], 155, 156, "B color value");
+
+ // bottom left
+ assertRange(result.color.bottomLeft[0], 105, 106, "R color value");
+ assertRange(result.color.bottomLeft[1], 55, 56, "G color value");
+ assertRange(result.color.bottomLeft[2], 105, 106, "B color value");
+
+ // bottom right
+ assertRange(result.color.bottomRight[0], 52, 53, "R color value");
+ assertRange(result.color.bottomRight[1], 127, 128, "G color value");
+ assertRange(result.color.bottomRight[2], 152, 153, "B color value");
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_page_crash.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_page_crash.js
new file mode 100644
index 0000000000..bed9d006db
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_page_crash.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_fullpageScreenshot() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ // click toolbar button so UI shows
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let panel = await helper.waitForPanel(gBrowser.selectedBrowser);
+
+ let waitForPanelHide = BrowserTestUtils.waitForMutationCondition(
+ panel,
+ { attributes: true },
+ () => {
+ return BrowserTestUtils.isHidden(panel);
+ }
+ );
+
+ await BrowserTestUtils.crashFrame(browser);
+
+ await waitForPanelHide;
+ ok(
+ BrowserTestUtils.isHidden(panel),
+ "Panel buttons are hidden after page crash"
+ );
+
+ await ContentTask.spawn(browser, null, async () => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ Assert.ok(!screenshotsChild._overlay, "The overlay doesn't exist");
+ });
+
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ SessionStore.reviveCrashedTab(tab);
+
+ ok(
+ BrowserTestUtils.isHidden(panel),
+ "Panel buttons are hidden after page crash"
+ );
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_screenshot_too_big.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_screenshot_too_big.js
new file mode 100644
index 0000000000..4e2da3d494
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_screenshot_too_big.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const SCREENSHOTS_EVENTS = [
+ { category: "screenshots", method: "failed", object: "screenshot_too_large" },
+ { category: "screenshots", method: "failed", object: "screenshot_too_large" },
+ { category: "screenshots", method: "failed", object: "screenshot_too_large" },
+ { category: "screenshots", method: "failed", object: "screenshot_too_large" },
+];
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+ChromeUtils.defineESModuleGetters(this, {
+ ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
+});
+
+add_task(async function test_screenshot_too_large_cropped() {
+ await clearAllTelemetryEvents();
+ const screenshotsLocalization = new Localization(
+ ["browser/screenshots.ftl"],
+ true
+ );
+
+ let [errorTitle, errorMessage] = screenshotsLocalization.formatMessagesSync([
+ { id: "screenshots-too-large-error-title" },
+ { id: "screenshots-too-large-error-details" },
+ ]);
+ let showAlertMessageStub = sinon
+ .stub(ScreenshotsUtils, "showAlertMessage")
+ .callsFake(function (title, message) {
+ is(title, errorTitle.value, "Got error title");
+ is(message, errorMessage.value, "Got error message");
+ });
+
+ let rect = { x: 0, y: 0, width: 40000, height: 40000, devicePixelRatio: 1 };
+
+ ScreenshotsUtils.cropScreenshotRectIfNeeded(rect);
+
+ is(
+ rect.width,
+ MAX_CAPTURE_DIMENSION,
+ `The width is ${MAX_CAPTURE_DIMENSION}`
+ );
+ is(
+ rect.height,
+ Math.floor(MAX_CAPTURE_AREA / MAX_CAPTURE_DIMENSION),
+ `The height is ${MAX_CAPTURE_AREA} / ${MAX_CAPTURE_DIMENSION}`
+ );
+
+ rect.width = 40000;
+ rect.hegith = 1;
+
+ ScreenshotsUtils.cropScreenshotRectIfNeeded(rect);
+
+ is(
+ rect.width,
+ MAX_CAPTURE_DIMENSION,
+ `The width was cropped to the max capture dimension (${MAX_CAPTURE_DIMENSION}).`
+ );
+
+ rect.width = 1;
+ rect.height = 40000;
+
+ ScreenshotsUtils.cropScreenshotRectIfNeeded(rect);
+
+ is(
+ rect.height,
+ MAX_CAPTURE_DIMENSION,
+ `The height was cropped to the max capture dimension (${MAX_CAPTURE_DIMENSION}).`
+ );
+
+ rect.width = 25000;
+ rect.height = 25000;
+
+ ScreenshotsUtils.cropScreenshotRectIfNeeded(rect);
+
+ is(rect.width, 25000, "The width was not cropped");
+ is(
+ rect.height,
+ Math.floor(MAX_CAPTURE_AREA / 25000),
+ `The height was cropped to ${MAX_CAPTURE_AREA / 25000}`
+ );
+
+ showAlertMessageStub.restore();
+
+ await assertScreenshotsEvents(SCREENSHOTS_EVENTS);
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_toggle_pref.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_toggle_pref.js
new file mode 100644
index 0000000000..0aafba8fb3
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_toggle_pref.js
@@ -0,0 +1,289 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
+});
+ChromeUtils.defineLazyGetter(this, "ExtensionManagement", () => {
+ const { Management } = ChromeUtils.importESModule(
+ "resource://gre/modules/Extension.sys.mjs"
+ );
+ return Management;
+});
+
+add_task(async function test() {
+ let observerSpy = sinon.spy();
+ let notifierSpy = sinon.spy();
+
+ let observerStub = sinon
+ .stub(ScreenshotsUtils, "observe")
+ .callsFake(observerSpy);
+ let notifierStub = sinon
+ .stub(ScreenshotsUtils, "notify")
+ .callsFake(function (window, type) {
+ notifierSpy();
+ ScreenshotsUtils.notify.wrappedMethod.apply(this, arguments);
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: SHORT_TEST_PAGE,
+ },
+ async browser => {
+ function awaitExtensionEvent(eventName, id) {
+ return new Promise(resolve => {
+ let listener = (_eventName, ...args) => {
+ let extension = args[0];
+ if (_eventName === eventName && extension.id == id) {
+ ExtensionManagement.off(eventName, listener);
+ resolve();
+ }
+ };
+ ExtensionManagement.on(eventName, listener);
+ });
+ }
+ const SCREENSHOT_EXTENSION = "screenshots@mozilla.org";
+
+ let helper = new ScreenshotsHelper(browser);
+
+ ok(observerSpy.notCalled, "Observer not called");
+ helper.triggerUIFromToolbar();
+ Assert.equal(observerSpy.callCount, 1, "Observer function called once");
+
+ ok(notifierSpy.notCalled, "Notifier not called");
+ EventUtils.synthesizeKey("s", { accelKey: true, shiftKey: true });
+
+ await TestUtils.waitForCondition(() => notifierSpy.callCount == 1);
+ Assert.equal(notifierSpy.callCount, 1, "Notify function called once");
+
+ await TestUtils.waitForCondition(() => observerSpy.callCount == 2);
+ Assert.equal(observerSpy.callCount, 2, "Observer function called twice");
+
+ let menu = document.getElementById("contentAreaContextMenu");
+ let popupshown = BrowserTestUtils.waitForPopupEvent(menu, "shown");
+ EventUtils.synthesizeMouseAtCenter(document.body, {
+ type: "contextmenu",
+ });
+ await popupshown;
+ Assert.equal(menu.state, "open", "Context menu is open");
+
+ let popuphidden = BrowserTestUtils.waitForPopupEvent(menu, "hidden");
+ menu.activateItem(menu.querySelector("#context-take-screenshot"));
+ await popuphidden;
+
+ Assert.equal(observerSpy.callCount, 3, "Observer function called thrice");
+
+ const COMPONENT_PREF = "screenshots.browser.component.enabled";
+ await SpecialPowers.pushPrefEnv({
+ set: [[COMPONENT_PREF, false]],
+ });
+ ok(!Services.prefs.getBoolPref(COMPONENT_PREF), "Extension enabled");
+ await awaitExtensionEvent("ready", SCREENSHOT_EXTENSION);
+
+ helper.triggerUIFromToolbar();
+ Assert.equal(
+ observerSpy.callCount,
+ 3,
+ "Observer function still called thrice"
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ ["#firefox-screenshots-preselection-iframe"],
+ async function (iframeSelector) {
+ info(
+ `in waitForUIContent content function, iframeSelector: ${iframeSelector}`
+ );
+ let iframe;
+ await ContentTaskUtils.waitForCondition(() => {
+ iframe = content.document.querySelector(iframeSelector);
+ if (!iframe || !ContentTaskUtils.isVisible(iframe)) {
+ info("in waitForUIContent, no visible iframe yet");
+ return false;
+ }
+ return true;
+ });
+ // wait a frame for the screenshots UI to finish any init
+ await new content.Promise(res => content.requestAnimationFrame(res));
+ }
+ );
+
+ helper.triggerUIFromToolbar();
+ await SpecialPowers.spawn(
+ browser,
+ ["#firefox-screenshots-preselection-iframe"],
+ async function (iframeSelector) {
+ info(
+ `in waitForUIContent content function, iframeSelector: ${iframeSelector}`
+ );
+ let iframe;
+ await ContentTaskUtils.waitForCondition(() => {
+ iframe = content.document.querySelector(iframeSelector);
+ if (!iframe || !ContentTaskUtils.isVisible(iframe)) {
+ info("in waitForUIContent, no visible iframe yet");
+ return true;
+ }
+ return false;
+ });
+ // wait a frame for the screenshots UI to finish any init
+ await new content.Promise(res => content.requestAnimationFrame(res));
+ }
+ );
+
+ popupshown = BrowserTestUtils.waitForPopupEvent(menu, "shown");
+ EventUtils.synthesizeMouseAtCenter(document.body, {
+ type: "contextmenu",
+ });
+ await popupshown;
+ Assert.equal(menu.state, "open", "Context menu is open");
+
+ popuphidden = BrowserTestUtils.waitForPopupEvent(menu, "hidden");
+ menu.activateItem(menu.querySelector("#context-take-screenshot"));
+ await popuphidden;
+
+ Assert.equal(
+ observerSpy.callCount,
+ 3,
+ "Observer function still called thrice"
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ ["#firefox-screenshots-preselection-iframe"],
+ async function (iframeSelector) {
+ info(
+ `in waitForUIContent content function, iframeSelector: ${iframeSelector}`
+ );
+ let iframe;
+ await ContentTaskUtils.waitForCondition(() => {
+ iframe = content.document.querySelector(iframeSelector);
+ if (!iframe || !ContentTaskUtils.isVisible(iframe)) {
+ info("in waitForUIContent, no visible iframe yet");
+ return false;
+ }
+ return true;
+ });
+ // wait a frame for the screenshots UI to finish any init
+ await new content.Promise(res => content.requestAnimationFrame(res));
+ }
+ );
+
+ helper.triggerUIFromToolbar();
+ await SpecialPowers.spawn(
+ browser,
+ ["#firefox-screenshots-preselection-iframe"],
+ async function (iframeSelector) {
+ info(
+ `in waitForUIContent content function, iframeSelector: ${iframeSelector}`
+ );
+ let iframe;
+ await ContentTaskUtils.waitForCondition(() => {
+ iframe = content.document.querySelector(iframeSelector);
+ if (!iframe || !ContentTaskUtils.isVisible(iframe)) {
+ return true;
+ }
+ info("in waitForUIContent, iframe still visible");
+ info(iframe);
+ return false;
+ });
+ // wait a frame for the screenshots UI to finish any init
+ await new content.Promise(res => content.requestAnimationFrame(res));
+ }
+ );
+
+ let componentReady = TestUtils.topicObserved(
+ "screenshots-component-initialized"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[COMPONENT_PREF, true]],
+ });
+ ok(Services.prefs.getBoolPref(COMPONENT_PREF), "Component enabled");
+ // Needed for component to initialize
+ await componentReady;
+
+ helper.triggerUIFromToolbar();
+ Assert.equal(
+ observerSpy.callCount,
+ 4,
+ "Observer function called four times"
+ );
+
+ const SCREENSHOTS_PREF = "extensions.screenshots.disabled";
+ await SpecialPowers.pushPrefEnv({
+ set: [[SCREENSHOTS_PREF, true]],
+ });
+ ok(Services.prefs.getBoolPref(SCREENSHOTS_PREF), "Screenshots disabled");
+ }
+ );
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: SHORT_TEST_PAGE,
+ },
+ async browser => {
+ const SCREENSHOTS_PREF = "extensions.screenshots.disabled";
+ ok(Services.prefs.getBoolPref(SCREENSHOTS_PREF), "Screenshots disabled");
+
+ ok(
+ document.getElementById("screenshot-button").disabled,
+ "Toolbar button disabled"
+ );
+
+ let menu = document.getElementById("contentAreaContextMenu");
+ let popupshown = BrowserTestUtils.waitForPopupEvent(menu, "shown");
+ EventUtils.synthesizeMouseAtCenter(document.body, {
+ type: "contextmenu",
+ });
+ await popupshown;
+ Assert.equal(menu.state, "open", "Context menu is open");
+
+ ok(
+ menu.querySelector("#context-take-screenshot").hidden,
+ "Take screenshot is not in context menu"
+ );
+
+ let popuphidden = BrowserTestUtils.waitForPopupEvent(menu, "hidden");
+ menu.hidePopup();
+ await popuphidden;
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[SCREENSHOTS_PREF, false]],
+ });
+ ok(!Services.prefs.getBoolPref(SCREENSHOTS_PREF), "Screenshots enabled");
+ }
+ );
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: SHORT_TEST_PAGE,
+ },
+ async browser => {
+ const SCREENSHOTS_PREF = "extensions.screenshots.disabled";
+ ok(!Services.prefs.getBoolPref(SCREENSHOTS_PREF), "Screenshots enabled");
+
+ let helper = new ScreenshotsHelper(browser);
+
+ helper.triggerUIFromToolbar();
+ Assert.equal(
+ observerSpy.callCount,
+ 5,
+ "Observer function called for the fifth time"
+ );
+ }
+ );
+
+ observerStub.restore();
+ notifierStub.restore();
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_toolbar_button.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_toolbar_button.js
new file mode 100644
index 0000000000..5ad7d32192
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_toolbar_button.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testScreenshotButtonDisabled() {
+ info("Test the Screenshots button in the panel");
+
+ let screenshotBtn = document.getElementById("screenshot-button");
+ Assert.ok(screenshotBtn, "The screenshots button was added to the nav bar");
+
+ await BrowserTestUtils.withNewTab("https://example.com/", () => {
+ Assert.equal(
+ screenshotBtn.disabled,
+ false,
+ "Screenshots button is enabled"
+ );
+ });
+ await BrowserTestUtils.withNewTab("about:home", () => {
+ Assert.equal(
+ screenshotBtn.disabled,
+ false,
+ "Screenshots button is still enabled on about pages"
+ );
+ });
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js
new file mode 100644
index 0000000000..7b7a46f73d
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js
@@ -0,0 +1,356 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_visibleScreenshot() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+ let devicePixelRatio = await getContentDevicePixelRatio(browser);
+
+ let expectedWidth = Math.floor(
+ devicePixelRatio * contentInfo.clientWidth
+ );
+ let expectedHeight = Math.floor(
+ devicePixelRatio * contentInfo.clientHeight
+ );
+
+ // click toolbar button so panel shows
+ helper.triggerUIFromToolbar();
+
+ await helper.waitForOverlay();
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ let panel = gBrowser.selectedBrowser.ownerDocument.querySelector(
+ "#screenshotsPagePanel"
+ );
+
+ // click the visible page button in panel
+ let visiblePageButton = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector(".visible-page");
+ visiblePageButton.click();
+
+ let dialog = helper.getDialog();
+
+ await screenshotReady;
+
+ let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ ok(copyButton, "Got the copy button");
+
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ expectedWidth,
+ expectedHeight
+ );
+
+ // click copy button on dialog box
+ copyButton.click();
+
+ info("Waiting for clipboard change");
+ let result = await clipboardChanged;
+
+ info("result: " + JSON.stringify(result, null, 2));
+ info("contentInfo: " + JSON.stringify(contentInfo, null, 2));
+
+ Assert.equal(result.width, expectedWidth, "Widths should be equal");
+ Assert.equal(result.height, expectedHeight, "Heights should be equal");
+
+ // top left
+ Assert.equal(111, result.color.topLeft[0], "R color value");
+ Assert.equal(111, result.color.topLeft[1], "G color value");
+ Assert.equal(111, result.color.topLeft[2], "B color value");
+
+ // top right
+ Assert.equal(111, result.color.topRight[0], "R color value");
+ Assert.equal(111, result.color.topRight[1], "G color value");
+ Assert.equal(111, result.color.topRight[2], "B color value");
+
+ // bottom left
+ Assert.equal(111, result.color.bottomLeft[0], "R color value");
+ Assert.equal(111, result.color.bottomLeft[1], "G color value");
+ Assert.equal(111, result.color.bottomLeft[2], "B color value");
+
+ // bottom right
+ Assert.equal(111, result.color.bottomRight[0], "R color value");
+ Assert.equal(111, result.color.bottomRight[1], "G color value");
+ Assert.equal(111, result.color.bottomRight[2], "B color value");
+ }
+ );
+});
+
+add_task(async function test_visibleScreenshotScrolledY() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.scrollTo(0, 2008);
+ });
+
+ let helper = new ScreenshotsHelper(browser);
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+ let devicePixelRatio = await getContentDevicePixelRatio(browser);
+
+ let expectedWidth = Math.floor(
+ devicePixelRatio * contentInfo.clientWidth
+ );
+ let expectedHeight = Math.floor(
+ devicePixelRatio * contentInfo.clientHeight
+ );
+
+ // click toolbar button so panel shows
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let panel = gBrowser.selectedBrowser.ownerDocument.querySelector(
+ "#screenshotsPagePanel"
+ );
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ // click the visible page button in panel
+ let visiblePageButton = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector(".visible-page");
+ visiblePageButton.click();
+
+ let dialog = helper.getDialog();
+
+ await screenshotReady;
+
+ let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ ok(copyButton, "Got the copy button");
+
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ expectedWidth,
+ expectedHeight
+ );
+
+ // click copy button on dialog box
+ copyButton.click();
+
+ info("Waiting for clipboard change");
+ let result = await clipboardChanged;
+
+ // let result = await helper.getImageSizeAndColorFromClipboard();
+ // debugger;
+
+ info("result: " + JSON.stringify(result, null, 2));
+ info("contentInfo: " + JSON.stringify(contentInfo, null, 2));
+
+ Assert.equal(result.width, expectedWidth, "Widths should be equal");
+ Assert.equal(result.height, expectedHeight, "Heights should be equal");
+
+ // top left
+ Assert.equal(105, result.color.topLeft[0], "R color value");
+ Assert.equal(55, result.color.topLeft[1], "G color value");
+ Assert.equal(105, result.color.topLeft[2], "B color value");
+
+ // top right
+ Assert.equal(105, result.color.topRight[0], "R color value");
+ Assert.equal(55, result.color.topRight[1], "G color value");
+ Assert.equal(105, result.color.topRight[2], "B color value");
+
+ // bottom left
+ Assert.equal(105, result.color.bottomLeft[0], "R color value");
+ Assert.equal(55, result.color.bottomLeft[1], "G color value");
+ Assert.equal(105, result.color.bottomLeft[2], "B color value");
+
+ // bottom right
+ Assert.equal(105, result.color.bottomRight[0], "R color value");
+ Assert.equal(55, result.color.bottomRight[1], "G color value");
+ Assert.equal(105, result.color.bottomRight[2], "B color value");
+ }
+ );
+});
+
+add_task(async function test_visibleScreenshotScrolledX() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.scrollTo(2004, 0);
+ });
+
+ let helper = new ScreenshotsHelper(browser);
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+ let devicePixelRatio = await getContentDevicePixelRatio(browser);
+
+ let expectedWidth = Math.floor(
+ devicePixelRatio * contentInfo.clientWidth
+ );
+ let expectedHeight = Math.floor(
+ devicePixelRatio * contentInfo.clientHeight
+ );
+
+ // click toolbar button so panel shows
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let panel = gBrowser.selectedBrowser.ownerDocument.querySelector(
+ "#screenshotsPagePanel"
+ );
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ // click the visible page button in panel
+ let visiblePageButton = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector(".visible-page");
+ visiblePageButton.click();
+
+ let dialog = helper.getDialog();
+
+ await screenshotReady;
+
+ let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ ok(copyButton, "Got the copy button");
+
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ expectedWidth,
+ expectedHeight
+ );
+
+ // click copy button on dialog box
+ copyButton.click();
+
+ info("Waiting for clipboard change");
+ let result = await clipboardChanged;
+
+ info("result: " + JSON.stringify(result, null, 2));
+ info("contentInfo: " + JSON.stringify(contentInfo, null, 2));
+
+ Assert.equal(result.width, expectedWidth, "Widths should be equal");
+ Assert.equal(result.height, expectedHeight, "Heights should be equal");
+
+ // top left
+ Assert.equal(55, result.color.topLeft[0], "R color value");
+ Assert.equal(155, result.color.topLeft[1], "G color value");
+ Assert.equal(155, result.color.topLeft[2], "B color value");
+
+ // top right
+ Assert.equal(55, result.color.topRight[0], "R color value");
+ Assert.equal(155, result.color.topRight[1], "G color value");
+ Assert.equal(155, result.color.topRight[2], "B color value");
+
+ // bottom left
+ Assert.equal(55, result.color.bottomLeft[0], "R color value");
+ Assert.equal(155, result.color.bottomLeft[1], "G color value");
+ Assert.equal(155, result.color.bottomLeft[2], "B color value");
+
+ // bottom right
+ Assert.equal(55, result.color.bottomRight[0], "R color value");
+ Assert.equal(155, result.color.bottomRight[1], "G color value");
+ Assert.equal(155, result.color.bottomRight[2], "B color value");
+ }
+ );
+});
+
+add_task(async function test_visibleScreenshotScrolledXAndY() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.scrollTo(2004, 2008);
+ });
+
+ let helper = new ScreenshotsHelper(browser);
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+ let devicePixelRatio = await getContentDevicePixelRatio(browser);
+
+ let expectedWidth = Math.floor(
+ devicePixelRatio * contentInfo.clientWidth
+ );
+ let expectedHeight = Math.floor(
+ devicePixelRatio * contentInfo.clientHeight
+ );
+
+ // click toolbar button so panel shows
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let panel = gBrowser.selectedBrowser.ownerDocument.querySelector(
+ "#screenshotsPagePanel"
+ );
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ // click the visible page button in panel
+ let visiblePageButton = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector(".visible-page");
+ visiblePageButton.click();
+
+ let dialog = helper.getDialog();
+
+ await screenshotReady;
+
+ let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ ok(copyButton, "Got the copy button");
+
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ expectedWidth,
+ expectedHeight
+ );
+
+ // click copy button on dialog box
+ copyButton.click();
+
+ info("Waiting for clipboard change");
+ let result = await clipboardChanged;
+
+ info("result: " + JSON.stringify(result, null, 2));
+ info("contentInfo: " + JSON.stringify(contentInfo, null, 2));
+
+ Assert.equal(result.width, expectedWidth, "Widths should be equal");
+ Assert.equal(result.height, expectedHeight, "Heights should be equal");
+
+ // top left
+ Assert.equal(52, result.color.topLeft[0], "R color value");
+ Assert.equal(127, result.color.topLeft[1], "G color value");
+ Assert.equal(152, result.color.topLeft[2], "B color value");
+
+ // top right
+ Assert.equal(52, result.color.topRight[0], "R color value");
+ Assert.equal(127, result.color.topRight[1], "G color value");
+ Assert.equal(152, result.color.topRight[2], "B color value");
+
+ // bottom left
+ Assert.equal(52, result.color.bottomLeft[0], "R color value");
+ Assert.equal(127, result.color.bottomLeft[1], "G color value");
+ Assert.equal(152, result.color.bottomLeft[2], "B color value");
+
+ // bottom right
+ Assert.equal(52, result.color.bottomRight[0], "R color value");
+ Assert.equal(127, result.color.bottomRight[1], "G color value");
+ Assert.equal(152, result.color.bottomRight[2], "B color value");
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_test_element_picker.js b/browser/components/screenshots/tests/browser/browser_test_element_picker.js
new file mode 100644
index 0000000000..17ed2a0190
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_test_element_picker.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_element_picker() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ await clearAllTelemetryEvents();
+ let helper = new ScreenshotsHelper(browser);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ await helper.clickTestPageElement();
+
+ let rect = await helper.getTestPageElementRect();
+ let region = await helper.getSelectionRegionDimensions();
+
+ is(
+ region.left,
+ rect.left,
+ "The selected region left is the same as the element left"
+ );
+ is(
+ region.right,
+ rect.right,
+ "The selected region right is the same as the element right"
+ );
+ is(
+ region.top,
+ rect.top,
+ "The selected region top is the same as the element top"
+ );
+ is(
+ region.bottom,
+ rect.bottom,
+ "The selected region bottom is the same as the element bottom"
+ );
+
+ mouse.click(10, 10);
+ await helper.waitForStateChange("crosshairs");
+
+ let hoverElementRegionValid = await helper.isHoverElementRegionValid();
+
+ ok(!hoverElementRegionValid, "Hover element rect is null");
+
+ mouse.click(10, 10);
+ await helper.waitForStateChange("crosshairs");
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_test_resize.js b/browser/components/screenshots/tests/browser/browser_test_resize.js
new file mode 100644
index 0000000000..b249a346d6
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_test_resize.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const windowWidth = 768;
+
+add_task(async function test_window_resize() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: RESIZE_TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ await helper.resizeContentWindow(windowWidth, window.outerHeight);
+ const originalContentDimensions = await helper.getContentDimensions();
+ info(JSON.stringify(originalContentDimensions, null, 2));
+
+ await helper.zoomBrowser(1.5);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ await helper.scrollContentWindow(windowWidth, window.outerHeight);
+
+ await helper.clickTestPageElement("hello");
+
+ await helper.zoomBrowser(1);
+
+ await helper.waitForOverlaySizeChangeTo(
+ originalContentDimensions.scrollWidth,
+ originalContentDimensions.scrollHeight
+ );
+
+ let contentDims = await helper.getContentDimensions();
+ info(JSON.stringify(contentDims, null, 2));
+
+ is(
+ contentDims.scrollWidth,
+ originalContentDimensions.scrollWidth,
+ "Width of page is back to original"
+ );
+ is(
+ contentDims.scrollHeight,
+ originalContentDimensions.scrollHeight,
+ "Height of page is back to original"
+ );
+ }
+ );
+});
+
+add_task(async function test_window_resize_vertical_writing_mode() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: RESIZE_TEST_PAGE,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.documentElement.style = "writing-mode: vertical-lr;";
+ });
+
+ let helper = new ScreenshotsHelper(browser);
+ await helper.resizeContentWindow(windowWidth, window.outerHeight);
+ const originalContentDimensions = await helper.getContentDimensions();
+ info(JSON.stringify(originalContentDimensions, null, 2));
+
+ await helper.zoomBrowser(1.5);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ await helper.scrollContentWindow(windowWidth, window.outerHeight);
+
+ await helper.clickTestPageElement("hello");
+
+ await helper.zoomBrowser(1);
+
+ await helper.waitForOverlaySizeChangeTo(
+ originalContentDimensions.scrollWidth,
+ originalContentDimensions.scrollHeight
+ );
+
+ let contentDims = await helper.getContentDimensions();
+ info(JSON.stringify(contentDims, null, 2));
+
+ is(
+ contentDims.scrollWidth,
+ originalContentDimensions.scrollWidth,
+ "Width of page is back to original"
+ );
+ is(
+ contentDims.scrollHeight,
+ originalContentDimensions.scrollHeight,
+ "Height of page is back to original"
+ );
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/first-iframe.html b/browser/components/screenshots/tests/browser/first-iframe.html
new file mode 100644
index 0000000000..9b0c123486
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/first-iframe.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <style>
+ div {
+ font-size: 40px;
+ margin: 30px;
+ width: 234px;
+ height: 51px;
+ color: blue;
+ }
+ </style>
+ </head>
+<body>
+ <div>Hello world!</div>
+ <iframe
+ width="300"
+ height="300"
+ src="https://example.org/browser/browser/components/screenshots/tests/browser/second-iframe.html"
+ ></iframe>
+</body>
+</html>
diff --git a/browser/components/screenshots/tests/browser/head.js b/browser/components/screenshots/tests/browser/head.js
new file mode 100644
index 0000000000..171e3b8c41
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/head.js
@@ -0,0 +1,951 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+const { UrlbarTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+);
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const TEST_PAGE = TEST_ROOT + "test-page.html";
+const SHORT_TEST_PAGE = TEST_ROOT + "short-test-page.html";
+const LARGE_TEST_PAGE = TEST_ROOT + "large-test-page.html";
+const IFRAME_TEST_PAGE = TEST_ROOT + "iframe-test-page.html";
+const RESIZE_TEST_PAGE = TEST_ROOT + "test-page-resize.html";
+
+const { MAX_CAPTURE_DIMENSION, MAX_CAPTURE_AREA } = ChromeUtils.importESModule(
+ "resource:///modules/ScreenshotsUtils.sys.mjs"
+);
+
+const gScreenshotUISelectors = {
+ panel: "#screenshotsPagePanel",
+ fullPageButton: "button.full-page",
+ visiblePageButton: "button.visible-page",
+ copyButton: "button.#copy",
+};
+
+// MouseEvents is for the mouse events on the Anonymous content
+const MouseEvents = {
+ mouse: new Proxy(
+ {},
+ {
+ get: (target, name) =>
+ async function (x, y, options = {}) {
+ if (name === "click") {
+ this.down(x, y, options);
+ this.up(x, y, options);
+ } else {
+ await safeSynthesizeMouseEventInContentPage(":root", x, y, {
+ type: "mouse" + name,
+ ...options,
+ });
+ }
+ },
+ }
+ ),
+};
+
+const { mouse } = MouseEvents;
+
+class ScreenshotsHelper {
+ constructor(browser) {
+ this.browser = browser;
+ this.selector = gScreenshotUISelectors;
+ }
+
+ get toolbarButton() {
+ return this.browser.ownerDocument.getElementById("screenshot-button");
+ }
+
+ get panel() {
+ return this.browser.ownerDocument.querySelector(this.selector.panel);
+ }
+
+ /**
+ * Click the screenshots button in the toolbar
+ */
+ triggerUIFromToolbar() {
+ let button = this.toolbarButton;
+ ok(
+ BrowserTestUtils.isVisible(button),
+ "The screenshot toolbar button is visible"
+ );
+ button.click();
+ }
+
+ async getPanelButton(selector) {
+ let panel = await this.waitForPanel();
+ let screenshotsButtons = panel.querySelector("screenshots-buttons");
+ ok(screenshotsButtons, "Found the screenshots-buttons");
+ let button = screenshotsButtons.shadowRoot.querySelector(selector);
+ ok(button, `Found ${selector} button`);
+ return button;
+ }
+
+ async waitForPanel() {
+ let panel = this.panel;
+ await BrowserTestUtils.waitForCondition(async () => {
+ if (!panel) {
+ panel = this.panel;
+ }
+ return panel && BrowserTestUtils.isVisible(panel);
+ });
+ return panel;
+ }
+
+ async waitForOverlay() {
+ const panel = await this.waitForPanel();
+ ok(BrowserTestUtils.isVisible(panel), "Panel buttons are visible");
+
+ await BrowserTestUtils.waitForCondition(async () => {
+ let init = await this.isOverlayInitialized();
+ return init;
+ });
+ info("Overlay is visible");
+ }
+
+ async waitForPanelClosed() {
+ let panel = this.panel;
+ if (!panel) {
+ info("waitForPanelClosed: Panel doesnt exist");
+ return;
+ }
+ if (panel.hidden) {
+ info("waitForPanelClosed: panel is already hidden");
+ return;
+ }
+ info("waitForPanelClosed: waiting for the panel to become hidden");
+ await BrowserTestUtils.waitForMutationCondition(
+ panel,
+ { attributes: true },
+ () => {
+ return BrowserTestUtils.isHidden(panel);
+ }
+ );
+ ok(BrowserTestUtils.isHidden(panel), "Panel buttons are hidden");
+ info("waitForPanelClosed, panel is hidden: " + panel.hidden);
+ }
+
+ async waitForOverlayClosed() {
+ await this.waitForPanelClosed();
+ await BrowserTestUtils.waitForCondition(async () => {
+ let init = !(await this.isOverlayInitialized());
+ info("Is overlay initialized: " + !init);
+ return init;
+ });
+ info("Overlay is not visible");
+ }
+
+ async isOverlayInitialized() {
+ return SpecialPowers.spawn(this.browser, [], () => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ return screenshotsChild?.overlay?.initialized;
+ });
+ }
+
+ waitForStateChange(newState) {
+ return SpecialPowers.spawn(this.browser, [newState], async state => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+
+ await ContentTaskUtils.waitForCondition(() => {
+ info(`got ${screenshotsChild.overlay.state}. expected ${state}`);
+ return screenshotsChild.overlay.state === state;
+ }, `Wait for overlay state to be ${state}`);
+
+ return screenshotsChild.overlay.state;
+ });
+ }
+
+ async assertStateChange(newState) {
+ let currentState = await this.waitForStateChange(newState);
+
+ is(
+ currentState,
+ newState,
+ `The current state is ${currentState}, expected ${newState}`
+ );
+ }
+
+ getHoverElementRect() {
+ return ContentTask.spawn(this.browser, null, async () => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ return screenshotsChild.overlay.hoverElementRegion.dimensions;
+ });
+ }
+
+ isHoverElementRegionValid() {
+ return ContentTask.spawn(this.browser, null, async () => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ return screenshotsChild.overlay.hoverElementRegion.isRegionValid;
+ });
+ }
+
+ async waitForHoverElementRect(expectedWidth, expectedHeight) {
+ return SpecialPowers.spawn(
+ this.browser,
+ [expectedWidth, expectedHeight],
+ async (width, height) => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ let dimensions;
+ await ContentTaskUtils.waitForCondition(() => {
+ dimensions = screenshotsChild.overlay.hoverElementRegion.dimensions;
+ return dimensions.width === width && dimensions.height === height;
+ }, "The hover element region is the expected width and height");
+ return dimensions;
+ }
+ );
+ }
+
+ async waitForSelectionRegionSizeChange(currentWidth) {
+ await ContentTask.spawn(
+ this.browser,
+ [currentWidth],
+ async ([currWidth]) => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+
+ let dimensions = screenshotsChild.overlay.selectionRegion.dimensions;
+ await ContentTaskUtils.waitForCondition(() => {
+ dimensions = screenshotsChild.overlay.selectionRegion.dimensions;
+ return dimensions.width !== currWidth;
+ }, "Wait for selection box width change");
+ }
+ );
+ }
+
+ /**
+ * This will drag an overlay starting at the given startX and startY coordinates and ending
+ * at the given endX and endY coordinates.
+ *
+ * endY should be at least 70px from the bottom of window and endX should be at least
+ * 265px from the left of the window. If these requirements are not met then the
+ * overlay buttons (cancel, copy, download) will be positioned different from the default
+ * and the methods to click the overlay buttons will not work unless the updated
+ * position coordinates are supplied.
+ * See https://searchfox.org/mozilla-central/rev/af78418c4b5f2c8721d1a06486cf4cf0b33e1e8d/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs#1789,1798
+ * for how the overlay buttons are positioned when the overlay rect is near the bottom or
+ * left edge of the window.
+ *
+ * Note: The distance of the rect should be greater than 40 to enter in the "dragging" state.
+ * See https://searchfox.org/mozilla-central/rev/af78418c4b5f2c8721d1a06486cf4cf0b33e1e8d/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs#809
+ * @param {Number} startX The starting X coordinate. The left edge of the overlay rect.
+ * @param {Number} startY The starting Y coordinate. The top edge of the overlay rect.
+ * @param {Number} endX The end X coordinate. The right edge of the overlay rect.
+ * @param {Number} endY The end Y coordinate. The bottom edge of the overlay rect.
+ */
+ async dragOverlay(
+ startX,
+ startY,
+ endX,
+ endY,
+ expectedStartingState = "crosshairs"
+ ) {
+ await this.assertStateChange(expectedStartingState);
+
+ mouse.down(startX, startY);
+
+ await Promise.any([
+ this.waitForStateChange("draggingReady"),
+ this.waitForStateChange("resizing"),
+ ]);
+ Assert.ok(true, "The overlay is in the draggingReady or resizing state");
+
+ mouse.move(endX, endY);
+
+ await Promise.any([
+ this.waitForStateChange("dragging"),
+ this.waitForStateChange("resizing"),
+ ]);
+ Assert.ok(true, "The overlay is in the dragging or resizing state");
+
+ mouse.up(endX, endY);
+
+ await this.assertStateChange("selected");
+
+ this.endX = endX;
+ this.endY = endY;
+ }
+
+ async moveOverlayViaKeyboard(mover, events) {
+ await SpecialPowers.spawn(
+ this.browser,
+ [mover, events],
+ async (moverToFocus, eventsArr) => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+
+ let overlay = screenshotsChild.overlay;
+
+ switch (moverToFocus) {
+ case "highlight":
+ overlay.highlightEl.focus({ focusVisible: true });
+ break;
+ case "mover-bottomLeft":
+ overlay.bottomLeftMover.focus({ focusVisible: true });
+ break;
+ case "mover-bottomRight":
+ overlay.bottomRightMover.focus({ focusVisible: true });
+ break;
+ case "mover-topLeft":
+ overlay.topLeftMover.focus({ focusVisible: true });
+ break;
+ case "mover-topRight":
+ overlay.topRightMover.focus({ focusVisible: true });
+ break;
+ }
+ screenshotsChild.overlay.highlightEl.focus();
+
+ for (let event of eventsArr) {
+ EventUtils.synthesizeKey(
+ event.key,
+ { type: "keydown", ...event.options },
+ content
+ );
+
+ await ContentTaskUtils.waitForCondition(
+ () => overlay.state === "resizing",
+ "Wait for overlay state to be resizing"
+ );
+
+ EventUtils.synthesizeKey(
+ event.key,
+ { type: "keyup", ...event.options },
+ content
+ );
+
+ await ContentTaskUtils.waitForCondition(
+ () => overlay.state === "selected",
+ "Wait for overlay state to be selected"
+ );
+ }
+ }
+ );
+ }
+
+ async scrollContentWindow(x, y) {
+ let promise = BrowserTestUtils.waitForContentEvent(this.browser, "scroll");
+ let contentDims = await this.getContentDimensions();
+ await ContentTask.spawn(
+ this.browser,
+ [x, y, contentDims],
+ async ([xPos, yPos, cDims]) => {
+ content.window.scroll(xPos, yPos);
+
+ info(JSON.stringify(cDims, null, 2));
+ const scrollbarHeight = {};
+ const scrollbarWidth = {};
+ content.window.windowUtils.getScrollbarSize(
+ false,
+ scrollbarWidth,
+ scrollbarHeight
+ );
+
+ await ContentTaskUtils.waitForCondition(() => {
+ function isCloseEnough(a, b, diff) {
+ return Math.abs(a - b) <= diff;
+ }
+
+ info(
+ `scrollbarWidth: ${scrollbarWidth.value}, scrollbarHeight: ${scrollbarHeight.value}`
+ );
+ info(
+ `scrollX: ${content.window.scrollX}, scrollY: ${content.window.scrollY}, scrollMaxX: ${content.window.scrollMaxX}, scrollMaxY: ${content.window.scrollMaxY}`
+ );
+
+ // Sometimes (read intermittently) the scroll width/height will be
+ // off by the width/height of the scrollbar when we are expecting the
+ // page to be scrolled to the very end. To mitigate this, we check
+ // that the below differences are within the scrollbar width/height.
+ return (
+ (content.window.scrollX === xPos ||
+ isCloseEnough(
+ cDims.clientWidth + content.window.scrollX,
+ cDims.scrollWidth,
+ scrollbarWidth.value + 1
+ )) &&
+ (content.window.scrollY === yPos ||
+ isCloseEnough(
+ cDims.clientHeight + content.window.scrollY,
+ cDims.scrollHeight,
+ scrollbarHeight.value + 1
+ ))
+ );
+ }, `Waiting for window to scroll to ${xPos}, ${yPos}`);
+ }
+ );
+ await promise;
+ }
+
+ async waitForScrollTo(x, y) {
+ await ContentTask.spawn(this.browser, [x, y], async ([xPos, yPos]) => {
+ await ContentTaskUtils.waitForCondition(() => {
+ info(
+ `Got scrollX: ${content.window.scrollX}. scrollY: ${content.window.scrollY}`
+ );
+ return (
+ content.window.scrollX === xPos && content.window.scrollY === yPos
+ );
+ }, `Waiting for window to scroll to ${xPos}, ${yPos}`);
+ });
+ }
+
+ async resizeContentWindow(width, height) {
+ this.browser.ownerGlobal.resizeTo(width, height);
+ await TestUtils.waitForCondition(
+ () => window.outerHeight === height && window.outerWidth === width,
+ "Waiting for window to resize"
+ );
+ }
+
+ async clickDownloadButton() {
+ let { centerX: x, centerY: y } = await ContentTask.spawn(
+ this.browser,
+ null,
+ async () => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ let { left, top, width, height } =
+ screenshotsChild.overlay.downloadButton.getBoundingClientRect();
+ let centerX = left + width / 2;
+ let centerY = top + height / 2;
+ return { centerX, centerY };
+ }
+ );
+
+ info(`clicking download button at ${x}, ${y}`);
+ mouse.click(x, y);
+ }
+
+ async clickCopyButton() {
+ let { centerX: x, centerY: y } = await ContentTask.spawn(
+ this.browser,
+ null,
+ async () => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ let { left, top, width, height } =
+ screenshotsChild.overlay.copyButton.getBoundingClientRect();
+ let centerX = left + width / 2;
+ let centerY = top + height / 2;
+ return { centerX, centerY };
+ }
+ );
+
+ info(`clicking copy button at ${x}, ${y}`);
+ mouse.click(x, y);
+ }
+
+ async clickCancelButton() {
+ let { centerX: x, centerY: y } = await ContentTask.spawn(
+ this.browser,
+ null,
+ async () => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ let { left, top, width, height } =
+ screenshotsChild.overlay.cancelButton.getBoundingClientRect();
+ let centerX = left + width / 2;
+ let centerY = top + height / 2;
+ return { centerX, centerY };
+ }
+ );
+
+ info(`clicking cancel button at ${x}, ${y}`);
+ mouse.click(x, y);
+ }
+
+ async clickPreviewCancelButton() {
+ let { centerX: x, centerY: y } = await ContentTask.spawn(
+ this.browser,
+ null,
+ async () => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ let { left, top, width, height } =
+ screenshotsChild.overlay.previewCancelButton.getBoundingClientRect();
+ let centerX = left + width / 2;
+ let centerY = top + height / 2;
+ return { centerX, centerY };
+ }
+ );
+
+ info(`clicking cancel button at ${x}, ${y}`);
+ mouse.click(x, y);
+ }
+
+ escapeKeyInContent() {
+ return SpecialPowers.spawn(this.browser, [], () => {
+ EventUtils.synthesizeKey("KEY_Escape", {}, content);
+ });
+ }
+
+ getTestPageElementRect(elementId = "testPageElement") {
+ return ContentTask.spawn(this.browser, [elementId], async id => {
+ let ele = content.document.getElementById(id);
+ return ele.getBoundingClientRect();
+ });
+ }
+
+ async clickTestPageElement(elementId = "testPageElement") {
+ let rect = await this.getTestPageElementRect(elementId);
+ let dims = await this.getContentDimensions();
+
+ let x = Math.floor(rect.x + dims.scrollX + rect.width / 2);
+ let y = Math.floor(rect.y + dims.scrollY + rect.height / 2);
+
+ mouse.move(x, y);
+ await this.waitForHoverElementRect(rect.width, rect.height);
+ mouse.down(x, y);
+ await this.assertStateChange("draggingReady");
+ mouse.up(x, y);
+ await this.assertStateChange("selected");
+ }
+
+ async zoomBrowser(zoom) {
+ let promise = BrowserTestUtils.waitForContentEvent(this.browser, "resize");
+ await SpecialPowers.spawn(this.browser, [zoom], zoomLevel => {
+ const { Layout } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+ Layout.zoomDocument(content.document, zoomLevel);
+ });
+ await promise;
+ }
+
+ /**
+ * Gets the dialog box
+ * @returns The dialog box
+ */
+ getDialog() {
+ let currDialogBox = this.browser.tabDialogBox;
+ let manager = currDialogBox.getTabDialogManager();
+ let dialogs = manager.hasDialogs && manager.dialogs;
+ return dialogs[0];
+ }
+
+ assertPanelVisible() {
+ info("assertPanelVisible, panel.hidden:" + this.panel?.hidden);
+ Assert.ok(
+ BrowserTestUtils.isVisible(this.panel),
+ "Screenshots panel is visible"
+ );
+ }
+
+ assertPanelNotVisible() {
+ info("assertPanelNotVisible, panel.hidden:" + this.panel?.hidden);
+ Assert.ok(
+ !this.panel || BrowserTestUtils.isHidden(this.panel),
+ "Screenshots panel is not visible"
+ );
+ }
+
+ /**
+ * Copied from screenshots extension
+ * Returns a promise that resolves when the clipboard data has changed
+ * Otherwise rejects
+ */
+ waitForRawClipboardChange(epectedWidth, expectedHeight) {
+ const initialClipboardData = Date.now().toString();
+ SpecialPowers.clipboardCopyString(initialClipboardData);
+
+ return TestUtils.waitForCondition(
+ async () => {
+ let data;
+ try {
+ data = await this.getImageSizeAndColorFromClipboard();
+ } catch (e) {
+ console.log("Failed to get image/png clipboard data:", e);
+ return false;
+ }
+ if (
+ data &&
+ initialClipboardData !== data &&
+ data.height === expectedHeight &&
+ data.width === epectedWidth
+ ) {
+ return data;
+ }
+ return false;
+ },
+ "Waiting for screenshot to copy to clipboard",
+ 200
+ );
+ }
+
+ /**
+ * Gets the client and scroll demensions on the window
+ * @returns { Object }
+ * clientHeight The visible height
+ * clientWidth The visible width
+ * scrollHeight The scrollable height
+ * scrollWidth The scrollable width
+ * scrollX The scroll x position
+ * scrollY The scroll y position
+ */
+ getContentDimensions() {
+ return SpecialPowers.spawn(this.browser, [], async function () {
+ let {
+ innerWidth,
+ innerHeight,
+ scrollMaxX,
+ scrollMaxY,
+ scrollX,
+ scrollY,
+ } = content.window;
+ let width = innerWidth + scrollMaxX;
+ let height = innerHeight + scrollMaxY;
+
+ const scrollbarHeight = {};
+ const scrollbarWidth = {};
+ content.window.windowUtils.getScrollbarSize(
+ false,
+ scrollbarWidth,
+ scrollbarHeight
+ );
+ width -= scrollbarWidth.value;
+ height -= scrollbarHeight.value;
+ innerWidth -= scrollbarWidth.value;
+ innerHeight -= scrollbarHeight.value;
+
+ return {
+ clientHeight: innerHeight,
+ clientWidth: innerWidth,
+ scrollHeight: height,
+ scrollWidth: width,
+ scrollX,
+ scrollY,
+ };
+ });
+ }
+
+ async getScreenshotsOverlayDimensions() {
+ return ContentTask.spawn(this.browser, null, async () => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ Assert.ok(screenshotsChild.overlay.initialized, "The overlay exists");
+
+ let screenshotsContainer = screenshotsChild.overlay.screenshotsContainer;
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return !screenshotsContainer.hasAttribute("resizing");
+ }, "Waiting for overlay to be done resizing");
+
+ info(
+ `${screenshotsContainer.style.width} ${
+ screenshotsContainer.style.height
+ } ${screenshotsContainer.hasAttribute("resizing")}`
+ );
+
+ return {
+ scrollWidth: screenshotsContainer.scrollWidth,
+ scrollHeight: screenshotsContainer.scrollHeight,
+ };
+ });
+ }
+
+ async waitForSelectionLayerDimensionChange(oldWidth, oldHeight) {
+ await ContentTask.spawn(
+ this.browser,
+ [oldWidth, oldHeight],
+ async ([prevWidth, prevHeight]) => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+
+ await ContentTaskUtils.waitForCondition(() => {
+ let screenshotsContainer =
+ screenshotsChild.overlay.screenshotsContainer;
+ info(
+ `old height: ${prevHeight}. new height: ${screenshotsContainer.scrollHeight}.\nold width: ${prevWidth}. new width: ${screenshotsContainer.scrollWidth}`
+ );
+ return (
+ screenshotsContainer.scrollHeight !== prevHeight &&
+ screenshotsContainer.scrollWidth !== prevWidth
+ );
+ }, "Wait for selection box width change");
+ }
+ );
+ }
+
+ waitForOverlaySizeChangeTo(width, height) {
+ return ContentTask.spawn(
+ this.browser,
+ [width, height],
+ async ([newWidth, newHeight]) => {
+ await ContentTaskUtils.waitForCondition(() => {
+ let {
+ innerHeight,
+ innerWidth,
+ scrollMaxY,
+ scrollMaxX,
+ scrollMinY,
+ scrollMinX,
+ } = content.window;
+ let scrollWidth = innerWidth + scrollMaxX - scrollMinX;
+ let scrollHeight = innerHeight + scrollMaxY - scrollMinY;
+
+ const scrollbarHeight = {};
+ const scrollbarWidth = {};
+ content.window.windowUtils.getScrollbarSize(
+ false,
+ scrollbarWidth,
+ scrollbarHeight
+ );
+ scrollWidth -= scrollbarWidth.value;
+ scrollHeight -= scrollbarHeight.value;
+ info(
+ `${scrollHeight}, ${newHeight}, ${scrollWidth}, ${newWidth}, ${content.window.scrollMaxX}`
+ );
+ return scrollHeight === newHeight && scrollWidth === newWidth;
+ }, "Wait for document size change");
+ }
+ );
+ }
+
+ getSelectionRegionDimensions() {
+ return ContentTask.spawn(this.browser, null, async () => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ Assert.ok(screenshotsChild.overlay.initialized, "The overlay exists");
+
+ return screenshotsChild.overlay.selectionRegion.dimensions;
+ });
+ }
+
+ /**
+ * Copied from screenshots extension
+ * A helper that returns the size of the image that was just put into the clipboard by the
+ * :screenshot command.
+ * @return The {width, height, color} dimension and color object.
+ */
+ async getImageSizeAndColorFromClipboard() {
+ let flavor = "image/png";
+ let image = getRawClipboardData(flavor);
+ if (!image) {
+ return false;
+ }
+
+ // Due to the differences in how images could be stored in the clipboard the
+ // checks below are needed. The clipboard could already provide the image as
+ // byte streams or as image container. If it's not possible obtain a
+ // byte stream, the function throws.
+
+ if (image instanceof Ci.imgIContainer) {
+ image = Cc["@mozilla.org/image/tools;1"]
+ .getService(Ci.imgITools)
+ .encodeImage(image, flavor);
+ }
+
+ if (!(image instanceof Ci.nsIInputStream)) {
+ throw new Error("Unable to read image data");
+ }
+
+ const binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ binaryStream.setInputStream(image);
+ const available = binaryStream.available();
+ const buffer = new ArrayBuffer(available);
+ info(
+ `${binaryStream.readArrayBuffer(
+ available,
+ buffer
+ )} read, ${available} available`
+ );
+
+ // We are going to load the image in the content page to measure its size.
+ // We don't want to insert the image directly in the browser's document
+ // which could mess all sorts of things up
+ return SpecialPowers.spawn(
+ this.browser,
+ [buffer],
+ async function (_buffer) {
+ const img = content.document.createElement("img");
+ const loaded = new Promise(r => {
+ img.addEventListener("load", r, { once: true });
+ });
+ const url = content.URL.createObjectURL(
+ new Blob([_buffer], { type: "image/png" })
+ );
+
+ img.src = url;
+ content.document.documentElement.appendChild(img);
+
+ info("Waiting for the clipboard image to load in the content page");
+ await loaded;
+
+ let canvas = content.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "html:canvas"
+ );
+ let context = canvas.getContext("2d");
+ canvas.width = img.width;
+ canvas.height = img.height;
+ context.drawImage(img, 0, 0);
+ let topLeft = context.getImageData(0, 0, 1, 1);
+ let topRight = context.getImageData(img.width - 1, 0, 1, 1);
+ let bottomLeft = context.getImageData(0, img.height - 1, 1, 1);
+ let bottomRight = context.getImageData(
+ img.width - 1,
+ img.height - 1,
+ 1,
+ 1
+ );
+
+ img.remove();
+ content.URL.revokeObjectURL(url);
+
+ return {
+ width: img.width,
+ height: img.height,
+ color: {
+ topLeft: topLeft.data,
+ topRight: topRight.data,
+ bottomLeft: bottomLeft.data,
+ bottomRight: bottomRight.data,
+ },
+ };
+ }
+ );
+ }
+}
+
+/**
+ * Get the raw clipboard data
+ * @param flavor Type of data to get from clipboard
+ * @returns The data from the clipboard
+ */
+function getRawClipboardData(flavor) {
+ const whichClipboard = Services.clipboard.kGlobalClipboard;
+ const xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ xferable.init(null);
+ xferable.addDataFlavor(flavor);
+ Services.clipboard.getData(
+ xferable,
+ whichClipboard,
+ SpecialPowers.wrap(window).browsingContext.currentWindowContext
+ );
+ let data = {};
+ try {
+ // xferable.getTransferData(flavor, data);
+ xferable.getAnyTransferData({}, data);
+ info(JSON.stringify(data, null, 2));
+ } catch (e) {
+ info(e);
+ }
+ data = data.value || null;
+ return data;
+}
+
+/**
+ * Synthesize a mouse event on an element, after ensuring that it is visible
+ * in the viewport.
+ *
+ * @param {String} selector: The node selector to get the node target for the event.
+ * @param {number} x
+ * @param {number} y
+ * @param {object} options: Options that will be passed to BrowserTestUtils.synthesizeMouse
+ */
+async function safeSynthesizeMouseEventInContentPage(
+ selector,
+ x,
+ y,
+ options = {}
+) {
+ let context = gBrowser.selectedBrowser.browsingContext;
+ BrowserTestUtils.synthesizeMouse(selector, x, y, options, context);
+}
+
+add_setup(async () => {
+ CustomizableUI.addWidgetToArea(
+ "screenshot-button",
+ CustomizableUI.AREA_NAVBAR,
+ 0
+ );
+ let screenshotBtn = document.getElementById("screenshot-button");
+ Assert.ok(screenshotBtn, "The screenshots button was added to the nav bar");
+});
+
+function getContentDevicePixelRatio(browser) {
+ return SpecialPowers.spawn(browser, [], async function () {
+ return content.window.devicePixelRatio;
+ });
+}
+
+async function clearAllTelemetryEvents() {
+ // Clear everything.
+ info("Clearing all telemetry events");
+ await TestUtils.waitForCondition(() => {
+ Services.telemetry.clearEvents();
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ );
+ let content = events.content;
+ let parent = events.parent;
+
+ return (!content && !parent) || (!content.length && !parent.length);
+ });
+}
+
+async function waitForScreenshotsEventCount(count, process = "parent") {
+ await TestUtils.waitForCondition(
+ () => {
+ let events = TelemetryTestUtils.getEvents(
+ { category: "screenshots" },
+ { process }
+ );
+
+ info(`Got ${events?.length} event(s)`);
+ info(`Actual events: ${JSON.stringify(events, null, 2)}`);
+ return events.length === count ? events : null;
+ },
+ `Waiting for ${count} ${process} event(s).`,
+ 200,
+ 100
+ );
+}
+
+async function assertScreenshotsEvents(
+ expectedEvents,
+ process = "parent",
+ clearEvents = true
+) {
+ info(`Expected events: ${JSON.stringify(expectedEvents, null, 2)}`);
+ // Make sure we have recorded the correct number of events
+ await waitForScreenshotsEventCount(expectedEvents.length, process);
+
+ TelemetryTestUtils.assertEvents(
+ expectedEvents,
+ { category: "screenshots" },
+ { clear: clearEvents, process }
+ );
+}
diff --git a/browser/components/screenshots/tests/browser/iframe-test-page.html b/browser/components/screenshots/tests/browser/iframe-test-page.html
new file mode 100644
index 0000000000..5439552734
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/iframe-test-page.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html lang="en">
+ <head>
+ <title>Screenshots</title>
+ <style>
+ div {
+ font-size: 40px;
+ margin: 30px;
+ width: 233px;
+ height: 50px;
+ color: green;
+ }
+ </style>
+ </head>
+ <body>
+ <div>Hello world!</div>
+ <iframe
+ width="500"
+ height="500"
+ src="https://example.com/browser/browser/components/screenshots/tests/browser/first-iframe.html"
+ ></iframe>
+ </body>
+</html>
diff --git a/browser/components/screenshots/tests/browser/large-test-page.html b/browser/components/screenshots/tests/browser/large-test-page.html
new file mode 100644
index 0000000000..ab2eb8d601
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/large-test-page.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Screenshots</title>
+</head>
+<body style="height:40000px; width:40000px; background-repeat: no-repeat; background-size: 40008px 40016px; background-color: rgb(111, 111, 111); background-image:linear-gradient(to right, transparent 50%, rgba(0, 200, 200, 0.5) 50%),linear-gradient(to bottom, transparent 50%, rgba(100, 0, 100, 0.5) 50%);">
+</body>
+</html>
diff --git a/browser/components/screenshots/tests/browser/second-iframe.html b/browser/components/screenshots/tests/browser/second-iframe.html
new file mode 100644
index 0000000000..ca5de26bb9
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/second-iframe.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <style>
+ div {
+ font-size: 40px;
+ margin: 30px;
+ width: 235px;
+ height: 52px;
+ color: red;
+ }
+ </style>
+ </head>
+<body>
+ <div>Hello world!</div>
+</body>
+</html>
diff --git a/browser/components/screenshots/tests/browser/short-test-page.html b/browser/components/screenshots/tests/browser/short-test-page.html
new file mode 100644
index 0000000000..7718892f8c
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/short-test-page.html
@@ -0,0 +1,8 @@
+<html lang="en">
+<head>
+ <title>Screenshots</title>
+</head>
+<body>
+ <div style="height:500px; width:500px; background-color: blue;"></div>
+</body>
+</html>
diff --git a/browser/components/screenshots/tests/browser/test-page-resize.html b/browser/components/screenshots/tests/browser/test-page-resize.html
new file mode 100644
index 0000000000..dea57909b4
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/test-page-resize.html
@@ -0,0 +1,25 @@
+<html>
+<head>
+ <title>Screenshots</title>
+</head>
+<body>
+ <div style="display: flex;flex-flow: row wrap;gap: 8px;">
+ <div style="height:100px; width:100px; background-color: blue;"></div>
+ <div style="height:100px; width:100px; background-color: blue;"></div>
+ <div style="height:100px; width:100px; background-color: blue;"></div>
+ <div style="height:100px; width:100px; background-color: blue;"></div>
+ <div style="height:100px; width:100px; background-color: blue;"></div>
+ <div style="height:100px; width:100px; background-color: blue;"></div>
+ </div>
+ <div style="display: flex;flex-direction: column;gap: 8px;">
+ <div style="height:100px; width:100px; background-color: red;"></div>
+ <div style="height:100px; width:100px; background-color: red;"></div>
+ <div style="height:100px; width:100px; background-color: red;"></div>
+ <div style="height:100px; width:100px; background-color: red;"></div>
+ <div style="height:100px; width:100px; background-color: red;"></div>
+ <div style="height:100px; width:100px; background-color: red;"></div>
+ <div style="height:100px; width:100px; background-color: red;"></div>
+ <div id="hello" style="height:100px; width:100px; background-color: red;"></div>
+ </div>
+</body>
+</html>
diff --git a/browser/components/screenshots/tests/browser/test-page.html b/browser/components/screenshots/tests/browser/test-page.html
new file mode 100644
index 0000000000..5ddc1d6eb6
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/test-page.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Screenshots</title>
+</head>
+<body style="height:4000px; width:4000px; background-repeat: no-repeat; background-size: 4008px 4016px; background-color: rgb(111, 111, 111); background-image:linear-gradient(to right, transparent 50%, rgba(0, 200, 200, 0.5) 50%),linear-gradient(to bottom, transparent 50%, rgba(100, 0, 100, 0.5) 50%);">
+ <div id="testPageElement" style="position:absolute; top:91px; left:93px; width:92px; height:94px; border:solid red;"></div>
+ <script>
+ // Make sure the screenshots overlay anonymous document always receives events
+ // that web content would normally be able to intercept, as that could break the
+ // overlay
+ function disabledEvent(event) {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ }
+
+ window.addEventListener("click", disabledEvent, true);
+ window.addEventListener("pointerdown", disabledEvent, true);
+ window.addEventListener("pointermove", disabledEvent, true);
+ window.addEventListener("mousemove", disabledEvent, true);
+ window.addEventListener("pointerup", disabledEvent, true);
+ window.addEventListener("keydown", disabledEvent, true);
+ window.addEventListener("keyup", disabledEvent, true);
+ </script>
+</body>
+</html>