From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../components/screenshots/tests/browser/head.js | 681 +++++++++++++++++++++ 1 file changed, 681 insertions(+) create mode 100644 browser/components/screenshots/tests/browser/head.js (limited to 'browser/components/screenshots/tests/browser/head.js') diff --git a/browser/components/screenshots/tests/browser/head.js b/browser/components/screenshots/tests/browser/head.js new file mode 100644 index 0000000000..5ab054a70c --- /dev/null +++ b/browser/components/screenshots/tests/browser/head.js @@ -0,0 +1,681 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); +const { UrlbarTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" +); + +const TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +const TEST_PAGE = TEST_ROOT + "test-page.html"; +const SHORT_TEST_PAGE = TEST_ROOT + "short-test-page.html"; +const LARGE_TEST_PAGE = TEST_ROOT + "large-test-page.html"; + +const MAX_CAPTURE_DIMENSION = 32767; +const MAX_CAPTURE_AREA = 124925329; + +const gScreenshotUISelectors = { + panelButtons: "#screenshotsPagePanel", + fullPageButton: "button.full-page", + visiblePageButton: "button.visible-page", + copyButton: "button.#copy", +}; + +// MouseEvents is for the mouse events on the Anonymous content +const MouseEvents = { + mouse: new Proxy( + {}, + { + get: (target, name) => + async function (x, y, selector = ":root") { + if (name === "click") { + this.down(x, y); + this.up(x, y); + } else { + await safeSynthesizeMouseEventInContentPage(selector, x, y, { + type: "mouse" + name, + }); + } + }, + } + ), +}; + +const { mouse } = MouseEvents; + +class ScreenshotsHelper { + constructor(browser) { + this.browser = browser; + this.selector = gScreenshotUISelectors; + } + + get toolbarButton() { + return document.getElementById("screenshot-button"); + } + + /** + * Click the screenshots button in the toolbar + */ + triggerUIFromToolbar() { + let button = this.toolbarButton; + ok( + BrowserTestUtils.is_visible(button), + "The screenshot toolbar button is visible" + ); + button.click(); + } + + async waitForPanel() { + let panel = this.browser.ownerDocument.querySelector( + "#screenshotsPagePanel" + ); + await BrowserTestUtils.waitForCondition(async () => { + if (!panel) { + panel = this.browser.ownerDocument.querySelector( + "#screenshotsPagePanel" + ); + } + return panel?.state === "open" && BrowserTestUtils.is_visible(panel); + }); + return panel; + } + + async waitForOverlay() { + const panel = await this.waitForPanel(); + ok(BrowserTestUtils.is_visible(panel), "Panel buttons are visible"); + + await BrowserTestUtils.waitForCondition(async () => { + let init = await this.isOverlayInitialized(); + return init; + }); + info("Overlay is visible"); + } + + async waitForOverlayClosed() { + let panel = this.browser.ownerDocument.querySelector( + "#screenshotsPagePanel" + ); + if (!panel) { + panel = await this.waitForPanel(); + } + await BrowserTestUtils.waitForMutationCondition( + panel, + { attributes: true }, + () => { + return BrowserTestUtils.is_hidden(panel); + } + ); + ok(BrowserTestUtils.is_hidden(panel), "Panel buttons are hidden"); + + await BrowserTestUtils.waitForCondition(async () => { + let init = !(await this.isOverlayInitialized()); + info("Is overlay initialized: " + !init); + return init; + }); + info("Overlay is not visible"); + } + + async isOverlayInitialized() { + return SpecialPowers.spawn(this.browser, [], () => { + let screenshotsChild = content.windowGlobalChild.getActor( + "ScreenshotsComponent" + ); + return screenshotsChild?._overlay?._initialized; + }); + } + + async getOverlayState() { + return ContentTask.spawn(this.browser, null, async () => { + let screenshotsChild = content.windowGlobalChild.getActor( + "ScreenshotsComponent" + ); + return screenshotsChild._overlay.stateHandler.getState(); + }); + } + + async waitForStateChange(newState) { + await BrowserTestUtils.waitForCondition(async () => { + let state = await this.getOverlayState(); + return state === newState; + }, `Waiting for state change to ${newState}`); + } + + async getHoverElementRect() { + return ContentTask.spawn(this.browser, null, async () => { + let screenshotsChild = content.windowGlobalChild.getActor( + "ScreenshotsComponent" + ); + return screenshotsChild._overlay.stateHandler.getHoverElementBoxRect(); + }); + } + + async waitForHoverElementRect() { + return TestUtils.waitForCondition(async () => { + let rect = await this.getHoverElementRect(); + return rect; + }); + } + + async waitForSelectionBoxSizeChange(currentWidth) { + await ContentTask.spawn( + this.browser, + [currentWidth], + async ([currWidth]) => { + let screenshotsChild = content.windowGlobalChild.getActor( + "ScreenshotsComponent" + ); + + let dimensions = + screenshotsChild._overlay.screenshotsContainer.getSelectionLayerDimensions(); + // return dimensions.boxWidth; + await ContentTaskUtils.waitForCondition(() => { + dimensions = + screenshotsChild._overlay.screenshotsContainer.getSelectionLayerDimensions(); + return dimensions.boxWidth !== currWidth; + }, "Wait for selection box width change"); + } + ); + } + + /** + * This will drag an overlay starting at the given startX and startY coordinates and ending + * at the given endX and endY coordinates. + * + * endY should be at least 70px from the bottom of window and endX should be at least + * 265px from the left of the window. If these requirements are not met then the + * overlay buttons (cancel, copy, download) will be positioned different from the default + * and the methods to click the overlay buttons will not work unless the updated + * position coordinates are supplied. + * See https://searchfox.org/mozilla-central/rev/af78418c4b5f2c8721d1a06486cf4cf0b33e1e8d/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs#1789,1798 + * for how the overlay buttons are positioned when the overlay rect is near the bottom or + * left edge of the window. + * + * Note: The distance of the rect should be greater than 40 to enter in the "dragging" state. + * See https://searchfox.org/mozilla-central/rev/af78418c4b5f2c8721d1a06486cf4cf0b33e1e8d/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs#809 + * @param {Number} startX The starting X coordinate. The left edge of the overlay rect. + * @param {Number} startY The starting Y coordinate. The top edge of the overlay rect. + * @param {Number} endX The end X coordinate. The right edge of the overlay rect. + * @param {Number} endY The end Y coordinate. The bottom edge of the overlay rect. + */ + async dragOverlay(startX, startY, endX, endY) { + await this.waitForStateChange("crosshairs"); + let state = await this.getOverlayState(); + Assert.equal(state, "crosshairs", "The overlay is in the crosshairs state"); + + mouse.down(startX, startY); + + await this.waitForStateChange("draggingReady"); + state = await this.getOverlayState(); + Assert.equal( + state, + "draggingReady", + "The overlay is in the draggingReady state" + ); + + mouse.move(endX, endY); + + await this.waitForStateChange("dragging"); + state = await this.getOverlayState(); + Assert.equal(state, "dragging", "The overlay is in the dragging state"); + + mouse.up(endX, endY); + + await this.waitForStateChange("selected"); + state = await this.getOverlayState(); + Assert.equal(state, "selected", "The overlay is in the selected state"); + + this.endX = endX; + this.endY = endY; + } + + async scrollContentWindow(x, y) { + let promise = BrowserTestUtils.waitForContentEvent(this.browser, "scroll"); + await ContentTask.spawn(this.browser, [x, y], async ([xPos, yPos]) => { + content.window.scroll(xPos, yPos); + + await ContentTaskUtils.waitForCondition(() => { + return ( + content.window.scrollX === xPos && content.window.scrollY === yPos + ); + }, `Waiting for window to scroll to ${xPos}, ${yPos}`); + }); + await promise; + } + + getWindowPosition() { + return ContentTask.spawn(this.browser, [], () => { + return { + scrollX: content.window.scrollX, + scrollY: content.window.scrollY, + }; + }); + } + + async waitForScrollTo(x, y) { + await ContentTask.spawn(this.browser, [x, y], async ([xPos, yPos]) => { + await ContentTaskUtils.waitForCondition(() => { + info( + `Got scrollX: ${content.window.scrollX}. scrollY: ${content.window.scrollY}` + ); + return ( + content.window.scrollX === xPos && content.window.scrollY === yPos + ); + }, `Waiting for window to scroll to ${xPos}, ${yPos}`); + }); + } + + clickDownloadButton() { + // Click the download button with last x and y position from dragOverlay. + // The middle of the copy button is last X - 70 and last Y + 36. + // Ex. 500, 500 would be 530, 536 + mouse.click(this.endX - 70, this.endY + 36); + } + + clickCopyButton(overrideX = null, overrideY = null) { + // Click the copy button with last x and y position from dragOverlay. + // The middle of the copy button is last X - 183 and last Y + 36. + // Ex. 500, 500 would be 317, 536 + if (overrideX && overrideY) { + mouse.click(overrideX - 183, overrideY + 36); + } else { + mouse.click(this.endX - 183, this.endY + 36); + } + } + + clickCancelButton() { + // Click the cancel button with last x and y position from dragOverlay. + // The middle of the copy button is last X - 259 and last Y + 36. + // Ex. 500, 500 would be 241, 536 + mouse.click(this.endX - 259, this.endY + 36); + } + + async clickTestPageElement() { + let rect = await ContentTask.spawn(this.browser, [], async () => { + let ele = content.document.getElementById("testPageElement"); + return ele.getBoundingClientRect(); + }); + + let x = Math.floor(rect.x + rect.width / 2); + let y = Math.floor(rect.y + rect.height / 2); + + mouse.move(x, y); + await this.waitForHoverElementRect(); + mouse.down(x, y); + await this.waitForStateChange("draggingReady"); + mouse.up(x, y); + await this.waitForStateChange("selected"); + } + + async zoomBrowser(zoom) { + await SpecialPowers.spawn(this.browser, [zoom], zoomLevel => { + const { Layout } = ChromeUtils.importESModule( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs" + ); + Layout.zoomDocument(content.document, zoomLevel); + }); + } + + /** + * Gets the dialog box + * @returns The dialog box + */ + getDialog() { + let currDialogBox = this.browser.tabDialogBox; + let manager = currDialogBox.getTabDialogManager(); + let dialogs = manager.hasDialogs && manager.dialogs; + return dialogs[0]; + } + + assertPanelVisible() { + let panel = this.browser.ownerDocument.querySelector( + "#screenshotsPagePanel" + ); + Assert.ok( + BrowserTestUtils.is_visible(panel), + "Screenshots panel is visible" + ); + } + + assertPanelNotVisible() { + let panel = this.browser.ownerDocument.querySelector( + "#screenshotsPagePanel" + ); + Assert.ok( + BrowserTestUtils.is_hidden(panel), + "Screenshots panel is not visible" + ); + } + + /** + * Copied from screenshots extension + * Returns a promise that resolves when the clipboard data has changed + * Otherwise rejects + */ + waitForRawClipboardChange() { + const initialClipboardData = Date.now().toString(); + SpecialPowers.clipboardCopyString(initialClipboardData); + + let promiseChanged = TestUtils.waitForCondition(() => { + let data; + try { + data = getRawClipboardData("image/png"); + } catch (e) { + console.log("Failed to get image/png clipboard data:", e); + return false; + } + return data && initialClipboardData !== data; + }); + return promiseChanged; + } + + /** + * Gets the client and scroll demensions on the window + * @returns { Object } + * clientHeight The visible height + * clientWidth The visible width + * scrollHeight The scrollable height + * scrollWidth The scrollable width + */ + getContentDimensions() { + return SpecialPowers.spawn(this.browser, [], async function () { + let { innerWidth, innerHeight, scrollMaxX, scrollMaxY } = content.window; + let width = innerWidth + scrollMaxX; + let height = innerHeight + scrollMaxY; + + const scrollbarHeight = {}; + const scrollbarWidth = {}; + content.window.windowUtils.getScrollbarSize( + false, + scrollbarWidth, + scrollbarHeight + ); + width -= scrollbarWidth.value; + height -= scrollbarHeight.value; + innerWidth -= scrollbarWidth.value; + innerHeight -= scrollbarHeight.value; + + return { + clientHeight: innerHeight, + clientWidth: innerWidth, + scrollHeight: height, + scrollWidth: width, + }; + }); + } + + getSelectionLayerDimensions() { + return ContentTask.spawn(this.browser, null, async () => { + let screenshotsChild = content.windowGlobalChild.getActor( + "ScreenshotsComponent" + ); + Assert.ok(screenshotsChild._overlay._initialized, "The overlay exists"); + + return screenshotsChild._overlay.screenshotsContainer.getSelectionLayerDimensions(); + }); + } + + async waitForSelectionLayerDimensionChange(oldWidth, oldHeight) { + await ContentTask.spawn( + this.browser, + [oldWidth, oldHeight], + async ([prevWidth, prevHeight]) => { + let screenshotsChild = content.windowGlobalChild.getActor( + "ScreenshotsComponent" + ); + + await ContentTaskUtils.waitForCondition(() => { + let dimensions = + screenshotsChild._overlay.screenshotsContainer.getSelectionLayerDimensions(); + info( + `old height: ${prevHeight}. new height: ${dimensions.scrollHeight}.\nold width: ${prevWidth}. new width: ${dimensions.scrollWidth}` + ); + return ( + dimensions.scrollHeight !== prevHeight && + dimensions.scrollWidth !== prevWidth + ); + }, "Wait for selection box width change"); + } + ); + } + + getSelectionBoxDimensions() { + return ContentTask.spawn(this.browser, null, async () => { + let screenshotsChild = content.windowGlobalChild.getActor( + "ScreenshotsComponent" + ); + Assert.ok(screenshotsChild._overlay._initialized, "The overlay exists"); + + return screenshotsChild._overlay.screenshotsContainer.getSelectionLayerBoxDimensions(); + }); + } + + /** + * Clicks an element on the screen + * @param eleSel The selector for the element to click + */ + async clickUIElement(eleSel) { + await SpecialPowers.spawn( + this.browser, + [eleSel], + async function (eleSelector) { + info( + `in clickScreenshotsUIElement content function, eleSelector: ${eleSelector}` + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + let ele = content.document.querySelector(eleSelector); + info(`Found the thing to click: ${eleSelector}: ${!!ele}`); + + EventUtils.synthesizeMouseAtCenter(ele, {}); + // wait a frame for the screenshots UI to finish any init + await new content.Promise(res => content.requestAnimationFrame(res)); + } + ); + } + + /** + * Copied from screenshots extension + * A helper that returns the size of the image that was just put into the clipboard by the + * :screenshot command. + * @return The {width, height, color} dimension and color object. + */ + async getImageSizeAndColorFromClipboard() { + let flavor = "image/png"; + let image = getRawClipboardData(flavor); + ok(image, "screenshot data exists on the clipboard"); + + // Due to the differences in how images could be stored in the clipboard the + // checks below are needed. The clipboard could already provide the image as + // byte streams or as image container. If it's not possible obtain a + // byte stream, the function throws. + + if (image instanceof Ci.imgIContainer) { + image = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .encodeImage(image, flavor); + } + + if (!(image instanceof Ci.nsIInputStream)) { + throw new Error("Unable to read image data"); + } + + const binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + binaryStream.setInputStream(image); + const available = binaryStream.available(); + const buffer = new ArrayBuffer(available); + is( + binaryStream.readArrayBuffer(available, buffer), + available, + "Read expected amount of data" + ); + + // We are going to load the image in the content page to measure its size. + // We don't want to insert the image directly in the browser's document + // which could mess all sorts of things up + return SpecialPowers.spawn( + this.browser, + [buffer], + async function (_buffer) { + const img = content.document.createElement("img"); + const loaded = new Promise(r => { + img.addEventListener("load", r, { once: true }); + }); + const url = content.URL.createObjectURL( + new Blob([_buffer], { type: "image/png" }) + ); + + img.src = url; + content.document.documentElement.appendChild(img); + + info("Waiting for the clipboard image to load in the content page"); + await loaded; + + let canvas = content.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "html:canvas" + ); + let context = canvas.getContext("2d"); + canvas.width = img.width; + canvas.height = img.height; + context.drawImage(img, 0, 0); + let topLeft = context.getImageData(0, 0, 1, 1); + let topRight = context.getImageData(img.width - 1, 0, 1, 1); + let bottomLeft = context.getImageData(0, img.height - 1, 1, 1); + let bottomRight = context.getImageData( + img.width - 1, + img.height - 1, + 1, + 1 + ); + + img.remove(); + content.URL.revokeObjectURL(url); + + return { + width: img.width, + height: img.height, + color: { + topLeft: topLeft.data, + topRight: topRight.data, + bottomLeft: bottomLeft.data, + bottomRight: bottomRight.data, + }, + }; + } + ); + } +} + +/** + * Get the raw clipboard data + * @param flavor Type of data to get from clipboard + * @returns The data from the clipboard + */ +function getRawClipboardData(flavor) { + const whichClipboard = Services.clipboard.kGlobalClipboard; + const xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + xferable.init(null); + xferable.addDataFlavor(flavor); + Services.clipboard.getData(xferable, whichClipboard); + let data = {}; + try { + // xferable.getTransferData(flavor, data); + xferable.getAnyTransferData({}, data); + info(JSON.stringify(data, null, 2)); + } catch (e) { + info(e); + } + data = data.value || null; + return data; +} + +/** + * Synthesize a mouse event on an element, after ensuring that it is visible + * in the viewport. + * + * @param {String} selector: The node selector to get the node target for the event. + * @param {number} x + * @param {number} y + * @param {object} options: Options that will be passed to BrowserTestUtils.synthesizeMouse + */ +async function safeSynthesizeMouseEventInContentPage( + selector, + x, + y, + options = {} +) { + let context = gBrowser.selectedBrowser.browsingContext; + BrowserTestUtils.synthesizeMouse(selector, x, y, options, context); +} + +add_setup(async () => { + CustomizableUI.addWidgetToArea( + "screenshot-button", + CustomizableUI.AREA_NAVBAR + ); + let screenshotBtn = document.getElementById("screenshot-button"); + Assert.ok(screenshotBtn, "The screenshots button was added to the nav bar"); +}); + +function getContentDevicePixelRatio(browser) { + return SpecialPowers.spawn(browser, [], async function () { + return content.window.devicePixelRatio; + }); +} + +async function clearAllTelemetryEvents() { + // Clear everything. + info("Clearing all telemetry events"); + await TestUtils.waitForCondition(() => { + Services.telemetry.clearEvents(); + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ); + let content = events.content; + let parent = events.parent; + + return (!content && !parent) || (!content.length && !parent.length); + }); +} + +async function waitForScreenshotsEventCount(count, process = "parent") { + await TestUtils.waitForCondition( + () => { + let events = TelemetryTestUtils.getEvents( + { category: "screenshots" }, + { process } + ); + + info(`Got ${events?.length} event(s)`); + info(`Actual events: ${JSON.stringify(events, null, 2)}`); + return events.length === count ? events : null; + }, + `Waiting for ${count} ${process} event(s).`, + 200, + 100 + ); +} + +async function assertScreenshotsEvents(expectedEvents, process = "parent") { + info(`Expected events: ${JSON.stringify(expectedEvents, null, 2)}`); + // Make sure we have recorded the correct number of events + await waitForScreenshotsEventCount(expectedEvents.length, process); + + TelemetryTestUtils.assertEvents( + expectedEvents, + { category: "screenshots", clear: true }, + { process } + ); +} -- cgit v1.2.3