Bug 1791086 - Select elements within iframes in screenshots. r=sfoster,mconley

Differential Revision: https://phabricator.services.mozilla.com/D195165
This commit is contained in:
Niklas Baumgardner 2024-02-09 22:02:51 +00:00
Родитель fa0e1f01ae
Коммит 0aaf858381
12 изменённых файлов: 354 добавлений и 10 удалений

Просмотреть файл

@ -713,6 +713,17 @@ let JSWINDOWACTORS = {
enablePreference: "screenshots.browser.component.enabled",
},
ScreenshotsHelper: {
parent: {
esModuleURI: "resource:///modules/ScreenshotsUtils.sys.mjs",
},
child: {
esModuleURI: "resource:///modules/ScreenshotsHelperChild.sys.mjs",
},
allFrames: true,
enablePreference: "screenshots.browser.component.enabled",
},
SearchSERPTelemetry: {
parent: {
esModuleURI: "resource:///actors/SearchSERPTelemetryParent.sys.mjs",

Просмотреть файл

@ -0,0 +1,47 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
getBestRectForElement,
getElementFromPoint,
} from "chrome://browser/content/screenshots/overlayHelpers.mjs";
/**
* This class is used to get the dimensions of hovered elements within iframes.
* The main content process cannot get the dimensions of elements within
* iframes so a message will be send to this actor to get the dimensions of the
* element for a given point inside the iframe.
*/
export class ScreenshotsHelperChild extends JSWindowActorChild {
receiveMessage(message) {
if (message.name === "ScreenshotsHelper:GetElementRectFromPoint") {
return this.getBestElementRectFromPoint(message.data);
}
return null;
}
async getBestElementRectFromPoint(data) {
let { x, y } = data;
x -= this.contentWindow.mozInnerScreenX;
y -= this.contentWindow.mozInnerScreenY;
let { ele, rect } = await getElementFromPoint(x, y, this.document);
if (!rect) {
rect = getBestRectForElement(ele, this.document);
}
if (rect) {
rect = {
left: rect.left + this.contentWindow.mozInnerScreenX,
right: rect.right + this.contentWindow.mozInnerScreenX,
top: rect.top + this.contentWindow.mozInnerScreenY,
bottom: rect.bottom + this.contentWindow.mozInnerScreenY,
};
}
return rect;
}
}

Просмотреть файл

@ -30,6 +30,7 @@ import {
setMaxDetectHeight,
setMaxDetectWidth,
getBestRectForElement,
getElementFromPoint,
Region,
WindowDimensions,
} from "chrome://browser/content/screenshots/overlayHelpers.mjs";
@ -1386,18 +1387,27 @@ export class ScreenshotsOverlay {
* @param {Number} clientX The x position relative to the viewport
* @param {Number} clientY The y position relative to the viewport
*/
handleElementHover(clientX, clientY) {
async handleElementHover(clientX, clientY) {
this.setPointerEventsNone();
let ele = this.document.elementFromPoint(clientX, clientY);
let promise = getElementFromPoint(clientX, clientY, this.document);
this.resetPointerEvents();
let { ele, rect } = await promise;
if (this.#cachedEle && this.#cachedEle === ele) {
if (
this.#cachedEle &&
!this.window.HTMLIFrameElement.isInstance(this.#cachedEle) &&
this.#cachedEle === ele
) {
// Still hovering over the same element
return;
}
this.#cachedEle = ele;
let rect = getBestRectForElement(ele, this.document);
if (!rect) {
// this means we found an element that wasn't an iframe
rect = getBestRectForElement(ele, this.document);
}
if (rect) {
let { scrollX, scrollY } = this.windowDimensions.dimensions;
let { left, top, right, bottom } = rect;

Просмотреть файл

@ -109,6 +109,19 @@ export class ScreenshotsComponentParent extends JSWindowActorParent {
}
}
export class ScreenshotsHelperParent extends JSWindowActorParent {
receiveMessage(message) {
switch (message.name) {
case "ScreenshotsHelper:GetElementRectFromPoint":
let cxt = BrowsingContext.get(message.data.bcId);
return cxt.currentWindowGlobal
.getActor("ScreenshotsHelper")
.sendQuery("ScreenshotsHelper:GetElementRectFromPoint", message.data);
}
return null;
}
}
export const UIPhases = {
CLOSED: 0, // nothing showing
INITIAL: 1, // panel and overlay showing

Просмотреть файл

@ -5,6 +5,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXTRA_JS_MODULES += [
"ScreenshotsHelperChild.sys.mjs",
"ScreenshotsOverlayChild.sys.mjs",
"ScreenshotsUtils.sys.mjs",
]

Просмотреть файл

@ -42,6 +42,64 @@ export function setMaxDetectWidth(maxWidth) {
MAX_DETECT_WIDTH = maxWidth;
}
/**
* This function will try to get an element from a given point in the doc.
* This function is recursive because when sending a message to the
* ScreenshotsHelper, the ScreenshotsHelper will call into this function.
* This only occurs when the element at the given point is an iframe.
*
* If the element is an iframe, we will send a message to the ScreenshotsHelper
* actor in the correct context to get the element at the given point.
* The message will return the "getBestRectForElement" for the element at the
* given point.
*
* If the element is not an iframe, then we will just return the element.
*
* @param {Number} x The x coordinate
* @param {Number} y The y coordinate
* @param {Document} doc The document
* @returns {Object}
* ele: The element for a given point (x, y)
* rect: The rect for the given point if ele is an iframe
* otherwise null
*/
export async function getElementFromPoint(x, y, doc) {
let ele = null;
let rect = null;
try {
ele = doc.elementFromPoint(x, y);
// if the element is an iframe, we need to send a message to that browsing context
// to get the coordinates of the element in the iframe
if (doc.defaultView.HTMLIFrameElement.isInstance(ele)) {
let actor =
ele.browsingContext.parentWindowContext.windowGlobalChild.getActor(
"ScreenshotsHelper"
);
rect = await actor.sendQuery(
"ScreenshotsHelper:GetElementRectFromPoint",
{
x: x + ele.ownerGlobal.mozInnerScreenX,
y: y + ele.ownerGlobal.mozInnerScreenY,
bcId: ele.browsingContext.id,
}
);
if (rect) {
rect = {
left: rect.left - ele.ownerGlobal.mozInnerScreenX,
right: rect.right - ele.ownerGlobal.mozInnerScreenX,
top: rect.top - ele.ownerGlobal.mozInnerScreenY,
bottom: rect.bottom - ele.ownerGlobal.mozInnerScreenY,
};
}
}
} catch (e) {
console.error(e);
}
return { ele, rect };
}
/**
* This function takes an element and finds a suitable rect to draw the hover box on
* @param {Element} ele The element to find a suitale rect of

Просмотреть файл

@ -1,6 +1,9 @@
[DEFAULT]
support-files = [
"head.js",
"iframe-test-page.html",
"first-iframe.html",
"second-iframe.html",
"test-page.html",
"short-test-page.html",
"large-test-page.html",
@ -11,6 +14,8 @@ prefs = [
"screenshots.browser.component.enabled=true",
]
["browser_iframe_test.js"]
["browser_overlay_keyboard_test.js"]
["browser_screenshots_drag_scroll_test.js"]

Просмотреть файл

@ -0,0 +1,123 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_selectingElementsInIframes() {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: IFRAME_TEST_PAGE,
},
async browser => {
let helper = new ScreenshotsHelper(browser);
helper.triggerUIFromToolbar();
// There are two iframes in the test page. One iframe is nested in the
// other so we SpecialPowers.spawn into the iframes to get the
// dimension/position of the elements within each iframe.
let elementDimensions = await SpecialPowers.spawn(
browser,
[],
async () => {
let divDims = content.document
.querySelector("div")
.getBoundingClientRect();
let iframe = content.document.querySelector("iframe");
let iframesDivsDimArr = await SpecialPowers.spawn(
iframe,
[],
async () => {
let iframeDivDims = content.document
.querySelector("div")
.getBoundingClientRect();
// Element within the first iframe
iframeDivDims = {
left: iframeDivDims.left + content.window.mozInnerScreenX,
top: iframeDivDims.top + content.window.mozInnerScreenY,
width: iframeDivDims.width,
height: iframeDivDims.height,
};
let nestedIframe = content.document.querySelector("iframe");
let nestedIframeDivDims = await SpecialPowers.spawn(
nestedIframe,
[],
async () => {
let secondIframeDivDims = content.document
.querySelector("div")
.getBoundingClientRect();
// Element within the nested iframe
secondIframeDivDims = {
left:
secondIframeDivDims.left +
content.document.defaultView.mozInnerScreenX,
top:
secondIframeDivDims.top +
content.document.defaultView.mozInnerScreenY,
width: secondIframeDivDims.width,
height: secondIframeDivDims.height,
};
return secondIframeDivDims;
}
);
return [iframeDivDims, nestedIframeDivDims];
}
);
// Offset each element position for the browser window
for (let dims of iframesDivsDimArr) {
dims.left -= content.window.mozInnerScreenX;
dims.top -= content.window.mozInnerScreenY;
}
return [divDims].concat(iframesDivsDimArr);
}
);
info(JSON.stringify(elementDimensions, null, 2));
for (let el of elementDimensions) {
let x = el.left + el.width / 2;
let y = el.top + el.height / 2;
mouse.move(x, y);
await helper.waitForHoverElementRect(el.width, el.height);
mouse.click(x, y);
await helper.waitForStateChange("selected");
let dimensions = await helper.getSelectionRegionDimensions();
is(
dimensions.left,
el.left,
"The region left position matches the elements left position"
);
is(
dimensions.top,
el.top,
"The region top position matches the elements top position"
);
is(
dimensions.width,
el.width,
"The region width matches the elements width"
);
is(
dimensions.height,
el.height,
"The region height matches the elements height"
);
mouse.click(500, 500);
await helper.waitForStateChange("crosshairs");
}
}
);
});

Просмотреть файл

@ -0,0 +1,23 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
div {
font-size: 40px;
margin: 30px;
width: 234px;
height: 51px;
color: blue;
}
</style>
</head>
<body>
<div>Hello world!</div>
<iframe
width="300"
height="300"
src="https://example.org/browser/browser/components/screenshots/tests/browser/second-iframe.html"
></iframe>
</body>
</html>

Просмотреть файл

@ -18,6 +18,7 @@ const TEST_ROOT = getRootDirectory(gTestPath).replace(
const TEST_PAGE = TEST_ROOT + "test-page.html";
const SHORT_TEST_PAGE = TEST_ROOT + "short-test-page.html";
const LARGE_TEST_PAGE = TEST_ROOT + "large-test-page.html";
const IFRAME_TEST_PAGE = TEST_ROOT + "iframe-test-page.html";
const { MAX_CAPTURE_DIMENSION, MAX_CAPTURE_AREA } = ChromeUtils.importESModule(
"resource:///modules/ScreenshotsUtils.sys.mjs"
@ -195,11 +196,22 @@ class ScreenshotsHelper {
});
}
async waitForHoverElementRect() {
return TestUtils.waitForCondition(async () => {
let rect = await this.getHoverElementRect();
return rect;
});
async waitForHoverElementRect(expectedWidth, expectedHeight) {
return SpecialPowers.spawn(
this.browser,
[expectedWidth, expectedHeight],
async (width, height) => {
let screenshotsChild = content.windowGlobalChild.getActor(
"ScreenshotsComponent"
);
let dimensions;
await ContentTaskUtils.waitForCondition(() => {
dimensions = screenshotsChild.overlay.hoverElementRegion.dimensions;
return dimensions.width === width && dimensions.height === height;
}, "The hover element region is the expected width and height");
return dimensions;
}
);
}
async waitForSelectionRegionSizeChange(currentWidth) {
@ -394,7 +406,7 @@ class ScreenshotsHelper {
let y = Math.floor(rect.y + rect.height / 2);
mouse.move(x, y);
await this.waitForHoverElementRect();
await this.waitForHoverElementRect(rect.width, rect.height);
mouse.down(x, y);
await this.assertStateChange("draggingReady");
mouse.up(x, y);

Просмотреть файл

@ -0,0 +1,23 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<title>Screenshots</title>
<style>
div {
font-size: 40px;
margin: 30px;
width: 233px;
height: 50px;
color: green;
}
</style>
</head>
<body>
<div>Hello world!</div>
<iframe
width="500"
height="500"
src="https://example.com/browser/browser/components/screenshots/tests/browser/first-iframe.html"
></iframe>
</body>
</html>

Просмотреть файл

@ -0,0 +1,18 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
div {
font-size: 40px;
margin: 30px;
width: 235px;
height: 52px;
color: red;
}
</style>
</head>
<body>
<div>Hello world!</div>
</body>
</html>