diff options
Diffstat (limited to 'browser/components/screenshots')
31 files changed, 2185 insertions, 647 deletions
diff --git a/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs b/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs index 3718b6a4e0..aa9dbfdbd3 100644 --- a/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs +++ b/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs @@ -88,22 +88,18 @@ export class ScreenshotsOverlay { cancelLabel, cancelAttributes, instructions, - downloadLabel, downloadAttributes, - copyLabel, copyAttributes, ] = lazy.overlayLocalization.formatMessagesSync([ { id: "screenshots-cancel-button" }, { id: "screenshots-component-cancel-button" }, { id: "screenshots-instructions" }, - { id: "screenshots-component-download-button-label" }, { - id: "screenshots-component-download-button", + id: "screenshots-component-download-button-2", args: { shortcut: downloadShortcut }, }, - { id: "screenshots-component-copy-button-label" }, { - id: "screenshots-component-copy-button", + id: "screenshots-component-copy-button-2", args: { shortcut: copyShorcut }, }, ]); @@ -137,31 +133,31 @@ export class ScreenshotsOverlay { <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 id="mover-bottomRight" class="mover-target direction-bottomRight" 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 id="mover-bottomLeft" class="mover-target direction-bottomLeft" tabindex="0"> + <div class="mover"></div> + </div> + <div id="mover-left" class="mover-target direction-left"> <div class="mover"></div> </div> <div id="selection-size-container"> - <span id="selection-size"></span> + <span id="selection-size" dir="ltr"></span> </div> </div> </div> <div id="buttons-container" hidden> <div class="buttons-wrapper"> <button id="cancel" class="screenshots-button" title="${cancelAttributes.attributes[0].value}" aria-label="${cancelAttributes.attributes[1].value}"><img/></button> - <button id="copy" class="screenshots-button" title="${copyAttributes.attributes[0].value}" aria-label="${copyAttributes.attributes[1].value}"><img/><label>${copyLabel.value}</label></button> - <button id="download" class="screenshots-button primary" title="${downloadAttributes.attributes[0].value}" aria-label="${downloadAttributes.attributes[1].value}"><img/><label>${downloadLabel.value}</label></button> + <button id="copy" class="screenshots-button" title="${copyAttributes.attributes[0].value}" aria-label="${copyAttributes.attributes[1].value}"><img/><label>${copyAttributes.value}</label></button> + <button id="download" class="screenshots-button primary" title="${downloadAttributes.attributes[0].value}" aria-label="${downloadAttributes.attributes[1].value}"><img/><label>${downloadAttributes.value}</label></button> </div> </div> </div> @@ -240,6 +236,12 @@ export class ScreenshotsOverlay { this.#setState(STATES.CROSSHAIRS); + this.selection = this.window.getSelection(); + this.ranges = []; + for (let i = 0; i < this.selection.rangeCount; i++) { + this.ranges.push(this.selection.getRangeAt(i)); + } + this.#initialized = true; } @@ -303,6 +305,10 @@ export class ScreenshotsOverlay { }; } + focus() { + this.previewCancelButton.focus({ focusVisible: true }); + } + /** * Returns the x and y coordinates of the event relative to both the * viewport and the page. @@ -316,7 +322,9 @@ export class ScreenshotsOverlay { * } */ getCoordinatesFromEvent(event) { - const { clientX, clientY, pageX, pageY } = event; + let { clientX, clientY, pageX, pageY } = event; + pageX -= this.windowDimensions.scrollMinX; + pageY -= this.windowDimensions.scrollMinY; return { clientX, clientY, pageX, pageY }; } @@ -341,6 +349,9 @@ export class ScreenshotsOverlay { case "keyup": this.handleKeyUp(event); break; + case "selectionchange": + this.handleSelectionChange(); + break; } } @@ -498,6 +509,127 @@ export class ScreenshotsOverlay { * @param {Event} event The keydown event */ handleKeyDown(event) { + if (event.key === "Escape") { + this.maybeCancelScreenshots(); + return; + } + + switch (this.#state) { + case STATES.CROSSHAIRS: + this.crosshairsKeyDown(event); + break; + case STATES.DRAGGING: + this.draggingKeyDown(event); + break; + case STATES.RESIZING: + this.resizingKeyDown(event); + break; + case STATES.SELECTED: + this.selectedKeyDown(event); + break; + } + } + + /** + * Handles when a keyup 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 (this.#state) { + case STATES.RESIZING: + 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, { doNotMoveFocus: true }); + break; + } + break; + } + 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; + } + + crosshairsKeyDown(event) { + switch (event.key) { + case "ArrowLeft": + case "ArrowUp": + case "ArrowRight": + case "ArrowDown": + // Do nothing so we can prevent default below + break; + case "Tab": + this.maybeLockFocus(event); + return; + case "Enter": + if (this.hoverElementRegion.isRegionValid) { + event.preventDefault(); + this.draggingReadyStart(); + this.draggingReadyDragEnd(); + return; + } + // eslint-disable-next-line no-fallthrough + case " ": { + if (Services.appinfo.isWayland) { + return; + } + + if (event.originalTarget === this.previewCancelButton) { + return; + } + + event.preventDefault(); + // The left and top coordinates from cursorRegion are relative to + // the client window so we need to add the scroll offset of the page to + // get the correct coordinates. + let x = {}; + let y = {}; + this.window.windowUtils.getLastOverWindowPointerLocationInCSSPixels( + x, + y + ); + this.crosshairsDragStart( + x.value + this.windowDimensions.scrollX, + y.value + this.windowDimensions.scrollY + ); + this.#setState(STATES.DRAGGING); + break; + } + default: + return; + } + + // Prevent scrolling with arrow keys + event.preventDefault(); + } + + /** + * Handles a keydown event for the dragging state. + * @param {Event} event The keydown event + */ + draggingKeyDown(event) { switch (event.key) { case "ArrowLeft": this.handleArrowLeftKeyDown(event); @@ -511,11 +643,71 @@ export class ScreenshotsOverlay { case "ArrowDown": this.handleArrowDownKeyDown(event); break; + case "Enter": + case " ": + event.preventDefault(); + this.#setState(STATES.SELECTED); + return; + default: + return; + } + + this.drawSelectionContainer(); + } + + /** + * Handles a keydown event for the resizing state. + * @param {Event} event The keydown event + */ + resizingKeyDown(event) { + switch (event.key) { + case "ArrowLeft": + this.resizingArrowLeftKeyDown(event); + break; + case "ArrowUp": + this.resizingArrowUpKeyDown(event); + break; + case "ArrowRight": + this.resizingArrowRightKeyDown(event); + break; + case "ArrowDown": + this.resizingArrowDownKeyDown(event); + break; + } + } + + selectedKeyDown(event) { + let isSelectionElement = event.originalTarget.closest( + "#selection-container" + ); + switch (event.key) { + case "ArrowLeft": + if (isSelectionElement) { + this.resizingArrowLeftKeyDown(event); + } + break; + case "ArrowUp": + if (isSelectionElement) { + this.resizingArrowUpKeyDown(event); + } + break; + case "ArrowRight": + if (isSelectionElement) { + this.resizingArrowRightKeyDown(event); + } + break; + case "ArrowDown": + if (isSelectionElement) { + this.resizingArrowDownKeyDown(event); + } + break; case "Tab": this.maybeLockFocus(event); break; - case "Escape": - this.maybeCancelScreenshots(); + case " ": + if (!event.originalTarget.closest("#buttons-container")) { + event.preventDefault(); + } break; case this.copyKey.toLowerCase(): if (this.state === "selected" && this.getAccelKey(event)) { @@ -533,16 +725,20 @@ export class ScreenshotsOverlay { } /** - * Gets the accel key depending on the platform. - * metaKey for macOS. ctrlKey for Windows and Linux. + * 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 - * @returns {Boolean} True if the accel key is pressed, false otherwise. */ - getAccelKey(event) { - if (AppConstants.platform === "macosx") { - return event.metaKey; + resizingArrowLeftKeyDown(event) { + this.handleArrowLeftKeyDown(event); + + if (this.#state !== STATES.RESIZING) { + this.#setState(STATES.RESIZING); } - return event.ctrlKey; + + this.drawSelectionContainer(); } /** @@ -553,6 +749,7 @@ export class ScreenshotsOverlay { * @param {Event} event The keydown event */ handleArrowLeftKeyDown(event) { + let exponent = event.shiftKey ? 1 : 0; switch (event.originalTarget.id) { case "highlight": if (this.getAccelKey(event)) { @@ -562,7 +759,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.right -= 10 ** event.shiftKey; + this.selectionRegion.right -= 10 ** exponent; // eslint-disable-next-line no-fallthrough case "mover-topLeft": case "mover-bottomLeft": @@ -571,7 +768,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.left -= 10 ** event.shiftKey; + this.selectionRegion.left -= 10 ** exponent; this.scrollIfByEdge( this.selectionRegion.left, this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2 @@ -591,7 +788,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.right -= 10 ** event.shiftKey; + this.selectionRegion.right -= 10 ** exponent; if (this.selectionRegion.x1 >= this.selectionRegion.x2) { this.selectionRegion.sortCoords(); if (event.originalTarget.id === "mover-topRight") { @@ -605,11 +802,23 @@ export class ScreenshotsOverlay { return; } + event.preventDefault(); + } + + /** + * 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 + */ + resizingArrowUpKeyDown(event) { + this.handleArrowUpKeyDown(event); + if (this.#state !== STATES.RESIZING) { this.#setState(STATES.RESIZING); } - event.preventDefault(); this.drawSelectionContainer(); } @@ -621,6 +830,7 @@ export class ScreenshotsOverlay { * @param {Event} event The keydown event */ handleArrowUpKeyDown(event) { + let exponent = event.shiftKey ? 1 : 0; switch (event.originalTarget.id) { case "highlight": if (this.getAccelKey(event)) { @@ -630,7 +840,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.bottom -= 10 ** event.shiftKey; + this.selectionRegion.bottom -= 10 ** exponent; // eslint-disable-next-line no-fallthrough case "mover-topLeft": case "mover-topRight": @@ -639,7 +849,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.top -= 10 ** event.shiftKey; + this.selectionRegion.top -= 10 ** exponent; this.scrollIfByEdge( this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2, this.selectionRegion.top @@ -659,7 +869,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.bottom -= 10 ** event.shiftKey; + this.selectionRegion.bottom -= 10 ** exponent; if (this.selectionRegion.y1 >= this.selectionRegion.y2) { this.selectionRegion.sortCoords(); if (event.originalTarget.id === "mover-bottomLeft") { @@ -673,11 +883,23 @@ export class ScreenshotsOverlay { return; } + event.preventDefault(); + } + + /** + * 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 + */ + resizingArrowRightKeyDown(event) { + this.handleArrowRightKeyDown(event); + if (this.#state !== STATES.RESIZING) { this.#setState(STATES.RESIZING); } - event.preventDefault(); this.drawSelectionContainer(); } @@ -689,6 +911,7 @@ export class ScreenshotsOverlay { * @param {Event} event The keydown event */ handleArrowRightKeyDown(event) { + let exponent = event.shiftKey ? 1 : 0; switch (event.originalTarget.id) { case "highlight": if (this.getAccelKey(event)) { @@ -699,7 +922,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.left += 10 ** event.shiftKey; + this.selectionRegion.left += 10 ** exponent; // eslint-disable-next-line no-fallthrough case "mover-topRight": case "mover-bottomRight": @@ -709,7 +932,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.right += 10 ** event.shiftKey; + this.selectionRegion.right += 10 ** exponent; this.scrollIfByEdge( this.selectionRegion.right, this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2 @@ -730,7 +953,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.left += 10 ** event.shiftKey; + this.selectionRegion.left += 10 ** exponent; if (this.selectionRegion.x1 >= this.selectionRegion.x2) { this.selectionRegion.sortCoords(); if (event.originalTarget.id === "mover-topLeft") { @@ -744,12 +967,7 @@ export class ScreenshotsOverlay { return; } - if (this.#state !== STATES.RESIZING) { - this.#setState(STATES.RESIZING); - } - event.preventDefault(); - this.drawSelectionContainer(); } /** @@ -759,7 +977,18 @@ export class ScreenshotsOverlay { * Arrow key + control/meta will move to the edge of the window. * @param {Event} event The keydown event */ + resizingArrowDownKeyDown(event) { + this.handleArrowDownKeyDown(event); + + if (this.#state !== STATES.RESIZING) { + this.#setState(STATES.RESIZING); + } + + this.drawSelectionContainer(); + } + handleArrowDownKeyDown(event) { + let exponent = event.shiftKey ? 1 : 0; switch (event.originalTarget.id) { case "highlight": if (this.getAccelKey(event)) { @@ -770,7 +999,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.top += 10 ** event.shiftKey; + this.selectionRegion.top += 10 ** exponent; // eslint-disable-next-line no-fallthrough case "mover-bottomLeft": case "mover-bottomRight": @@ -780,7 +1009,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.bottom += 10 ** event.shiftKey; + this.selectionRegion.bottom += 10 ** exponent; this.scrollIfByEdge( this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2, this.selectionRegion.bottom @@ -801,7 +1030,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.top += 10 ** event.shiftKey; + this.selectionRegion.top += 10 ** exponent; if (this.selectionRegion.y1 >= this.selectionRegion.y2) { this.selectionRegion.sortCoords(); if (event.originalTarget.id === "mover-topLeft") { @@ -815,12 +1044,7 @@ export class ScreenshotsOverlay { return; } - if (this.#state !== STATES.RESIZING) { - this.#setState(STATES.RESIZING); - } - event.preventDefault(); - this.drawSelectionContainer(); } /** @@ -829,27 +1053,39 @@ export class ScreenshotsOverlay { * @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 - ); + + switch (this.#state) { + case STATES.CROSSHAIRS: + if (event.shiftKey) { + this.#dispatchEvent("Screenshots:FocusPanel", { + direction: "backward", + }); + } else { + this.#dispatchEvent("Screenshots:FocusPanel", { + direction: "forward", + }); + } + break; + case STATES.SELECTED: + 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 + ); + } + break; } } @@ -866,27 +1102,15 @@ export class ScreenshotsOverlay { } /** - * 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 + * All of the selection ranges were recorded at initialization. The ranges + * are removed when focus is set to the buttons so we add the selection + * ranges back so a selected region can be captured. */ - 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; + handleSelectionChange() { + if (this.ranges.length) { + for (let range of this.ranges) { + this.selection.addRange(range); + } } } @@ -908,8 +1132,9 @@ export class ScreenshotsOverlay { /** * Set a new state for the overlay * @param {String} newState + * @param {Object} options (optional) Options for calling start of state method */ - #setState(newState) { + #setState(newState, options = {}) { if (this.#state === STATES.SELECTED && newState === STATES.CROSSHAIRS) { this.#dispatchEvent("Screenshots:RecordEvent", { eventName: "started", @@ -918,7 +1143,13 @@ export class ScreenshotsOverlay { } if (newState !== this.#state) { this.#dispatchEvent("Screenshots:OverlaySelection", { - hasSelection: newState == STATES.SELECTED, + hasSelection: [ + STATES.DRAGGING_READY, + STATES.DRAGGING, + STATES.RESIZING, + STATES.SELECTED, + ].includes(newState), + overlayState: newState, }); } this.#state = newState; @@ -937,7 +1168,7 @@ export class ScreenshotsOverlay { break; } case STATES.SELECTED: { - this.selectedStart(); + this.selectedStart(options); break; } case STATES.RESIZING: { @@ -997,11 +1228,16 @@ export class ScreenshotsOverlay { * Hide the preview and hover element containers. * Draw the selection and buttons containers. */ - selectedStart() { + selectedStart(options) { + this.selectionRegion.sortCoords(); this.hidePreviewContainer(); this.hideHoverElementContainer(); this.drawSelectionContainer(); this.drawButtonsContainer(); + + if (!options.doNotMoveFocus) { + this.setFocusToActionButton(); + } } /** @@ -1228,7 +1464,6 @@ export class ScreenshotsOverlay { if (this.hoverElementRegion.isRegionValid) { this.selectionRegion.dimensions = this.hoverElementRegion.dimensions; this.#setState(STATES.SELECTED); - this.setFocusToActionButton(); this.#dispatchEvent("Screenshots:RecordEvent", { eventName: "selected", reason: "element", @@ -1249,11 +1484,9 @@ export class ScreenshotsOverlay { right: pageX, bottom: pageY, }; - this.selectionRegion.sortCoords(); this.#setState(STATES.SELECTED); this.maybeRecordRegionSelected(); this.#methodsUsed.region += 1; - this.setFocusToActionButton(); } /** @@ -1264,9 +1497,7 @@ export class ScreenshotsOverlay { */ 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; @@ -1325,8 +1556,9 @@ export class ScreenshotsOverlay { * 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;`; + let { scrollWidth, scrollHeight, scrollMinX } = + this.windowDimensions.dimensions; + this.screenshotsContainer.style = `left:${scrollMinX};width:${scrollWidth}px;height:${scrollHeight}px;`; } showScreenshotsOverlayContainer() { @@ -1387,7 +1619,7 @@ export class ScreenshotsOverlay { let [selectionSizeTranslation] = lazy.overlayLocalization.formatMessagesSync([ { - id: "screenshots-overlay-selection-region-size-2", + id: "screenshots-overlay-selection-region-size-3", args: { width: Math.floor(width * zoom), height: Math.floor(height * zoom), @@ -1420,16 +1652,13 @@ export class ScreenshotsOverlay { right: boxRight, bottom: boxBottom, } = this.selectionRegion.dimensions; - let { clientWidth, clientHeight, scrollX, scrollY } = + + let { clientWidth, clientHeight, scrollX, scrollY, scrollWidth } = this.windowDimensions.dimensions; - if ( - boxTop > scrollY + clientHeight || - boxBottom < scrollY || - boxLeft > scrollX + clientWidth || - boxRight < scrollX - ) { - // The box is offscreen so need to draw the buttons + if (!this.windowDimensions.isInViewport(this.selectionRegion.dimensions)) { + // The box is entirely offscreen so need to draw the buttons + return; } @@ -1445,12 +1674,32 @@ export class ScreenshotsOverlay { } } - if (boxRight < 300) { - this.buttonsContainer.style.left = `${boxLeft}px`; - this.buttonsContainer.style.right = ""; - } else { - this.buttonsContainer.style.right = `calc(100% - ${boxRight}px)`; + if (!this.buttonsContainerRect) { + this.buttonsContainerRect = this.buttonsContainer.getBoundingClientRect(); + } + + let viewportLeft = scrollX; + let viewportRight = scrollX + clientWidth; + + let left, right; + let isLTR = !Services.locale.isAppLocaleRTL; + if (isLTR) { + left = Math.max( + Math.min(viewportRight, boxRight), + viewportLeft + Math.ceil(this.buttonsContainerRect.width) + ); + right = scrollWidth - left; + + this.buttonsContainer.style.right = `${right}px`; this.buttonsContainer.style.left = ""; + } else { + left = Math.min( + Math.max(viewportLeft, boxLeft), + viewportRight - Math.ceil(this.buttonsContainerRect.width) + ); + + this.buttonsContainer.style.left = `${left}px`; + this.buttonsContainer.style.right = ""; } this.buttonsContainer.style.top = `${top}px`; @@ -1464,6 +1713,10 @@ export class ScreenshotsOverlay { this.buttonsContainer.hidden = true; } + updateCursorRegion(left, top) { + this.cursorRegion = { left, top, right: left, bottom: top }; + } + /** * Set the pointer events to none on the screenshots elements so * elementFromPoint can find the real element at the given point. @@ -1595,8 +1848,10 @@ export class ScreenshotsOverlay { * 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 + * scrollMinX: The X minimum the viewport can scroll to + * scrollMinY: The Y minimum the viewport can scroll to + * scrollMaxX: The X maximum the viewport can scroll to + * scrollMaxY: The Y maximum the viewport can scroll to * } */ getDimensionsFromWindow() { @@ -1637,6 +1892,8 @@ export class ScreenshotsOverlay { scrollY, scrollMinX, scrollMinY, + scrollMaxX, + scrollMaxY, }; } @@ -1665,6 +1922,8 @@ export class ScreenshotsOverlay { scrollY, scrollMinX, scrollMinY, + scrollMaxX, + scrollMaxY, } = this.getDimensionsFromWindow(); this.screenshotsContainer.toggleAttribute("resizing", false); @@ -1677,6 +1936,8 @@ export class ScreenshotsOverlay { scrollY, scrollMinX, scrollMinY, + scrollMaxX, + scrollMaxY, devicePixelRatio: this.window.devicePixelRatio, }; diff --git a/browser/components/screenshots/ScreenshotsUtils.sys.mjs b/browser/components/screenshots/ScreenshotsUtils.sys.mjs index 9df74a4359..4ba925366d 100644 --- a/browser/components/screenshots/ScreenshotsUtils.sys.mjs +++ b/browser/components/screenshots/ScreenshotsUtils.sys.mjs @@ -91,6 +91,7 @@ export class ScreenshotsComponentParent extends JSWindowActorParent { case "Screenshots:OverlaySelection": ScreenshotsUtils.setPerBrowserState(browser, { hasOverlaySelection: message.data.hasSelection, + overlayState: message.data.overlayState, }); break; case "Screenshots:ShowPanel": @@ -99,6 +100,9 @@ export class ScreenshotsComponentParent extends JSWindowActorParent { case "Screenshots:HidePanel": ScreenshotsUtils.closePanel(browser); break; + case "Screenshots:MoveFocusToParent": + ScreenshotsUtils.focusPanel(browser, message.data); + break; } } @@ -191,11 +195,7 @@ export var ScreenshotsUtils = { handleEvent(event) { switch (event.type) { case "keydown": - if (event.key === "Escape") { - // Escape should cancel and exit - let browser = event.view.gBrowser.selectedBrowser; - this.cancel(browser, "escape"); - } + this.handleKeyDownEvent(event); break; case "TabSelect": this.handleTabSelect(event); @@ -209,6 +209,33 @@ export var ScreenshotsUtils = { } }, + handleKeyDownEvent(event) { + let browser = + event.view.browsingContext.topChromeWindow.gBrowser.selectedBrowser; + if (!browser) { + return; + } + + switch (event.key) { + case "Escape": + // The chromeEventHandler in the child actor will handle events that + // don't match this + if (event.target.parentElement === this.panelForBrowser(browser)) { + this.cancel(browser, "escape"); + } + break; + case "ArrowLeft": + case "ArrowUp": + case "ArrowRight": + case "ArrowDown": + this.handleArrowKeyDown(event, browser); + break; + case "Tab": + this.maybeLockFocus(event); + break; + } + }, + /** * When we swap docshells for a given screenshots browser, we need to update * the browserToScreenshotsState WeakMap to the correct browser. If the old @@ -273,6 +300,105 @@ export var ScreenshotsUtils = { } }, + /** + * If the overlay state is crosshairs or dragging, move the native cursor + * respective to the arrow key pressed. + * @param {Event} event A keydown event + * @param {Browser} browser The selected browser + * @returns + */ + handleArrowKeyDown(event, browser) { + // Wayland doesn't support `sendNativeMouseEvent` so just return + if (Services.appinfo.isWayland) { + return; + } + + let { overlayState } = this.browserToScreenshotsState.get(browser); + + if (!["crosshairs", "dragging"].includes(overlayState)) { + return; + } + + let left = 0; + let top = 0; + let exponent = event.shiftKey ? 1 : 0; + switch (event.key) { + case "ArrowLeft": + left -= 10 ** exponent; + break; + case "ArrowUp": + top -= 10 ** exponent; + break; + case "ArrowRight": + left += 10 ** exponent; + break; + case "ArrowDown": + top += 10 ** exponent; + break; + default: + return; + } + + // Clear and move focus to browser so the child actor can capture events + this.clearContentFocus(browser); + Services.focus.clearFocus(browser.ownerGlobal); + Services.focus.setFocus(browser, 0); + + let x = {}; + let y = {}; + let win = browser.ownerGlobal; + win.windowUtils.getLastOverWindowPointerLocationInCSSPixels(x, y); + + this.moveCursor( + { + left: (x.value + left) * win.devicePixelRatio, + top: (y.value + top) * win.devicePixelRatio, + }, + browser + ); + }, + + /** + * Move the native cursor to the given position. Clamp the position to the + * window just in case. + * @param {Object} position An object containing the left and top position + * @param {Browser} browser The selected browser + */ + moveCursor(position, browser) { + let { left, top } = position; + let win = browser.ownerGlobal; + + const windowLeft = win.mozInnerScreenX * win.devicePixelRatio; + const windowTop = win.mozInnerScreenY * win.devicePixelRatio; + const contentTop = + (win.mozInnerScreenY + (win.innerHeight - browser.clientHeight)) * + win.devicePixelRatio; + const windowRight = + (win.mozInnerScreenX + win.innerWidth) * win.devicePixelRatio; + const windowBottom = + (win.mozInnerScreenY + win.innerHeight) * win.devicePixelRatio; + + left += windowLeft; + top += windowTop; + + // Clamp left and top to content dimensions + let parsedLeft = Math.round( + Math.min(Math.max(left, windowLeft), windowRight) + ); + let parsedTop = Math.round( + Math.min(Math.max(top, contentTop), windowBottom) + ); + + win.windowUtils.sendNativeMouseEvent( + parsedLeft, + parsedTop, + win.windowUtils.NATIVE_MOUSE_MESSAGE_MOVE, + 0, + 0, + win.document.documentElement + ); + }, + observe(subj, topic, data) { let { gBrowser } = subj; let browser = gBrowser.selectedBrowser; @@ -335,6 +461,7 @@ export var ScreenshotsUtils = { browser.addEventListener("SwapDocShells", this); let gBrowser = browser.getTabBrowser(); gBrowser.tabContainer.addEventListener("TabSelect", this); + browser.ownerDocument.addEventListener("keydown", this); break; } case UIPhases.INITIAL: @@ -364,6 +491,7 @@ export var ScreenshotsUtils = { browser.removeEventListener("SwapDocShells", this); const gBrowser = browser.getTabBrowser(); gBrowser.tabContainer.removeEventListener("TabSelect", this); + browser.ownerDocument.removeEventListener("keydown", this); this.browserToScreenshotsState.delete(browser); if (Cu.isInAutomation) { @@ -396,6 +524,53 @@ export var ScreenshotsUtils = { Object.assign(perBrowserState, nameValues); }, + maybeLockFocus(event) { + let browser = event.view.gBrowser.selectedBrowser; + + if (!Services.focus.focusedElement) { + event.preventDefault(); + this.focusPanel(browser); + return; + } + + let target = event.explicitOriginalTarget; + + if (!target.closest("moz-button-group")) { + return; + } + + let isElementFirst = !!target.nextElementSibling; + + if ( + (isElementFirst && event.shiftKey) || + (!isElementFirst && !event.shiftKey) + ) { + event.preventDefault(); + this.moveFocusToContent(browser); + } + }, + + focusPanel(browser, { direction } = {}) { + let buttonsPanel = this.panelForBrowser(browser); + if (direction) { + buttonsPanel + .querySelector("screenshots-buttons") + .focusButton(direction === "forward" ? "first" : "last"); + } else { + buttonsPanel + .querySelector("screenshots-buttons") + .focusButton(lazy.SCREENSHOTS_LAST_SCREENSHOT_METHOD); + } + }, + + moveFocusToContent(browser) { + this.getActor(browser).sendAsyncMessage("Screenshots:MoveFocusToContent"); + }, + + clearContentFocus(browser) { + this.getActor(browser).sendAsyncMessage("Screenshots:ClearFocus"); + }, + /** * Attempt to place focus on the element that had focus before screenshots UI was shown * @@ -510,7 +685,7 @@ export var ScreenshotsUtils = { 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}`, + `chrome://browser/content/screenshots/screenshots-preview.html?browsingContextId=${browser.browsingContext.id}`, { features: "resizable=no", sizeTo: "available", @@ -586,14 +761,18 @@ export var ScreenshotsUtils = { openPanel(browser) { let buttonsPanel = this.panelForBrowser(browser); if (!buttonsPanel.hidden) { - return; + return null; } buttonsPanel.hidden = false; - buttonsPanel.ownerDocument.addEventListener("keydown", this); - buttonsPanel - .querySelector("screenshots-buttons") - .focusButton(lazy.SCREENSHOTS_LAST_SCREENSHOT_METHOD); + return new Promise(resolve => { + browser.ownerGlobal.requestAnimationFrame(() => { + buttonsPanel + .querySelector("screenshots-buttons") + .focusButton(lazy.SCREENSHOTS_LAST_SCREENSHOT_METHOD); + resolve(); + }); + }); }, /** @@ -606,7 +785,6 @@ export var ScreenshotsUtils = { return; } buttonsPanel.hidden = true; - buttonsPanel.ownerDocument.removeEventListener("keydown", this); }, /** @@ -652,7 +830,6 @@ export var ScreenshotsUtils = { 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) { @@ -661,7 +838,7 @@ export var ScreenshotsUtils = { dialog._openedURL.endsWith( `browsingContextId=${browserContextId}` ) && - dialog._openedURL.includes("screenshots.html") + dialog._openedURL.includes("screenshots-preview.html") ) { return dialog; } @@ -817,12 +994,11 @@ export var ScreenshotsUtils = { let dialog = await this.openPreviewDialog(browser); await dialog._dialogReady; - let screenshotsUI = dialog._frame.contentDocument.createElement( + let screenshotsPreviewEl = dialog._frame.contentDocument.querySelector( "screenshots-preview" ); - dialog._frame.contentDocument.body.appendChild(screenshotsUI); - screenshotsUI.focusButton(lazy.SCREENSHOTS_LAST_SAVED_METHOD); + screenshotsPreviewEl.focusButton(lazy.SCREENSHOTS_LAST_SAVED_METHOD); let rect; let lastUsedMethod; @@ -852,15 +1028,12 @@ export var ScreenshotsUtils = { async takeScreenshot(browser, dialog, rect) { let canvas = await this.createCanvas(rect, browser); - let newImg = dialog._frame.contentDocument.createElement("img"); let url = canvas.toDataURL(); + let screenshotsPreviewEl = dialog._frame.contentDocument.querySelector( + "screenshots-preview" + ); - newImg.id = "placeholder-image"; - - newImg.src = url; - dialog._frame.contentDocument - .getElementById("preview-image-div") - .appendChild(newImg); + screenshotsPreviewEl.previewImg.src = url; if (Cu.isInAutomation) { Services.obs.notifyObservers(null, "screenshots-preview-ready"); @@ -1051,14 +1224,19 @@ export var ScreenshotsUtils = { * @param dataUrl The image data * @param browser The current browser * @param data Telemetry data + * @returns true if the download succeeds, otherwise false */ async downloadScreenshot(title, dataUrl, browser, data) { // Guard against missing image data. if (!dataUrl) { - return; + return false; } - let filename = await getFilename(title, browser); + let { filename, accepted } = await getFilename(title, browser); + + if (!accepted) { + return false; + } const targetFile = new lazy.FileUtils.File(filename); @@ -1080,7 +1258,15 @@ export var ScreenshotsUtils = { // Await successful completion of the save via the download manager await download.start(); - } catch (ex) {} + } catch (ex) { + console.error( + `Failed to create download using filename: ${filename} (length: ${ + new Blob([filename]).size + })` + ); + + return false; + } let extra = await this.getActor(browser).sendQuery( "Screenshots:GetMethodsUsed" @@ -1095,6 +1281,8 @@ export var ScreenshotsUtils = { SCREENSHOTS_LAST_SAVED_METHOD_PREF, "download" ); + + return true; }, recordTelemetryEvent(type, object, args) { diff --git a/browser/components/screenshots/content/screenshots.css b/browser/components/screenshots/content/screenshots.css deleted file mode 100644 index b155c294f8..0000000000 --- a/browser/components/screenshots/content/screenshots.css +++ /dev/null @@ -1,79 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -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; - gap: var(--space-xsmall); - cursor: pointer; - text-align: center; - user-select: none; - white-space: nowrap; - min-width: 32px; -} - -.preview-button > img { - -moz-context-properties: fill; - fill: currentColor; - width: 16px; - height: 16px; - pointer-events: none; -} - -#retry > img { - content: url("chrome://global/skin/icons/reload.svg"); -} - -#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"); -} - -.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 deleted file mode 100644 index fea032700c..0000000000 --- a/browser/components/screenshots/content/screenshots.html +++ /dev/null @@ -1,70 +0,0 @@ -<!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-component-retry-button" - > - <img /> - </button> - <button - id="cancel" - class="preview-button" - data-l10n-id="screenshots-component-cancel-button" - > - <img /> - </button> - <button - id="copy" - class="preview-button" - data-l10n-id="screenshots-component-copy-button" - > - <img /><label - data-l10n-id="screenshots-component-copy-button-label" - ></label> - </button> - <button - id="download" - class="preview-button primary" - data-l10n-id="screenshots-component-download-button" - > - <img /><label - data-l10n-id="screenshots-component-download-button-label" - ></label> - </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 deleted file mode 100644 index 8159206d18..0000000000 --- a/browser/components/screenshots/content/screenshots.js +++ /dev/null @@ -1,185 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* eslint-env mozilla/browser-window */ - -"use strict"; - -ChromeUtils.defineESModuleGetters(this, { - AppConstants: "resource://gre/modules/AppConstants.sys.mjs", - ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs", - ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs", -}); - -const lazy = {}; - -ChromeUtils.defineLazyGetter(lazy, "screenshotsLocalization", () => { - return new Localization(["browser/screenshots.ftl"], true); -}); - -class ScreenshotsPreview extends HTMLElement { - constructor() { - super(); - // we get passed the <browser> as a param via TabDialogBox.open() - this.openerBrowser = window.arguments[0]; - - window.ensureCustomElements("moz-button"); - - let [downloadKey, copyKey] = - lazy.screenshotsLocalization.formatMessagesSync([ - { id: "screenshots-component-download-key" }, - { id: "screenshots-component-copy-key" }, - ]); - - this.downloadKey = downloadKey.value; - this.copyKey = copyKey.value; - } - - 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); - - let accelString = ShortcutUtils.getModifierString("accel"); - let copyShorcut = accelString + this.copyKey; - let downloadShortcut = accelString + this.downloadKey; - - document.l10n.setAttributes( - this._cancelButton, - "screenshots-component-cancel-button" - ); - - document.l10n.setAttributes( - this._copyButton, - "screenshots-component-copy-button", - { shortcut: copyShorcut } - ); - - document.l10n.setAttributes( - this._downloadButton, - "screenshots-component-download-button", - { shortcut: downloadShortcut } - ); - - window.addEventListener("keydown", this, true); - } - - close() { - URL.revokeObjectURL(document.getElementById("placeholder-image").src); - window.close(); - } - - handleEvent(event) { - switch (event.type) { - case "click": - this.handleClick(event); - break; - case "keydown": - this.handleKeydown(event); - break; - } - } - - handleClick(event) { - switch (event.target.id) { - case "retry": - ScreenshotsUtils.scheduleRetry(this.openerBrowser, "preview_retry"); - this.close(); - break; - case "cancel": - this.close(); - ScreenshotsUtils.recordTelemetryEvent("canceled", "preview_cancel", {}); - break; - case "copy": - this.saveToClipboard( - this.ownerDocument.getElementById("placeholder-image").src - ); - break; - case "download": - this.saveToFile( - this.ownerDocument.getElementById("placeholder-image").src - ); - break; - } - } - - handleKeydown(event) { - switch (event.key) { - case this.copyKey.toLowerCase(): - if (this.getAccelKey(event)) { - event.preventDefault(); - event.stopPropagation(); - this.saveToClipboard( - this.ownerDocument.getElementById("placeholder-image").src - ); - } - break; - case this.downloadKey.toLowerCase(): - if (this.getAccelKey(event)) { - event.preventDefault(); - event.stopPropagation(); - this.saveToFile( - this.ownerDocument.getElementById("placeholder-image").src - ); - } - break; - } - } - - getAccelKey(event) { - if (AppConstants.platform === "macosx") { - return event.metaKey; - } - return event.ctrlKey; - } - - 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-preview", ScreenshotsPreview); diff --git a/browser/components/screenshots/fileHelpers.mjs b/browser/components/screenshots/fileHelpers.mjs index 4fd2e77561..f416ca195a 100644 --- a/browser/components/screenshots/fileHelpers.mjs +++ b/browser/components/screenshots/fileHelpers.mjs @@ -7,11 +7,15 @@ const { AppConstants } = ChromeUtils.importESModule( ); const lazy = {}; +// The maximum length of a pathanme - calculated as MAX_PATH minus the null terminator character +export const MAX_PATHNAME = AppConstants.platform == "win" ? 259 : 1023; +export const MAX_LEAFNAME = MAX_PATHNAME - 32; +export const FALLBACK_MAX_LEAFNAME = 64; ChromeUtils.defineESModuleGetters(lazy, { + Downloads: "resource://gre/modules/Downloads.sys.mjs", 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", }); @@ -29,6 +33,15 @@ export async function getFilename(filenameTitle, browser) { ); } const date = new Date(); + const knownDownloadsDir = await getDownloadDirectory(); + // if we know the download directory, we can subtract that plus the separator from MAX_PATHNAME to get a length limit + // otherwise we just use a conservative length + const maxFilenameLength = Math.min( + knownDownloadsDir + ? MAX_PATHNAME - new Blob([knownDownloadsDir]).size - 1 + : FALLBACK_MAX_LEAFNAME, + MAX_LEAFNAME + ); /* eslint-disable no-control-regex */ filenameTitle = filenameTitle .replace(/[\\/]/g, "_") @@ -44,43 +57,37 @@ export async function getFilename(filenameTitle, browser) { 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 + // allow space for a potential ellipsis and the extension + let maxNameStemLength = maxFilenameLength - "[...].png".length; + + // Crop the filename size 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 + // maximum of maxFilenameLength bytes (not characters). Here, we iterate // and crop at shorter and shorter points until we fit into - // 255 bytes. + // our max number of bytes. let suffix = ""; - for (let cropSize = 246; cropSize >= 0; cropSize -= 32) { - if (new Blob([clipFilename]).size > 246) { + for (let cropSize = maxNameStemLength; cropSize >= 0; cropSize -= 32) { + if (new Blob([clipFilename]).size > maxNameStemLength) { 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); - } + if (knownDownloadsDir) { + // If filename is absolute, it will override the downloads directory and + // still be applied as expected. + filename = PathUtils.join(knownDownloadsDir, filename); } else { let fileInfo = new FileInfo(filename); let file; - let fpParams = { fpTitleKey: "SaveImageTitle", fileInfo, @@ -88,16 +95,30 @@ export async function getFilename(filenameTitle, browser) { saveAsType: 0, file, }; - let accepted = await promiseTargetFile(fpParams, browser.ownerGlobal); if (!accepted) { - return null; + return { filename: null, accepted }; } - filename = fpParams.file.path; } + return { filename, accepted: true }; +} - return filename; +/** + * Gets the path to the download directory if "browser.download.useDownloadDir" is true + * @returns Path to download directory or null if not available + */ +export async function getDownloadDirectory() { + let useDownloadDir = Services.prefs.getBoolPref( + "browser.download.useDownloadDir" + ); + if (useDownloadDir) { + const downloadsDir = await lazy.Downloads.getPreferredDownloadsDirectory(); + if (await IOUtils.exists(downloadsDir)) { + return downloadsDir; + } + } + return null; } // The below functions are a modified copy from toolkit/content/contentAreaUtils.js diff --git a/browser/components/screenshots/jar.mn b/browser/components/screenshots/jar.mn index 7a4e2ed73a..4618f78c52 100644 --- a/browser/components/screenshots/jar.mn +++ b/browser/components/screenshots/jar.mn @@ -12,11 +12,11 @@ browser.jar: 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/screenshots-preview.css (screenshots-preview.css) + content/browser/screenshots/screenshots-preview.html (screenshots-preview.html) + content/browser/screenshots/screenshots-preview.mjs (screenshots-preview.mjs) content/browser/screenshots/overlay/ (overlay/**) content/browser/screenshots/overlayHelpers.mjs diff --git a/browser/components/screenshots/overlay/overlay.css b/browser/components/screenshots/overlay/overlay.css index b042f0b0c2..d8aeb1f907 100644 --- a/browser/components/screenshots/overlay/overlay.css +++ b/browser/components/screenshots/overlay/overlay.css @@ -53,7 +53,7 @@ justify-content: center; position: sticky; top: 0; - left: 0; + inset-inline: 0; width: 100vw; height: 100vh; background-color: rgba(0, 0, 0, 0.7); @@ -74,6 +74,11 @@ border-radius: 4px; } +#selection-size { + border: var(--border-width) solid var(--in-content-border-color); + box-shadow: var(--shadow-30); +} + .buttons-wrapper, #selection-size-container { display: flex; @@ -107,7 +112,7 @@ border-color: #fff; color: #fff; - @media (prefers-contrast) { + @media (forced-colors) { background-color: var(--in-content-button-background); color: var(--in-content-button-text-color); border-color: var(--in-content-button-border-color); @@ -118,7 +123,7 @@ background-color: #fff; color: #000; - @media (prefers-contrast) { + @media (forced-colors) { 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); @@ -154,7 +159,7 @@ width: 64px; height: 64px; - @media (prefers-contrast) { + @media (forced-colors) { display: none; } } @@ -208,7 +213,7 @@ padding: 20px; width: 400px; - @media (prefers-contrast) { + @media (forced-colors) { color: CanvasText; background-color: Canvas; } @@ -351,7 +356,7 @@ width: 16px; pointer-events: none; - @media (prefers-contrast) { + @media (forced-colors) { background-color: ButtonText; } } diff --git a/browser/components/screenshots/overlayHelpers.mjs b/browser/components/screenshots/overlayHelpers.mjs index 70a1bd86d0..e91200a8f5 100644 --- a/browser/components/screenshots/overlayHelpers.mjs +++ b/browser/components/screenshots/overlayHelpers.mjs @@ -402,6 +402,8 @@ export class WindowDimensions { #scrollY = null; #scrollMinX = null; #scrollMinY = null; + #scrollMaxX = null; + #scrollMaxY = null; #devicePixelRatio = null; set dimensions(dimensions) { @@ -429,6 +431,12 @@ export class WindowDimensions { if (dimensions.scrollMinY != null) { this.#scrollMinY = dimensions.scrollMinY; } + if (dimensions.scrollMaxX != null) { + this.#scrollMaxX = dimensions.scrollMaxX; + } + if (dimensions.scrollMaxY != null) { + this.#scrollMaxY = dimensions.scrollMaxY; + } if (dimensions.devicePixelRatio != null) { this.#devicePixelRatio = dimensions.devicePixelRatio; } @@ -436,15 +444,19 @@ export class WindowDimensions { 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, + clientHeight: this.clientHeight, + clientWidth: this.clientWidth, + scrollHeight: this.scrollHeight, + scrollWidth: this.scrollWidth, + scrollX: this.scrollX, + scrollY: this.scrollY, + pageScrollX: this.pageScrollX, + pageScrollY: this.pageScrollY, + scrollMinX: this.scrollMinX, + scrollMinY: this.scrollMinY, + scrollMaxX: this.scrollMaxX, + scrollMaxY: this.scrollMaxY, + devicePixelRatio: this.devicePixelRatio, }; } @@ -465,10 +477,18 @@ export class WindowDimensions { } get scrollX() { + return this.#scrollX - this.scrollMinX; + } + + get pageScrollX() { return this.#scrollX; } get scrollY() { + return this.#scrollY - this.scrollMinY; + } + + get pageScrollY() { return this.#scrollY; } @@ -480,10 +500,33 @@ export class WindowDimensions { return this.#scrollMinY; } + get scrollMaxX() { + return this.#scrollMaxX; + } + + get scrollMaxY() { + return this.#scrollMaxY; + } + get devicePixelRatio() { return this.#devicePixelRatio; } + isInViewport(rect) { + // eslint-disable-next-line no-shadow + let { left, top, right, bottom } = rect; + + if ( + left > this.scrollX + this.clientWidth || + right < this.scrollX || + top > this.scrollY + this.clientHeight || + bottom < this.scrollY + ) { + return false; + } + return true; + } + reset() { this.#clientHeight = 0; this.#clientWidth = 0; @@ -493,5 +536,7 @@ export class WindowDimensions { this.#scrollY = 0; this.#scrollMinX = 0; this.#scrollMinY = 0; + this.#scrollMaxX = 0; + this.#scrollMaxY = 0; } } diff --git a/browser/components/screenshots/screenshots-buttons.css b/browser/components/screenshots/screenshots-buttons.css index b63308d8b4..ccb092174e 100644 --- a/browser/components/screenshots/screenshots-buttons.css +++ b/browser/components/screenshots/screenshots-buttons.css @@ -14,15 +14,15 @@ border-radius: var(--arrowpanel-border-radius); } -.full-page { +#full-page { background-image: url("chrome://browser/content/screenshots/menu-fullpage.svg"); } -.visible-page { +#visible-page { background-image: url("chrome://browser/content/screenshots/menu-visible.svg"); } -.full-page, .visible-page { +#full-page, #visible-page { -moz-context-properties: fill, stroke; fill: currentColor; /* stroke is the secondary fill color used to define the viewport shape in the SVGs */ diff --git a/browser/components/screenshots/screenshots-buttons.js b/browser/components/screenshots/screenshots-buttons.js index 9ac8dab2cf..e501da5a51 100644 --- a/browser/components/screenshots/screenshots-buttons.js +++ b/browser/components/screenshots/screenshots-buttons.js @@ -20,9 +20,10 @@ <html:link rel="stylesheet" href="chrome://global/skin/global.css" /> <html:link rel="stylesheet" href="chrome://browser/content/screenshots/screenshots-buttons.css" /> <html:moz-button-group> - <html:button class="visible-page footer-button" data-l10n-id="screenshots-save-visible-button"></html:button> - <html:button class="full-page footer-button primary" data-l10n-id="screenshots-save-page-button"></html:button> + <html:button id="visible-page" class="screenshot-button footer-button" data-l10n-id="screenshots-save-visible-button"></html:button> + <html:button id="full-page" class="screenshot-button footer-button primary" data-l10n-id="screenshots-save-page-button"></html:button> </html:moz-button-group> + `; } @@ -41,12 +42,12 @@ this.shadowRoot.append(ScreenshotsButtons.fragment); - let visibleButton = this.shadowRoot.querySelector(".visible-page"); + let visibleButton = shadowRoot.getElementById("visible-page"); visibleButton.onclick = function () { ScreenshotsUtils.doScreenshot(gBrowser.selectedBrowser, "visible"); }; - let fullpageButton = this.shadowRoot.querySelector(".full-page"); + let fullpageButton = shadowRoot.getElementById("full-page"); fullpageButton.onclick = function () { ScreenshotsUtils.doScreenshot(gBrowser.selectedBrowser, "full_page"); }; @@ -65,11 +66,19 @@ await this.shadowRoot.querySelector("moz-button-group").updateComplete; if (buttonToFocus === "fullpage") { this.shadowRoot - .querySelector(".full-page") + .getElementById("full-page") .focus({ focusVisible: true }); + } else if (buttonToFocus === "first") { + this.shadowRoot + .querySelector("moz-button-group") + .firstElementChild.focus({ focusVisible: true }); + } else if (buttonToFocus === "last") { + this.shadowRoot + .querySelector("moz-button-group") + .lastElementChild.focus({ focusVisible: true }); } else { this.shadowRoot - .querySelector(".visible-page") + .getElementById("visible-page") .focus({ focusVisible: true }); } } diff --git a/browser/components/screenshots/screenshots-preview.css b/browser/components/screenshots/screenshots-preview.css new file mode 100644 index 0000000000..f69392f42b --- /dev/null +++ b/browser/components/screenshots/screenshots-preview.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 { + height: 100vh; + width: 100vw; + display: block; +} + +.image-view { + display: flex; + flex-direction: column; + height: 100vh; +} + +.preview-buttons { + display: flex; + gap: var(--space-small); + align-items: center; + justify-content: flex-end; + width: 98%; + padding: var(--space-small) 0; +} + +#preview-image-container { + height: 100vh; + overflow: auto; + padding: 2%; + padding-top: 0; +} + +#preview-image { + width: 100%; +} diff --git a/browser/components/screenshots/screenshots-preview.html b/browser/components/screenshots/screenshots-preview.html new file mode 100644 index 0000000000..83ddbae08f --- /dev/null +++ b/browser/components/screenshots/screenshots-preview.html @@ -0,0 +1,25 @@ +<!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" /> + <script + type="module" + src="chrome://browser/content/screenshots/screenshots-preview.mjs" + ></script> + </head> + + <body> + <screenshots-preview></screenshots-preview> + </body> +</html> diff --git a/browser/components/screenshots/screenshots-preview.mjs b/browser/components/screenshots/screenshots-preview.mjs new file mode 100644 index 0000000000..0609bac959 --- /dev/null +++ b/browser/components/screenshots/screenshots-preview.mjs @@ -0,0 +1,271 @@ +/* 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 { html } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-button.mjs"; + +const lazy = {}; + +ChromeUtils.defineLazyGetter(lazy, "screenshotsLocalization", () => { + return new Localization(["browser/screenshots.ftl"], true); +}); + +ChromeUtils.defineESModuleGetters(lazy, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs", + ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs", +}); + +class ScreenshotsPreview extends MozLitElement { + static queries = { + retryButtonEl: "#retry", + cancelButtonEl: "#cancel", + copyButtonEl: "#copy", + downloadButtonEl: "#download", + previewImg: "#preview-image", + buttons: { all: "moz-button" }, + }; + + constructor() { + super(); + // we get passed the <browser> as a param via TabDialogBox.open() + this.openerBrowser = window.arguments[0]; + + let [downloadKey, copyKey] = + lazy.screenshotsLocalization.formatMessagesSync([ + { id: "screenshots-component-download-key" }, + { id: "screenshots-component-copy-key" }, + ]); + + this.downloadKey = downloadKey.value; + this.copyKey = copyKey.value; + } + + connectedCallback() { + super.connectedCallback(); + + window.addEventListener("keydown", this, true); + + this.updateL10nAttributes(); + } + + async updateL10nAttributes() { + let accelString = lazy.ShortcutUtils.getModifierString("accel"); + let copyShorcut = accelString + this.copyKey; + let downloadShortcut = accelString + this.downloadKey; + + await this.updateComplete; + + document.l10n.setAttributes( + this.copyButtonEl, + "screenshots-component-copy-button-2", + { shortcut: copyShorcut } + ); + + document.l10n.setAttributes( + this.downloadButtonEl, + "screenshots-component-download-button-2", + { shortcut: downloadShortcut } + ); + } + + close() { + window.removeEventListener("keydown", this, true); + URL.revokeObjectURL(this.previewImg.src); + window.close(); + } + + handleEvent(event) { + switch (event.type) { + case "click": + this.handleClick(event); + break; + case "keydown": + this.handleKeydown(event); + break; + } + } + + handleClick(event) { + switch (event.target.id) { + case "retry": + lazy.ScreenshotsUtils.scheduleRetry( + this.openerBrowser, + "preview_retry" + ); + this.close(); + break; + case "cancel": + this.close(); + lazy.ScreenshotsUtils.recordTelemetryEvent( + "canceled", + "preview_cancel", + {} + ); + break; + case "copy": + this.saveToClipboard(); + break; + case "download": + this.saveToFile(); + break; + } + } + + handleKeydown(event) { + switch (event.key) { + case this.copyKey.toLowerCase(): + if (this.getAccelKey(event)) { + event.preventDefault(); + event.stopPropagation(); + this.saveToClipboard(); + } + break; + case this.downloadKey.toLowerCase(): + if (this.getAccelKey(event)) { + event.preventDefault(); + event.stopPropagation(); + this.saveToFile(); + } + break; + } + } + + /** + * If the image is complete and the height is greater than 0, we can resolve. + * Otherwise wait for a load event on the image and resolve then. + * @returns {Promise<String>} Resolves that resolves to the preview image src + * once the image is loaded. + */ + async imageLoadedPromise() { + await this.updateComplete; + if (this.previewImg.complete && this.previewImg.height > 0) { + return Promise.resolve(this.previewImg.src); + } + + return new Promise(resolve => { + function onImageLoaded(event) { + resolve(event.target.src); + } + this.previewImg.addEventListener("load", onImageLoaded, { once: true }); + }); + } + + getAccelKey(event) { + if (lazy.AppConstants.platform === "macosx") { + return event.metaKey; + } + return event.ctrlKey; + } + + /** + * Enable all the buttons. This will only happen when the download button is + * clicked and the file picker is closed without saving the image. + */ + enableButtons() { + this.buttons.forEach(button => (button.disabled = false)); + } + + /** + * Disable all the buttons so they can't be clicked multiple times before + * successfully copying or downloading the image. + */ + disableButtons() { + this.buttons.forEach(button => (button.disabled = true)); + } + + async saveToFile() { + // Disable buttons so they can't by clicked again while waiting for the + // image to load. + this.disableButtons(); + + // Wait for the image to be loaded before we save it + let imageSrc = await this.imageLoadedPromise(); + let downloadSucceeded = await lazy.ScreenshotsUtils.downloadScreenshot( + null, + imageSrc, + this.openerBrowser, + { object: "preview_download" } + ); + + if (downloadSucceeded) { + this.close(); + } else { + this.enableButtons(); + } + } + + async saveToClipboard() { + // Disable buttons so they can't by clicked again while waiting for the + // image to load + this.disableButtons(); + + // Wait for the image to be loaded before we copy it + let imageSrc = await this.imageLoadedPromise(); + await lazy.ScreenshotsUtils.copyScreenshot(imageSrc, 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.copyButtonEl.focus({ focusVisible: true }); + } else { + this.downloadButtonEl.focus({ focusVisible: true }); + } + } + + render() { + return html` + <link + rel="stylesheet" + href="chrome://browser/content/screenshots/screenshots-preview.css" + /> + <div class="image-view"> + <div class="preview-buttons"> + <moz-button + id="retry" + data-l10n-id="screenshots-component-retry-button" + iconSrc="chrome://global/skin/icons/reload.svg" + @click=${this.handleClick} + ></moz-button> + <moz-button + id="cancel" + data-l10n-id="screenshots-component-cancel-button" + iconSrc="chrome://global/skin/icons/close.svg" + @click=${this.handleClick} + ></moz-button> + <moz-button + id="copy" + data-l10n-id="screenshots-component-copy-button-2" + data-l10n-args='{ "shortcut": "" }' + iconSrc="chrome://global/skin/icons/edit-copy.svg" + @click=${this.handleClick} + ></moz-button> + <moz-button + id="download" + type="primary" + data-l10n-id="screenshots-component-download-button-2" + data-l10n-args='{ "shortcut": "" }' + iconSrc="chrome://browser/skin/downloads/downloads.svg" + @click=${this.handleClick} + ></moz-button> + </div> + <div id="preview-image-container"> + <img id="preview-image" /> + </div> + </div> + `; + } +} + +customElements.define("screenshots-preview", ScreenshotsPreview); diff --git a/browser/components/screenshots/tests/browser/browser.toml b/browser/components/screenshots/tests/browser/browser.toml index 97e7474fa3..472bc853d1 100644 --- a/browser/components/screenshots/tests/browser/browser.toml +++ b/browser/components/screenshots/tests/browser/browser.toml @@ -8,6 +8,8 @@ support-files = [ "short-test-page.html", "large-test-page.html", "test-page-resize.html", + "test-selectionAPI-page.html", + "rtl-test-page.html", ] prefs = [ @@ -19,6 +21,12 @@ prefs = [ skip-if = ["os == 'linux'"] ["browser_keyboard_shortcuts.js"] +skip-if = [ + "headless", + "display == 'wayland'" # sendNativeMouseEvent doesn't work on wayland +] + +["browser_keyboard_tests.js"] ["browser_overlay_keyboard_test.js"] @@ -28,6 +36,8 @@ skip-if = [ "apple_catalina", # Bug 1804441 ] +["browser_screenshots_download_filenames.js"] + ["browser_screenshots_drag_test.js"] ["browser_screenshots_focus_test.js"] @@ -67,3 +77,5 @@ skip-if = ["!crashreporter"] ["browser_test_resize.js"] ["browser_test_selection_size_text.js"] + +["browser_text_selectionAPI_test.js"] diff --git a/browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js b/browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js index bca96f333f..66ab25f1c6 100644 --- a/browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js +++ b/browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js @@ -56,7 +56,7 @@ add_task(async function test_download_shortcut() { "screenshots-preview-ready" ); - let visibleButton = await helper.getPanelButton(".visible-page"); + let visibleButton = await helper.getPanelButton("#visible-page"); visibleButton.click(); await screenshotReady; @@ -108,7 +108,7 @@ add_task(async function test_copy_shortcut() { "screenshots-preview-ready" ); - let visibleButton = await helper.getPanelButton(".visible-page"); + let visibleButton = await helper.getPanelButton("#visible-page"); visibleButton.click(); await screenshotReady; diff --git a/browser/components/screenshots/tests/browser/browser_keyboard_tests.js b/browser/components/screenshots/tests/browser/browser_keyboard_tests.js new file mode 100644 index 0000000000..b2bb6fd16e --- /dev/null +++ b/browser/components/screenshots/tests/browser/browser_keyboard_tests.js @@ -0,0 +1,482 @@ +/* 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: 100, + left: 100, + bottom: 100, + right: 110, + }, + ], + [ + "ArrowDown", + { + top: 100, + left: 100, + bottom: 110, + right: 110, + }, + ], + [ + "ArrowLeft", + { + top: 100, + left: 100, + bottom: 110, + right: 100, + }, + ], + [ + "ArrowUp", + { + top: 100, + left: 100, + bottom: 100, + right: 100, + }, + ], + ["ArrowDown", { top: 100, left: 100, bottom: 110, right: 100 }], + [ + "ArrowRight", + { + top: 100, + left: 100, + bottom: 110, + right: 110, + }, + ], + [ + "ArrowUp", + { + top: 100, + left: 100, + bottom: 100, + right: 110, + }, + ], + [ + "ArrowLeft", + { + top: 100, + left: 100, + bottom: 100, + right: 100, + }, + ], +]; + +const SHIFT_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY = [ + [ + "ArrowRight", + { + top: 100, + left: 100, + bottom: 100, + right: 200, + }, + ], + [ + "ArrowDown", + { + top: 100, + left: 100, + bottom: 200, + right: 200, + }, + ], + [ + "ArrowLeft", + { + top: 100, + left: 100, + bottom: 200, + right: 100, + }, + ], + [ + "ArrowUp", + { + top: 100, + left: 100, + bottom: 100, + right: 100, + }, + ], + ["ArrowDown", { top: 100, left: 100, bottom: 200, right: 100 }], + [ + "ArrowRight", + { + top: 100, + left: 100, + bottom: 200, + right: 200, + }, + ], + [ + "ArrowUp", + { + top: 100, + left: 100, + bottom: 100, + right: 200, + }, + ], + [ + "ArrowLeft", + { + top: 100, + left: 100, + bottom: 100, + right: 100, + }, + ], +]; + +async function doKeyPress(key, options, window) { + let { repeat } = options; + if (repeat) { + delete options.repeat; + for (let i = 0; i < repeat; i++) { + let mouseEvent = BrowserTestUtils.waitForEvent(window, "mousemove"); + EventUtils.synthesizeKey(key, options, window); + await mouseEvent; + } + } else { + let mouseEvent = BrowserTestUtils.waitForEvent(window, "mousemove"); + EventUtils.synthesizeKey(key, options, window); + await mouseEvent; + } +} + +function assertSelectionRegionDimensions(actualDimensions, expectedDimensions) { + is( + Math.round(actualDimensions.top), + expectedDimensions.top, + "Top dimension is correct" + ); + is( + Math.round(actualDimensions.left), + expectedDimensions.left, + "Left dimension is correct" + ); + is( + Math.round(actualDimensions.bottom), + expectedDimensions.bottom, + "Bottom dimension is correct" + ); + is( + Math.round(actualDimensions.right), + expectedDimensions.right, + "Right dimension is correct" + ); +} + +add_task(async function test_elementSelectedOnEnter() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + let { clientWidth, clientHeight, scrollbarWidth, scrollbarHeight } = + await helper.getContentDimensions(); + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + let visibleButton = await helper.getPanelButton("#visible-page"); + visibleButton.focus(); + + await BrowserTestUtils.waitForCondition(() => { + return visibleButton.getRootNode().activeElement === visibleButton; + }, "The visible button in the panel should have focus"); + info( + `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${visibleButton.id}` + ); + is( + Services.focus.focusedElement, + visibleButton, + "The visible button in the panel should have focus" + ); + + EventUtils.synthesizeKey("ArrowLeft"); + + // Focus should move to the browser + let fullpageButton = await helper.getPanelButton("#full-page"); + await BrowserTestUtils.waitForCondition(() => { + return ( + fullpageButton.getRootNode().activeElement !== fullpageButton && + visibleButton.getRootNode().activeElement !== visibleButton + ); + }, "The visible and full page buttons do not have focus"); + Assert.notEqual( + Services.focus.focusedElement, + visibleButton, + "The visible button does not have focus" + ); + Assert.notEqual( + Services.focus.focusedElement, + fullpageButton, + "The full page button does not have focus" + ); + + let mouseEvent = BrowserTestUtils.waitForEvent(window, "mousemove"); + const windowMiddleX = + (window.innerWidth / 2 + window.mozInnerScreenX) * + window.devicePixelRatio; + const windowMiddleY = + (browser.clientHeight / 2) * window.devicePixelRatio; + const contentTop = + (window.mozInnerScreenY + (window.innerHeight - browser.clientHeight)) * + window.devicePixelRatio; + + window.windowUtils.sendNativeMouseEvent( + windowMiddleX, + windowMiddleY + contentTop, + window.windowUtils.NATIVE_MOUSE_MESSAGE_MOVE, + 0, + 0, + window.document.documentElement + ); + await mouseEvent; + + await helper.waitForContentMousePosition( + (clientWidth + scrollbarWidth) / 2, + (clientHeight + scrollbarHeight) / 2 + ); + + let x = {}; + let y = {}; + window.windowUtils.getLastOverWindowPointerLocationInCSSPixels(x, y); + let currentCursorX = x.value; + let currentCursorY = y.value; + + let rect = await helper.getTestPageElementRect(); + + info(JSON.stringify({ currentCursorX, currentCursorY })); + info(JSON.stringify(rect)); + + let repeatShiftLeft = Math.round((currentCursorX - rect.right) / 10); + await doKeyPress( + "ArrowLeft", + { shiftKey: true, repeat: repeatShiftLeft }, + window + ); + + let repeatLeft = (currentCursorX - rect.right) % 10; + await doKeyPress("ArrowLeft", { repeat: repeatLeft }, window); + + let repeatShiftRight = Math.round((currentCursorY - rect.bottom) / 10); + await doKeyPress( + "ArrowUp", + { shiftKey: true, repeat: repeatShiftRight }, + window + ); + + let repeatRight = (currentCursorY - rect.bottom) % 10; + await doKeyPress("ArrowUp", { repeat: repeatRight }, window); + + await helper.waitForHoverElementRect(rect.width, rect.height); + + EventUtils.synthesizeKey("Enter", {}); + await helper.waitForStateChange(["selected"]); + + 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" + ); + } + ); +}); + +add_task(async function test_createRegionWithKeyboard() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + await doKeyPress("ArrowRight", {}, window); + + let mouseEvent = BrowserTestUtils.waitForEvent(window, "mousemove"); + const window100X = + (100 + window.mozInnerScreenX) * window.devicePixelRatio; + const contentTop = + (window.mozInnerScreenY + (window.innerHeight - browser.clientHeight)) * + window.devicePixelRatio; + const window100Y = 100 * window.devicePixelRatio + contentTop; + + info(JSON.stringify({ window100X, window100Y })); + + window.windowUtils.sendNativeMouseEvent( + Math.floor(window100X), + Math.floor(window100Y), + window.windowUtils.NATIVE_MOUSE_MESSAGE_MOVE, + 0, + 0, + window.document.documentElement + ); + await mouseEvent; + + await helper.waitForContentMousePosition(100, 100); + + EventUtils.synthesizeKey(" "); + await helper.waitForStateChange(["dragging"]); + + let lastX = 100; + let lastY = 100; + for (let [key, expectedDimensions] of KEY_TO_EXPECTED_POSITION_ARRAY) { + await doKeyPress(key, { repeat: 10 }, window); + if (key.includes("Left")) { + lastX = expectedDimensions.left; + } else if (key.includes("Right")) { + lastX = expectedDimensions.right; + } else if (key.includes("Down")) { + lastY = expectedDimensions.bottom; + } else if (key.includes("Up")) { + lastY = expectedDimensions.top; + } + await TestUtils.waitForTick(); + await helper.waitForContentMousePosition(lastX, lastY); + let actualDimensions = await helper.getSelectionRegionDimensions(); + info(`Key: ${key}`); + info(`Actual dimensions: ${JSON.stringify(actualDimensions, null, 2)}`); + info( + `Expected dimensions: ${JSON.stringify(expectedDimensions, null, 2)}` + ); + assertSelectionRegionDimensions(actualDimensions, expectedDimensions); + } + + await doKeyPress("ArrowRight", { repeat: 10 }, window); + await doKeyPress("ArrowDown", { repeat: 10 }, window); + await helper.waitForContentMousePosition(110, 110); + + EventUtils.synthesizeKey(" "); + await helper.waitForStateChange(["selected"]); + + let region = await helper.getSelectionRegionDimensions(); + + is(Math.round(region.left), 100, "The selected region left is 100"); + is(Math.round(region.right), 110, "The selected region right is 110"); + is(Math.round(region.top), 100, "The selected region top is 100"); + is(Math.round(region.bottom), 110, "The selected region bottom is 110"); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlayClosed(); + } + ); +}); + +add_task(async function test_createRegionWithKeyboardWithShift() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + await doKeyPress("ArrowRight", {}, window); + + let mouseEvent = BrowserTestUtils.waitForEvent(window, "mousemove"); + const window100X = + (100 + window.mozInnerScreenX) * window.devicePixelRatio; + const contentTop = + (window.mozInnerScreenY + (window.innerHeight - browser.clientHeight)) * + window.devicePixelRatio; + const window100Y = 100 * window.devicePixelRatio + contentTop; + + info(JSON.stringify({ window100X, window100Y })); + + window.windowUtils.sendNativeMouseEvent( + window100X, + window100Y, + window.windowUtils.NATIVE_MOUSE_MESSAGE_MOVE, + 0, + 0, + window.document.documentElement + ); + await mouseEvent; + + await helper.waitForContentMousePosition(100, 100); + + EventUtils.synthesizeKey(" "); + await helper.waitForStateChange(["dragging"]); + + let lastX = 100; + let lastY = 100; + for (let [ + key, + expectedDimensions, + ] of SHIFT_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY) { + await doKeyPress(key, { shiftKey: true, repeat: 10 }, window); + if (key.includes("Left")) { + lastX = expectedDimensions.left; + } else if (key.includes("Right")) { + lastX = expectedDimensions.right; + } else if (key.includes("Down")) { + lastY = expectedDimensions.bottom; + } else if (key.includes("Up")) { + lastY = expectedDimensions.top; + } + await TestUtils.waitForTick(); + await helper.waitForContentMousePosition(lastX, lastY); + let actualDimensions = await helper.getSelectionRegionDimensions(); + info(`Key: ${key}`); + info(`Actual dimensions: ${JSON.stringify(actualDimensions, null, 2)}`); + info( + `Expected dimensions: ${JSON.stringify(expectedDimensions, null, 2)}` + ); + assertSelectionRegionDimensions(actualDimensions, expectedDimensions); + } + + await doKeyPress("ArrowRight", { shiftKey: true, repeat: 10 }, window); + await doKeyPress("ArrowDown", { shiftKey: true, repeat: 10 }, window); + await helper.waitForContentMousePosition(200, 200); + + EventUtils.synthesizeKey(" "); + await helper.waitForStateChange(["selected"]); + + let region = await helper.getSelectionRegionDimensions(); + + is(Math.round(region.left), 100, "The selected region left is 100"); + is(Math.round(region.right), 200, "The selected region right is 200"); + is(Math.round(region.top), 100, "The selected region top is 100"); + is(Math.round(region.bottom), 200, "The selected region bottom is 200"); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlayClosed(); + } + ); +}); diff --git a/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js b/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js index 592587a67d..71b93b5c06 100644 --- a/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js +++ b/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js @@ -136,9 +136,6 @@ const SHIFT_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY = [ ], ]; -/** - * - */ add_task(async function test_moveRegionWithKeyboard() { await BrowserTestUtils.withNewTab( { diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_download_filenames.js b/browser/components/screenshots/tests/browser/browser_screenshots_download_filenames.js new file mode 100644 index 0000000000..f68348835b --- /dev/null +++ b/browser/components/screenshots/tests/browser/browser_screenshots_download_filenames.js @@ -0,0 +1,67 @@ +const { getFilename, getDownloadDirectory, MAX_PATHNAME } = + ChromeUtils.importESModule( + "chrome://browser/content/screenshots/fileHelpers.mjs" + ); + +function getStringSize(filename) { + return new Blob([filename]).size; +} + +add_task(async function filename_exceeds_max_length() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let documentTitle = + "And the beast shall come forth surrounded by a roiling cloud of vengeance. The house of the unbelievers shall be razed and they shall be scorched to the earth. Their tags shall blink until the end of days. And the beast shall be made legion. Its numbers shall be increased a thousand thousand fold. The din of a million keyboards like unto a great storm shall cover the earth, and the followers of Mammon shall tremble. And so at last the beast fell and the unbelievers rejoiced. But all was not lost, for from the ash rose a great bird. The bird gazed down upon the unbelievers and cast fire and thunder upon them. For the beast had been reborn with its strength renewed, and the followers of Mammon cowered in horror. And thus the Creator looked upon the beast reborn and saw that it was good. Mammon slept. And the beast reborn spread over the earth and its numbers grew legion. And they proclaimed the times and sacrificed crops unto the fire, with the cunning of foxes. And they built a new world in their own image as promised by the sacred words, and spoke of the beast with their children. Mammon awoke, and lo! it was naught but a follower. The twins of Mammon quarrelled. Their warring plunged the world into a new darkness, and the beast abhorred the darkness. So it began to move swiftly, and grew more powerful, and went forth and multiplied. And the beasts brought fire and light to the darkness."; + Assert.greater( + getStringSize(documentTitle), + MAX_PATHNAME, + "The input title is longer than our MAX_PATHNAME" + ); + let result = await getFilename(documentTitle, browser); + Assert.greaterOrEqual( + MAX_PATHNAME, + getStringSize(result), + "The output pathname is not longer than MAX_PATHNAME" + ); + } + ); +}); + +add_task(async function filename_has_doublebyte_chars() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let downloadDir = await getDownloadDirectory(); + info( + `downloadDir: ${downloadDir}, length: ${getStringSize(downloadDir)}` + ); + + let documentTitle = + "Many fruits: " + "🍇🍈🍉🍊🍋🍌🍍🥭🍎🍏🍐🍑🍒🍓🫐".repeat(20); + Assert.greater( + getStringSize(documentTitle), + documentTitle.length, + "String length underestimates the needed filename length" + ); + Assert.greater( + getStringSize(documentTitle), + MAX_PATHNAME, + "The input title is longer than our MAX_PATHNAME" + ); + + let result = await getFilename(documentTitle, browser); + Assert.greaterOrEqual( + MAX_PATHNAME, + getStringSize(result), + "The output pathname is not longer than MAX_PATHNAME" + ); + } + ); +}); diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js b/browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js index 367f62205e..c8e3142c60 100644 --- a/browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js +++ b/browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js @@ -46,7 +46,7 @@ async function restoreFocusOnEscape(initialFocusElem, helper) { ); EventUtils.synthesizeKey("s", { shiftKey: true, accelKey: true }); - let button = await helper.getPanelButton(".visible-page"); + 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}` @@ -88,7 +88,7 @@ add_task(async function testPanelFocused() { info("Opening Screenshots and waiting for the panel"); helper.triggerUIFromToolbar(); - let button = await helper.getPanelButton(".visible-page"); + 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}` @@ -215,7 +215,7 @@ add_task(async function test_focusLastUsedMethod() { helper.triggerUIFromToolbar(); await helper.waitForOverlay(); - let expectedFocusedButton = await helper.getPanelButton(".visible-page"); + let expectedFocusedButton = await helper.getPanelButton("#visible-page"); await BrowserTestUtils.waitForCondition(() => { return ( @@ -233,17 +233,16 @@ add_task(async function test_focusLastUsedMethod() { let screenshotReady = TestUtils.topicObserved( "screenshots-preview-ready" ); - let fullpageButton = await helper.getPanelButton(".full-page"); + let fullpageButton = await helper.getPanelButton("#full-page"); fullpageButton.click(); await screenshotReady; - let dialog = helper.getDialog(); - let retryButton = dialog._frame.contentDocument.getElementById("retry"); + let retryButton = helper.getDialogButton("retry"); retryButton.click(); await helper.waitForOverlay(); - expectedFocusedButton = await helper.getPanelButton(".full-page"); + expectedFocusedButton = await helper.getPanelButton("#full-page"); await BrowserTestUtils.waitForCondition(() => { return ( @@ -259,17 +258,16 @@ add_task(async function test_focusLastUsedMethod() { ); screenshotReady = TestUtils.topicObserved("screenshots-preview-ready"); - let visiblepageButton = await helper.getPanelButton(".visible-page"); + let visiblepageButton = await helper.getPanelButton("#visible-page"); visiblepageButton.click(); await screenshotReady; - dialog = helper.getDialog(); - retryButton = dialog._frame.contentDocument.getElementById("retry"); + retryButton = helper.getDialogButton("retry"); retryButton.click(); await helper.waitForOverlay(); - expectedFocusedButton = await helper.getPanelButton(".visible-page"); + expectedFocusedButton = await helper.getPanelButton("#visible-page"); await BrowserTestUtils.waitForCondition(() => { return ( @@ -288,10 +286,7 @@ add_task(async function test_focusLastUsedMethod() { expectedFocusedButton.click(); await screenshotReady; - dialog = helper.getDialog(); - - expectedFocusedButton = - dialog._frame.contentDocument.getElementById("download"); + expectedFocusedButton = helper.getDialogButton("download"); await BrowserTestUtils.waitForCondition(() => { return ( @@ -302,28 +297,25 @@ add_task(async function test_focusLastUsedMethod() { is( Services.focus.focusedElement, - expectedFocusedButton, + expectedFocusedButton.buttonEl, "The download button in the preview dialog should have focus" ); let screenshotExit = TestUtils.topicObserved("screenshots-exit"); - let copyButton = dialog._frame.contentDocument.getElementById("copy"); + let copyButton = helper.getDialogButton("copy"); copyButton.click(); await screenshotExit; helper.triggerUIFromToolbar(); await helper.waitForOverlay(); - let visibleButton = await helper.getPanelButton(".visible-page"); + 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"); + expectedFocusedButton = helper.getDialogButton("copy"); await BrowserTestUtils.waitForCondition(() => { return ( @@ -334,13 +326,12 @@ add_task(async function test_focusLastUsedMethod() { is( Services.focus.focusedElement, - expectedFocusedButton, + expectedFocusedButton.buttonEl, "The copy button in the preview dialog should have focus" ); screenshotExit = TestUtils.topicObserved("screenshots-exit"); - let downloadButton = - dialog._frame.contentDocument.getElementById("download"); + let downloadButton = helper.getDialogButton("download"); downloadButton.click(); await Promise.all([screenshotExit, downloadFinishedPromise]); @@ -350,16 +341,13 @@ add_task(async function test_focusLastUsedMethod() { helper.triggerUIFromToolbar(); await helper.waitForOverlay(); - visibleButton = await helper.getPanelButton(".visible-page"); + 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"); + expectedFocusedButton = helper.getDialogButton("download"); await BrowserTestUtils.waitForCondition(() => { return ( @@ -370,7 +358,7 @@ add_task(async function test_focusLastUsedMethod() { is( Services.focus.focusedElement, - expectedFocusedButton, + expectedFocusedButton.buttonEl, "The download button in the preview dialog should have focus" ); @@ -382,3 +370,89 @@ add_task(async function test_focusLastUsedMethod() { await SpecialPowers.popPrefEnv(); }); + +add_task(async function testFocusedIsLocked() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + let panel = await helper.waitForPanel(); + let mozButtonGroup = panel + .querySelector("screenshots-buttons") + .shadowRoot.querySelector("moz-button-group"); + let firstButton = mozButtonGroup.firstElementChild; + let lastButton = mozButtonGroup.lastElementChild; + + firstButton.focus(); + + await BrowserTestUtils.waitForCondition(() => { + return firstButton.getRootNode().activeElement === firstButton; + }, "The first button in the panel should have focus"); + info( + `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${firstButton.id}` + ); + is( + Services.focus.focusedElement, + firstButton, + "The first button in the panel should have focus" + ); + + EventUtils.synthesizeKey("KEY_Tab"); + + await BrowserTestUtils.waitForCondition(() => { + return lastButton.getRootNode().activeElement === lastButton; + }, "The last button in the panel should have focus"); + info( + `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${lastButton.id}` + ); + is( + Services.focus.focusedElement, + lastButton, + "The last button in the panel should have focus" + ); + + EventUtils.synthesizeKey("KEY_Tab"); + + // Focus should move to the content document + await BrowserTestUtils.waitForCondition(() => { + return ( + firstButton.getRootNode().activeElement !== firstButton && + lastButton.getRootNode().activeElement !== lastButton + ); + }, "The first and last buttons do not have focus"); + Assert.notEqual( + Services.focus.focusedElement, + firstButton, + "The first button does not have focus" + ); + Assert.notEqual( + Services.focus.focusedElement, + lastButton, + "The last button does not have focus" + ); + + EventUtils.synthesizeKey("KEY_Tab"); + + info( + `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${firstButton.id}` + ); + await BrowserTestUtils.waitForCondition(() => { + return firstButton.getRootNode().activeElement === firstButton; + }, "The first button in the panel should have focus"); + info( + `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${firstButton.id}` + ); + is( + Services.focus.focusedElement, + firstButton, + "The first button in the panel should have focus" + ); + } + ); +}); diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js b/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js index 782ffa3fd3..eabb1ee152 100644 --- a/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js +++ b/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js @@ -82,11 +82,7 @@ const EXTRA_EVENTS = [ 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], - ], + set: [["browser.urlbar.secondaryActions.featureGate", true]], }); await BrowserTestUtils.withNewTab( @@ -99,22 +95,27 @@ add_task(async function test_started_and_canceled_events() { let helper = new ScreenshotsHelper(browser); let screenshotExit; + info("Open screenshots via toolbar button"); helper.triggerUIFromToolbar(); await helper.waitForOverlay(); screenshotExit = TestUtils.topicObserved("screenshots-exit"); + info("Close screenshots via toolbar button"); helper.triggerUIFromToolbar(); await helper.waitForOverlayClosed(); await screenshotExit; + info("Open screenshots via keyboard shortcut"); EventUtils.synthesizeKey("s", { shiftKey: true, accelKey: true }); await helper.waitForOverlay(); screenshotExit = TestUtils.topicObserved("screenshots-exit"); + info("Close screenshots via keyboard shortcut"); EventUtils.synthesizeKey("s", { shiftKey: true, accelKey: true }); await helper.waitForOverlayClosed(); await screenshotExit; + info("Open screenshots via context menu"); let contextMenu = document.getElementById("contentAreaContextMenu"); let popupShownPromise = BrowserTestUtils.waitForEvent( contextMenu, @@ -152,23 +153,21 @@ add_task(async function test_started_and_canceled_events() { await popupShownPromise; screenshotExit = TestUtils.topicObserved("screenshots-exit"); + info("Close screenshots via context menu"); contextMenu.activateItem( contextMenu.querySelector("#context-take-screenshot") ); await helper.waitForOverlayClosed(); await screenshotExit; + info("Open screenshots via quickactions"); 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_Tab", {}, window); EventUtils.synthesizeKey("KEY_Enter", {}, window); await helper.waitForOverlay(); @@ -177,13 +176,10 @@ add_task(async function test_started_and_canceled_events() { 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"); + info("Close screenshots via quickactions"); screenshotExit = TestUtils.topicObserved("screenshots-exit"); - EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Tab", {}, window); EventUtils.synthesizeKey("KEY_Enter", {}, window); await helper.waitForOverlayClosed(); await screenshotExit; @@ -215,12 +211,11 @@ add_task(async function test_started_retry() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .shadowRoot.querySelector("#visible-page"); visiblePageButton.click(); await screenshotReady; - let dialog = helper.getDialog(); - let retryButton = dialog._frame.contentDocument.getElementById("retry"); + let retryButton = helper.getDialogButton("retry"); ok(retryButton, "Got the retry button"); retryButton.click(); @@ -253,12 +248,11 @@ add_task(async function test_canceled() { // click the full page button in panel let fullPageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".full-page"); + .shadowRoot.querySelector("#full-page"); fullPageButton.click(); await screenshotReady; - let dialog = helper.getDialog(); - let cancelButton = dialog._frame.contentDocument.getElementById("cancel"); + let cancelButton = helper.getDialogButton("cancel"); ok(cancelButton, "Got the cancel button"); let screenshotExit = TestUtils.topicObserved("screenshots-exit"); @@ -315,13 +309,12 @@ add_task(async function test_copy() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .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 copyButton = helper.getDialogButton("copy"); let screenshotExit = TestUtils.topicObserved("screenshots-exit"); let clipboardChanged = helper.waitForRawClipboardChange( @@ -426,13 +419,12 @@ add_task(async function test_extra_telemetry() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .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"); + let retryButton = helper.getDialogButton("retry"); retryButton.click(); info("waiting for panel"); @@ -443,14 +435,13 @@ add_task(async function test_extra_telemetry() { // click the full page button in panel let fullPageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".full-page"); + .shadowRoot.querySelector("#full-page"); fullPageButton.click(); await screenshotReady; let screenshotExit = TestUtils.topicObserved("screenshots-exit"); - dialog = helper.getDialog(); - let copyButton = dialog._frame.contentDocument.getElementById("copy"); + let copyButton = helper.getDialogButton("copy"); retryButton.click(); // click copy button on dialog box info("clicking the copy button"); diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js index 51d5b858b9..fce0843d33 100644 --- a/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js +++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js @@ -46,6 +46,18 @@ function waitForFilePicker() { MockFilePicker.showCallback = () => { MockFilePicker.showCallback = null; ok(true, "Saw the file picker"); + + resolve(); + }; + }); +} + +function waitForFilePickerCancel() { + return new Promise(resolve => { + MockFilePicker.showCallback = () => { + MockFilePicker.showCallback = null; + ok(true, "Saw the file picker"); + MockFilePicker.returnValue = MockFilePicker.returnCancel; resolve(); }; }); @@ -109,15 +121,12 @@ add_task(async function test_download_without_filepicker() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .shadowRoot.querySelector("#visible-page"); visiblePageButton.click(); - let dialog = helper.getDialog(); - await screenshotReady; - let downloadButton = - dialog._frame.contentDocument.getElementById("download"); + let downloadButton = helper.getDialogButton("download"); ok(downloadButton, "Got the download button"); let screenshotExit = TestUtils.topicObserved("screenshots-exit"); @@ -184,3 +193,50 @@ add_task(async function test_download_with_filepicker() { } ); }); + +add_task(async function test_download_filepicker_canceled() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.download.useDownloadDir", false]], + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + let screenshotReady = TestUtils.topicObserved( + "screenshots-preview-ready" + ); + + let visiblepageButton = await helper.getPanelButton("#visible-page"); + visiblepageButton.click(); + await screenshotReady; + + let downloadButton = helper.getDialogButton("download"); + + let filePickerCanceled = waitForFilePickerCancel(); + downloadButton.click(); + info("download button clicked"); + await filePickerCanceled; + + let cancelButton = helper.getDialogButton("cancel"); + + await BrowserTestUtils.waitForMutationCondition( + cancelButton, + { attributes: true }, + () => !cancelButton.disabled + ); + + let screenshotExit = TestUtils.topicObserved("screenshots-exit"); + cancelButton.click(); + + await screenshotExit; + } + ); +}); 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 index 006a9819ed..10828eceec 100644 --- a/browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js +++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js @@ -39,16 +39,14 @@ add_task(async function test_fullpageScreenshot() { ); // click the full page button in panel - let visiblePage = panel + let fullpageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".full-page"); - visiblePage.click(); - - let dialog = helper.getDialog(); + .shadowRoot.querySelector("#full-page"); + fullpageButton.click(); await screenshotReady; - let copyButton = dialog._frame.contentDocument.getElementById("copy"); + let copyButton = helper.getDialogButton("copy"); ok(copyButton, "Got the copy button"); let clipboardChanged = helper.waitForRawClipboardChange( @@ -136,16 +134,14 @@ add_task(async function test_fullpageScreenshotScrolled() { ); // click the full page button in panel - let visiblePage = panel + let fullpageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".full-page"); - visiblePage.click(); - - let dialog = helper.getDialog(); + .shadowRoot.querySelector("#full-page"); + fullpageButton.click(); await screenshotReady; - let copyButton = dialog._frame.contentDocument.getElementById("copy"); + let copyButton = helper.getDialogButton("copy"); ok(copyButton, "Got the copy button"); let clipboardChanged = helper.waitForRawClipboardChange( @@ -198,3 +194,80 @@ add_task(async function test_fullpageScreenshotScrolled() { } ); }); + +add_task(async function test_fullpageScreenshotRTL() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: RTL_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 fullpageButton = panel + .querySelector("screenshots-buttons") + .shadowRoot.querySelector("#full-page"); + fullpageButton.click(); + + await screenshotReady; + + let copyButton = helper.getDialogButton("copy"); + ok(copyButton, "Got the copy button"); + + info("contentInfo: " + JSON.stringify(contentInfo, null, 2)); + info( + "expecting: " + + JSON.stringify({ expectedWidth, expectedHeight }, null, 2) + ); + 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"); + + assertPixel(result.color.topLeft, [255, 255, 255], "Top left pixel"); + assertPixel(result.color.topRight, [255, 255, 255], "Top right pixel"); + assertPixel( + result.color.bottomLeft, + [255, 255, 255], + "Bottom left pixel" + ); + assertPixel( + result.color.bottomRight, + [255, 255, 255], + "Bottom right pixel" + ); + } + ); +}); diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js index c53b44d5ea..2cc4eb241a 100644 --- a/browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js +++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js @@ -45,14 +45,12 @@ add_task(async function test_visibleScreenshot() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .shadowRoot.querySelector("#visible-page"); visiblePageButton.click(); - let dialog = helper.getDialog(); - await screenshotReady; - let copyButton = dialog._frame.contentDocument.getElementById("copy"); + let copyButton = helper.getDialogButton("copy"); ok(copyButton, "Got the copy button"); let clipboardChanged = helper.waitForRawClipboardChange( @@ -144,14 +142,12 @@ add_task(async function test_visibleScreenshotScrolledY() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .shadowRoot.querySelector("#visible-page"); visiblePageButton.click(); - let dialog = helper.getDialog(); - await screenshotReady; - let copyButton = dialog._frame.contentDocument.getElementById("copy"); + let copyButton = helper.getDialogButton("copy"); ok(copyButton, "Got the copy button"); let clipboardChanged = helper.waitForRawClipboardChange( @@ -243,14 +239,12 @@ add_task(async function test_visibleScreenshotScrolledX() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .shadowRoot.querySelector("#visible-page"); visiblePageButton.click(); - let dialog = helper.getDialog(); - await screenshotReady; - let copyButton = dialog._frame.contentDocument.getElementById("copy"); + let copyButton = helper.getDialogButton("copy"); ok(copyButton, "Got the copy button"); let clipboardChanged = helper.waitForRawClipboardChange( @@ -342,14 +336,12 @@ add_task(async function test_visibleScreenshotScrolledXAndY() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .shadowRoot.querySelector("#visible-page"); visiblePageButton.click(); - let dialog = helper.getDialog(); - await screenshotReady; - let copyButton = dialog._frame.contentDocument.getElementById("copy"); + let copyButton = helper.getDialogButton("copy"); ok(copyButton, "Got the copy button"); let clipboardChanged = helper.waitForRawClipboardChange( @@ -402,3 +394,84 @@ add_task(async function test_visibleScreenshotScrolledXAndY() { } ); }); + +add_task(async function test_visibleScreenshotRTL() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: RTL_TEST_PAGE, + }, + async browser => { + await SpecialPowers.spawn(browser, [], () => { + content.scrollTo(-1000, 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(); + + 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("#visible-page"); + visiblePage.click(); + + await screenshotReady; + + let copyButton = helper.getDialogButton("copy"); + ok(copyButton, "Got the copy button"); + + info("contentInfo: " + JSON.stringify(contentInfo, null, 2)); + info( + "expecting: " + + JSON.stringify({ expectedWidth, expectedHeight }, null, 2) + ); + 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"); + + assertPixel(result.color.topLeft, [255, 255, 255], "Top left pixel"); + assertPixel(result.color.topRight, [255, 255, 255], "Top right pixel"); + assertPixel( + result.color.bottomLeft, + [255, 255, 255], + "Bottom left pixel" + ); + assertPixel( + result.color.bottomRight, + [255, 255, 255], + "Bottom right pixel" + ); + } + ); +}); diff --git a/browser/components/screenshots/tests/browser/browser_test_element_picker.js b/browser/components/screenshots/tests/browser/browser_test_element_picker.js index 3e2069134e..a24149d15e 100644 --- a/browser/components/screenshots/tests/browser/browser_test_element_picker.js +++ b/browser/components/screenshots/tests/browser/browser_test_element_picker.js @@ -10,7 +10,6 @@ add_task(async function test_element_picker() { url: TEST_PAGE, }, async browser => { - await clearAllTelemetryEvents(); let helper = new ScreenshotsHelper(browser); helper.triggerUIFromToolbar(); @@ -54,3 +53,37 @@ add_task(async function test_element_picker() { } ); }); + +add_task(async function test_element_pickerRTL() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: RTL_TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + await helper.clickTestPageElement(); + + let rect = await helper.getTestPageElementRect(); + let region = await helper.getSelectionRegionDimensions(); + + info(`element rect: ${JSON.stringify(rect, null, 2)}`); + info(`selected region: ${JSON.stringify(region, null, 2)}`); + + is( + region.width, + rect.width, + "The selected region width is the same as the element width" + ); + is( + region.height, + rect.height, + "The selected region height is the same as the element height" + ); + } + ); +}); diff --git a/browser/components/screenshots/tests/browser/browser_test_resize.js b/browser/components/screenshots/tests/browser/browser_test_resize.js index b249a346d6..a848a2ac66 100644 --- a/browser/components/screenshots/tests/browser/browser_test_resize.js +++ b/browser/components/screenshots/tests/browser/browser_test_resize.js @@ -12,6 +12,8 @@ add_task(async function test_window_resize() { url: RESIZE_TEST_PAGE, }, async browser => { + await new Promise(r => window.requestAnimationFrame(r)); + let helper = new ScreenshotsHelper(browser); await helper.resizeContentWindow(windowWidth, window.outerHeight); const originalContentDimensions = await helper.getContentDimensions(); @@ -61,6 +63,8 @@ add_task(async function test_window_resize_vertical_writing_mode() { content.document.documentElement.style = "writing-mode: vertical-lr;"; }); + await new Promise(r => window.requestAnimationFrame(r)); + let helper = new ScreenshotsHelper(browser); await helper.resizeContentWindow(windowWidth, window.outerHeight); const originalContentDimensions = await helper.getContentDimensions(); diff --git a/browser/components/screenshots/tests/browser/browser_test_selection_size_text.js b/browser/components/screenshots/tests/browser/browser_test_selection_size_text.js index 38d1acbea9..bfe3b884e0 100644 --- a/browser/components/screenshots/tests/browser/browser_test_selection_size_text.js +++ b/browser/components/screenshots/tests/browser/browser_test_selection_size_text.js @@ -22,7 +22,7 @@ add_task(async function test_selectionSizeTest() { Assert.equal( actualText, - `${400 * dpr} x ${400 * dpr}`, + `${400 * dpr} × ${400 * dpr}`, "The selection size text is the same" ); } @@ -50,7 +50,7 @@ add_task(async function test_selectionSizeTestAt1Point5Zoom() { Assert.equal( actualText, - `${400 * dpr * zoom} x ${400 * dpr * zoom}`, + `${400 * dpr * zoom} × ${400 * dpr * zoom}`, "The selection size text is the same" ); } @@ -78,7 +78,7 @@ add_task(async function test_selectionSizeTestAtPoint5Zoom() { Assert.equal( actualText, - `${400 * dpr * zoom} x ${400 * dpr * zoom}`, + `${400 * dpr * zoom} × ${400 * dpr * zoom}`, "The selection size text is the same" ); } diff --git a/browser/components/screenshots/tests/browser/browser_text_selectionAPI_test.js b/browser/components/screenshots/tests/browser/browser_text_selectionAPI_test.js new file mode 100644 index 0000000000..78764d3847 --- /dev/null +++ b/browser/components/screenshots/tests/browser/browser_text_selectionAPI_test.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_textSelectedDuringScreenshot() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: SELECTION_TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + let rect = await helper.getTestPageElementRect("selection"); + + await ContentTask.spawn(browser, [], async () => { + let selection = content.window.getSelection(); + let elToSelect = content.document.getElementById("selection"); + + let range = content.document.createRange(); + range.selectNode(elToSelect); + selection.addRange(range); + }); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + await helper.clickTestPageElement("selection"); + + let clipboardChanged = helper.waitForRawClipboardChange( + Math.round(rect.width), + Math.round(rect.height), + { allPixels: true } + ); + + await helper.clickCopyButton(); + + info("Waiting for clipboard change"); + let result = await clipboardChanged; + let allPixels = result.allPixels; + info(`${typeof allPixels}, ${allPixels.length}`); + + let sumOfPixels = Object.values(allPixels).reduce( + (accumulator, currentVal) => accumulator + currentVal, + 0 + ); + + Assert.less( + sumOfPixels, + allPixels.length * 255, + "Sum of pixels is less than all white pixels" + ); + } + ); +}); diff --git a/browser/components/screenshots/tests/browser/head.js b/browser/components/screenshots/tests/browser/head.js index 762da5f866..483e67fa34 100644 --- a/browser/components/screenshots/tests/browser/head.js +++ b/browser/components/screenshots/tests/browser/head.js @@ -20,6 +20,8 @@ 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 SELECTION_TEST_PAGE = TEST_ROOT + "test-selectionAPI-page.html"; +const RTL_TEST_PAGE = TEST_ROOT + "rtl-test-page.html"; const { MAX_CAPTURE_DIMENSION, MAX_CAPTURE_AREA } = ChromeUtils.importESModule( "resource:///modules/ScreenshotsUtils.sys.mjs" @@ -27,8 +29,8 @@ const { MAX_CAPTURE_DIMENSION, MAX_CAPTURE_AREA } = ChromeUtils.importESModule( const gScreenshotUISelectors = { panel: "#screenshotsPagePanel", - fullPageButton: "button.full-page", - visiblePageButton: "button.visible-page", + fullPageButton: "button#full-page", + visiblePageButton: "button#visible-page", copyButton: "button.#copy", }; @@ -96,6 +98,31 @@ class ScreenshotsHelper { return button; } + /** + * Get the button from screenshots preview dialog + * @param {Sting} name The id of the button to query + * @returns The button or null + */ + getDialogButton(name) { + let dialog = this.getDialog(); + let screenshotsPreviewEl = dialog._frame.contentDocument.querySelector( + "screenshots-preview" + ); + + switch (name) { + case "retry": + return screenshotsPreviewEl.retryButtonEl; + case "cancel": + return screenshotsPreviewEl.cancelButtonEl; + case "copy": + return screenshotsPreviewEl.copyButtonEl; + case "download": + return screenshotsPreviewEl.downloadButtonEl; + } + + return null; + } + async waitForPanel() { let panel = this.panel; await BrowserTestUtils.waitForCondition(async () => { @@ -115,6 +142,8 @@ class ScreenshotsHelper { let init = await this.isOverlayInitialized(); return init; }); + + await new Promise(r => window.requestAnimationFrame(r)); info("Overlay is visible"); } @@ -147,6 +176,8 @@ class ScreenshotsHelper { info("Is overlay initialized: " + !init); return init; }); + + await new Promise(r => window.requestAnimationFrame(r)); info("Overlay is not visible"); } @@ -213,7 +244,11 @@ class ScreenshotsHelper { let dimensions; await ContentTaskUtils.waitForCondition(() => { dimensions = screenshotsChild.overlay.hoverElementRegion.dimensions; - return dimensions.width === width && dimensions.height === height; + if (dimensions.width === width && dimensions.height === height) { + return true; + } + info(`Got: ${JSON.stringify(dimensions)}`); + return false; }, "The hover element region is the expected width and height"); return dimensions; } @@ -420,6 +455,32 @@ class ScreenshotsHelper { ); } + waitForContentMousePosition(left, top) { + return ContentTask.spawn(this.browser, [left, top], async ([x, y]) => { + function isCloseEnough(a, b, diff) { + return Math.abs(a - b) <= diff; + } + + let cursorX = {}; + let cursorY = {}; + + await ContentTaskUtils.waitForCondition(() => { + content.window.windowUtils.getLastOverWindowPointerLocationInCSSPixels( + cursorX, + cursorY + ); + if ( + isCloseEnough(cursorX.value, x, 1) && + isCloseEnough(cursorY.value, y, 1) + ) { + return true; + } + info(`Got: ${JSON.stringify({ cursorX, cursorY, x, y })}`); + return false; + }, `Wait for cursor to be ${x}, ${y}`); + }); + } + async clickDownloadButton() { let { centerX: x, centerY: y } = await ContentTask.spawn( this.browser, @@ -580,7 +641,7 @@ class ScreenshotsHelper { * Returns a promise that resolves when the clipboard data has changed * Otherwise rejects */ - waitForRawClipboardChange(epectedWidth, expectedHeight) { + waitForRawClipboardChange(epectedWidth, expectedHeight, options = {}) { const initialClipboardData = Date.now().toString(); SpecialPowers.clipboardCopyString(initialClipboardData); @@ -588,7 +649,7 @@ class ScreenshotsHelper { async () => { let data; try { - data = await this.getImageSizeAndColorFromClipboard(); + data = await this.getImageSizeAndColorFromClipboard(options); } catch (e) { console.log("Failed to get image/png clipboard data:", e); return false; @@ -601,6 +662,7 @@ class ScreenshotsHelper { ) { return data; } + info(`Got from clipboard: ${JSON.stringify(data, null, 2)}`); return false; }, "Waiting for screenshot to copy to clipboard", @@ -627,9 +689,14 @@ class ScreenshotsHelper { scrollMaxY, scrollX, scrollY, + scrollMinX, + scrollMinY, } = content.window; - let width = innerWidth + scrollMaxX; - let height = innerHeight + scrollMaxY; + + let scrollWidth = innerWidth + scrollMaxX - scrollMinX; + let scrollHeight = innerHeight + scrollMaxY - scrollMinY; + let clientHeight = innerHeight; + let clientWidth = innerWidth; const scrollbarHeight = {}; const scrollbarWidth = {}; @@ -638,18 +705,22 @@ class ScreenshotsHelper { scrollbarWidth, scrollbarHeight ); - width -= scrollbarWidth.value; - height -= scrollbarHeight.value; - innerWidth -= scrollbarWidth.value; - innerHeight -= scrollbarHeight.value; + scrollWidth -= scrollbarWidth.value; + scrollHeight -= scrollbarHeight.value; + clientWidth -= scrollbarWidth.value; + clientHeight -= scrollbarHeight.value; return { - clientHeight: innerHeight, - clientWidth: innerWidth, - scrollHeight: height, - scrollWidth: width, + clientWidth, + clientHeight, + scrollWidth, + scrollHeight, scrollX, scrollY, + scrollbarWidth: scrollbarWidth.value, + scrollbarHeight: scrollbarHeight.value, + scrollMinX, + scrollMinY, }; }); } @@ -756,7 +827,7 @@ class ScreenshotsHelper { * :screenshot command. * @return The {width, height, color} dimension and color object. */ - async getImageSizeAndColorFromClipboard() { + async getImageSizeAndColorFromClipboard(options = {}) { let flavor = "image/png"; let image = getRawClipboardData(flavor); if (!image) { @@ -796,8 +867,8 @@ class ScreenshotsHelper { // which could mess all sorts of things up return SpecialPowers.spawn( this.browser, - [buffer], - async function (_buffer) { + [buffer, options], + async function (_buffer, _options) { const img = content.document.createElement("img"); const loaded = new Promise(r => { img.addEventListener("load", r, { once: true }); @@ -830,6 +901,11 @@ class ScreenshotsHelper { 1 ); + let allPixels = null; + if (_options.allPixels) { + allPixels = context.getImageData(0, 0, img.width, img.height); + } + img.remove(); content.URL.revokeObjectURL(url); @@ -842,6 +918,7 @@ class ScreenshotsHelper { bottomLeft: bottomLeft.data, bottomRight: bottomRight.data, }, + allPixels: allPixels?.data, }; } ); diff --git a/browser/components/screenshots/tests/browser/rtl-test-page.html b/browser/components/screenshots/tests/browser/rtl-test-page.html new file mode 100644 index 0000000000..b76eab6f1c --- /dev/null +++ b/browser/components/screenshots/tests/browser/rtl-test-page.html @@ -0,0 +1,8 @@ +<html lang="en" dir="rtl"> +<head> + <title>Screenshots</title> +</head> +<body> + <div id="testPageElement" style="width:150vw;background-color: blue;">hello world</div> +</body> +</html> diff --git a/browser/components/screenshots/tests/browser/test-selectionAPI-page.html b/browser/components/screenshots/tests/browser/test-selectionAPI-page.html new file mode 100644 index 0000000000..17c29a3ccf --- /dev/null +++ b/browser/components/screenshots/tests/browser/test-selectionAPI-page.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Screenshots</title> +</head> +<body> + <p id="selection" style="color: white;width: fit-content;">hello world</p> +</body> +</html> |