summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/test/highlighter-test-actor.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/test/highlighter-test-actor.js')
-rw-r--r--devtools/client/shared/test/highlighter-test-actor.js939
1 files changed, 939 insertions, 0 deletions
diff --git a/devtools/client/shared/test/highlighter-test-actor.js b/devtools/client/shared/test/highlighter-test-actor.js
new file mode 100644
index 0000000000..8a7b404df1
--- /dev/null
+++ b/devtools/client/shared/test/highlighter-test-actor.js
@@ -0,0 +1,939 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* exported HighlighterTestActor, HighlighterTestFront */
+
+"use strict";
+
+// A helper actor for testing highlighters.
+// ⚠️ This should only be used for getting data for objects using CanvasFrameAnonymousContentHelper,
+// that we can't get directly from tests.
+const {
+ getRect,
+ getAdjustedQuads,
+} = require("resource://devtools/shared/layout/utils.js");
+
+// Set up a dummy environment so that EventUtils works. We need to be careful to
+// pass a window object into each EventUtils method we call rather than having
+// it rely on the |window| global.
+const EventUtils = {};
+EventUtils.window = {};
+EventUtils.parent = {};
+/* eslint-disable camelcase */
+EventUtils._EU_Ci = Ci;
+EventUtils._EU_Cc = Cc;
+/* eslint-disable camelcase */
+Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+);
+
+// We're an actor so we don't run in the browser test environment, so
+// we need to import TestUtils manually despite what the linter thinks.
+// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+const protocol = require("resource://devtools/shared/protocol.js");
+const { Arg, RetVal } = protocol;
+
+const dumpn = msg => {
+ dump(msg + "\n");
+};
+
+/**
+ * Get the instance of CanvasFrameAnonymousContentHelper used by a given
+ * highlighter actor.
+ * The instance provides methods to get/set attributes/text/style on nodes of
+ * the highlighter, inserted into the nsCanvasFrame.
+ * @see /devtools/server/actors/highlighters.js
+ * @param {String} actorID
+ */
+function getHighlighterCanvasFrameHelper(conn, actorID) {
+ // Retrieve the CustomHighlighterActor by its actorID:
+ const actor = conn.getActor(actorID);
+ if (!actor) {
+ return null;
+ }
+
+ // Retrieve the sub class instance specific to each highlighter type:
+ let highlighter = actor.instance;
+
+ // SelectorHighlighter and TabbingOrderHighlighter can hold multiple highlighters.
+ // For now, only retrieve the first highlighter.
+ if (
+ highlighter._highlighters &&
+ Array.isArray(highlighter._highlighters) &&
+ highlighter._highlighters.length
+ ) {
+ highlighter = highlighter._highlighters[0];
+ }
+
+ // Now, `highlighter` should be a final highlighter class, exposing
+ // `CanvasFrameAnonymousContentHelper` via a `markup` attribute.
+ if (highlighter.markup) {
+ return highlighter.markup;
+ }
+
+ // Here we didn't find any highlighter; it can happen if the actor is a
+ // FontsHighlighter (which does not use a CanvasFrameAnonymousContentHelper).
+ return null;
+}
+
+var highlighterTestSpec = protocol.generateActorSpec({
+ typeName: "highlighterTest",
+
+ events: {
+ "highlighter-updated": {},
+ },
+
+ methods: {
+ getHighlighterAttribute: {
+ request: {
+ nodeID: Arg(0, "string"),
+ name: Arg(1, "string"),
+ actorID: Arg(2, "string"),
+ },
+ response: {
+ value: RetVal("string"),
+ },
+ },
+ getHighlighterBoundingClientRect: {
+ request: {
+ nodeID: Arg(0, "string"),
+ actorID: Arg(1, "string"),
+ },
+ response: {
+ value: RetVal("json"),
+ },
+ },
+ getHighlighterComputedStyle: {
+ request: {
+ nodeID: Arg(0, "string"),
+ property: Arg(1, "string"),
+ actorID: Arg(2, "string"),
+ },
+ response: {
+ value: RetVal("string"),
+ },
+ },
+ getHighlighterNodeTextContent: {
+ request: {
+ nodeID: Arg(0, "string"),
+ actorID: Arg(1, "string"),
+ },
+ response: {
+ value: RetVal("string"),
+ },
+ },
+ getSelectorHighlighterBoxNb: {
+ request: {
+ highlighter: Arg(0, "string"),
+ },
+ response: {
+ value: RetVal("number"),
+ },
+ },
+ changeHighlightedNodeWaitForUpdate: {
+ request: {
+ name: Arg(0, "string"),
+ value: Arg(1, "string"),
+ actorID: Arg(2, "string"),
+ },
+ response: {},
+ },
+ registerOneTimeHighlighterUpdate: {
+ request: {
+ actorID: Arg(0, "string"),
+ },
+ response: {},
+ },
+ getNodeRect: {
+ request: {
+ selector: Arg(0, "string"),
+ },
+ response: {
+ value: RetVal("json"),
+ },
+ },
+ getTextNodeRect: {
+ request: {
+ parentSelector: Arg(0, "string"),
+ childNodeIndex: Arg(1, "number"),
+ },
+ response: {
+ value: RetVal("json"),
+ },
+ },
+ isPausedDebuggerOverlayVisible: {
+ request: {},
+ response: {
+ value: RetVal("boolean"),
+ },
+ },
+ clickPausedDebuggerOverlayButton: {
+ request: {
+ id: Arg(0, "string"),
+ },
+ response: {},
+ },
+ isEyeDropperVisible: {
+ request: {},
+ response: {
+ value: RetVal("boolean"),
+ },
+ },
+ getEyeDropperElementAttribute: {
+ request: {
+ elementId: Arg(0, "string"),
+ attributeName: Arg(1, "string"),
+ },
+ response: {
+ value: RetVal("string"),
+ },
+ },
+ getEyeDropperColorValue: {
+ request: {},
+ response: {
+ value: RetVal("string"),
+ },
+ },
+ getTabbingOrderHighlighterData: {
+ request: {},
+ response: {
+ value: RetVal("json"),
+ },
+ },
+ },
+});
+
+class HighlighterTestActor extends protocol.Actor {
+ constructor(conn, targetActor, options) {
+ super(conn, highlighterTestSpec);
+
+ this.targetActor = targetActor;
+ }
+
+ get content() {
+ return this.targetActor.window;
+ }
+
+ /**
+ * Helper to retrieve a DOM element.
+ * @param {string | array} selector Either a regular selector string
+ * or a selector array. If an array, each item, except the last one
+ * are considered matching an iframe, so that we can query element
+ * within deep iframes.
+ */
+ _querySelector(selector) {
+ let document = this.content.document;
+ if (Array.isArray(selector)) {
+ const fullSelector = selector.join(" >> ");
+ while (selector.length > 1) {
+ const str = selector.shift();
+ const iframe = document.querySelector(str);
+ if (!iframe) {
+ throw new Error(
+ 'Unable to find element with selector "' +
+ str +
+ '"' +
+ " (full selector:" +
+ fullSelector +
+ ")"
+ );
+ }
+ if (!iframe.contentWindow) {
+ throw new Error(
+ "Iframe selector doesn't target an iframe \"" +
+ str +
+ '"' +
+ " (full selector:" +
+ fullSelector +
+ ")"
+ );
+ }
+ document = iframe.contentWindow.document;
+ }
+ selector = selector.shift();
+ }
+ const node = document.querySelector(selector);
+ if (!node) {
+ throw new Error(
+ 'Unable to find element with selector "' + selector + '"'
+ );
+ }
+ return node;
+ }
+
+ /**
+ * Get a value for a given attribute name, on one of the elements of the box
+ * model highlighter, given its ID.
+ * @param {String} nodeID The full ID of the element to get the attribute for
+ * @param {String} name The name of the attribute to get
+ * @param {String} actorID The highlighter actor ID
+ * @return {String} The value, if found, null otherwise
+ */
+ getHighlighterAttribute(nodeID, name, actorID) {
+ const helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
+
+ if (!helper) {
+ throw new Error(`Highlighter not found`);
+ }
+
+ return helper.getAttributeForElement(nodeID, name);
+ }
+
+ /**
+ * Get the bounding client rect for an highlighter element, given its ID.
+ *
+ * @param {String} nodeID The full ID of the element to get the DOMRect for
+ * @param {String} actorID The highlighter actor ID
+ * @return {DOMRect} The value, if found, null otherwise
+ */
+ getHighlighterBoundingClientRect(nodeID, actorID) {
+ const helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
+
+ if (!helper) {
+ throw new Error(`Highlighter not found`);
+ }
+
+ return helper.getBoundingClientRect(nodeID);
+ }
+
+ /**
+ * Get the computed style for a given property, on one of the elements of the
+ * box model highlighter, given its ID.
+ * @param {String} nodeID The full ID of the element to get the attribute for
+ * @param {String} property The name of the property
+ * @param {String} actorID The highlighter actor ID
+ * @return {String} The computed style of the property
+ */
+ getHighlighterComputedStyle(nodeID, property, actorID) {
+ const helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
+
+ if (!helper) {
+ throw new Error(`Highlighter not found`);
+ }
+
+ return helper.getElement(nodeID).computedStyle.getPropertyValue(property);
+ }
+
+ /**
+ * Get the textcontent of one of the elements of the box model highlighter,
+ * given its ID.
+ * @param {String} nodeID The full ID of the element to get the attribute for
+ * @param {String} actorID The highlighter actor ID
+ * @return {String} The textcontent value
+ */
+ getHighlighterNodeTextContent(nodeID, actorID) {
+ let value;
+ const helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
+ if (helper) {
+ value = helper.getTextContentForElement(nodeID);
+ }
+ return value;
+ }
+
+ /**
+ * Get the number of box-model highlighters created by the SelectorHighlighter
+ * @param {String} actorID The highlighter actor ID
+ * @return {Number} The number of box-model highlighters created, or null if the
+ * SelectorHighlighter was not found.
+ */
+ getSelectorHighlighterBoxNb(actorID) {
+ const highlighter = this.conn.getActor(actorID);
+ const { _highlighter: h } = highlighter;
+ if (!h || !h._highlighters) {
+ return null;
+ }
+ return h._highlighters.length;
+ }
+
+ /**
+ * Subscribe to the box-model highlighter's update event, modify an attribute of
+ * the currently highlighted node and send a message when the highlighter has
+ * updated.
+ * @param {String} the name of the attribute to be changed
+ * @param {String} the new value for the attribute
+ * @param {String} actorID The highlighter actor ID
+ */
+ changeHighlightedNodeWaitForUpdate(name, value, actorID) {
+ return new Promise(resolve => {
+ const highlighter = this.conn.getActor(actorID);
+ const { _highlighter: h } = highlighter;
+
+ h.once("updated", resolve);
+
+ h.currentNode.setAttribute(name, value);
+ });
+ }
+
+ /**
+ * Register a one-time "updated" event listener.
+ * The method does not wait for the "updated" event itself so the response can be sent
+ * back and the client would know the event listener is properly set.
+ * A separate event, "highlighter-updated", will be emitted when the highlighter updates.
+ *
+ * @param {String} actorID The highlighter actor ID
+ */
+ registerOneTimeHighlighterUpdate(actorID) {
+ const { _highlighter } = this.conn.getActor(actorID);
+ _highlighter.once("updated").then(() => this.emit("highlighter-updated"));
+
+ // Return directly so the client knows the event listener is set
+ }
+
+ async getNodeRect(selector) {
+ const node = this._querySelector(selector);
+ return getRect(this.content, node, this.content);
+ }
+
+ async getTextNodeRect(parentSelector, childNodeIndex) {
+ const parentNode = this._querySelector(parentSelector);
+ const node = parentNode.childNodes[childNodeIndex];
+ return getAdjustedQuads(this.content, node)[0].bounds;
+ }
+
+ /**
+ * @returns {PausedDebuggerOverlay} The paused overlay instance
+ */
+ _getPausedDebuggerOverlay() {
+ // We use `_pauseOverlay` since it's the cached value; `pauseOverlay` is a getter that
+ // will create the overlay when called (if it does not exist yet).
+ return this.targetActor?.threadActor?._pauseOverlay;
+ }
+
+ isPausedDebuggerOverlayVisible() {
+ const pauseOverlay = this._getPausedDebuggerOverlay();
+ if (!pauseOverlay) {
+ return false;
+ }
+
+ const root = pauseOverlay.getElement("root");
+ const toolbar = pauseOverlay.getElement("toolbar");
+
+ return (
+ root.getAttribute("hidden") !== "true" &&
+ root.getAttribute("overlay") == "true" &&
+ toolbar.getAttribute("hidden") !== "true" &&
+ !!toolbar.getTextContent()
+ );
+ }
+
+ /**
+ * Simulates a click on a button of the debugger pause overlay.
+ *
+ * @param {String} id: The id of the element (e.g. "paused-dbg-resume-button").
+ */
+ async clickPausedDebuggerOverlayButton(id) {
+ const pauseOverlay = this._getPausedDebuggerOverlay();
+ if (!pauseOverlay) {
+ return;
+ }
+
+ // Because the highlighter markup elements live inside an anonymous content frame which
+ // does not expose an API to dispatch events to them, we can't directly dispatch
+ // events to the nodes themselves.
+ // We're directly calling `handleEvent` on the pause overlay, which is the mouse events
+ // listener callback on the overlay.
+ pauseOverlay.handleEvent({ type: "mousedown", target: { id } });
+ }
+
+ /**
+ * @returns {EyeDropper}
+ */
+ _getEyeDropper() {
+ const form = this.targetActor.form();
+ const inspectorActor = this.conn._getOrCreateActor(form.inspectorActor);
+ return inspectorActor?._eyeDropper;
+ }
+
+ isEyeDropperVisible() {
+ const eyeDropper = this._getEyeDropper();
+ if (!eyeDropper) {
+ return false;
+ }
+
+ return eyeDropper.getElement("root").getAttribute("hidden") !== "true";
+ }
+
+ getEyeDropperElementAttribute(elementId, attributeName) {
+ const eyeDropper = this._getEyeDropper();
+ if (!eyeDropper) {
+ return null;
+ }
+
+ return eyeDropper.getElement(elementId).getAttribute(attributeName);
+ }
+
+ async getEyeDropperColorValue() {
+ const eyeDropper = this._getEyeDropper();
+ if (!eyeDropper) {
+ return null;
+ }
+
+ // It might happen that while the eyedropper isn't hidden anymore, the color-value
+ // is not set yet.
+ const color = await TestUtils.waitForCondition(() => {
+ const colorValueElement = eyeDropper.getElement("color-value");
+ const textContent = colorValueElement.getTextContent();
+ return textContent;
+ }, "Couldn't get a non-empty text content for the color-value element");
+
+ return color;
+ }
+
+ /**
+ * Get the TabbingOrderHighlighter for the associated targetActor
+ *
+ * @returns {TabbingOrderHighlighter}
+ */
+ _getTabbingOrderHighlighter() {
+ const form = this.targetActor.form();
+ const accessibilityActor = this.conn._getOrCreateActor(
+ form.accessibilityActor
+ );
+
+ if (!accessibilityActor) {
+ return null;
+ }
+ // We use `_tabbingOrderHighlighter` since it's the cached value; `tabbingOrderHighlighter`
+ // is a getter that will create the highlighter when called (if it does not exist yet).
+ return accessibilityActor.walker?._tabbingOrderHighlighter;
+ }
+
+ /**
+ * Get a representation of the NodeTabbingOrderHighlighters created by the
+ * TabbingOrderHighlighter of a given targetActor.
+ *
+ * @returns {Array<String>} An array which will contain as many entry as they are
+ * NodeTabbingOrderHighlighters displayed.
+ * Each item will be of the form `nodename[#id]: index`.
+ * For example:
+ * [
+ * `button#top-btn-1 : 1`,
+ * `html : 2`,
+ * `button#iframe-btn-1 : 3`,
+ * `button#iframe-btn-2 : 4`,
+ * `button#top-btn-2 : 5`,
+ * ]
+ */
+ getTabbingOrderHighlighterData() {
+ const highlighter = this._getTabbingOrderHighlighter();
+ if (!highlighter) {
+ return [];
+ }
+
+ const nodeTabbingOrderHighlighters = [
+ ...highlighter._highlighter._highlighters.values(),
+ ].filter(h => h.getElement("root").getAttribute("hidden") !== "true");
+
+ return nodeTabbingOrderHighlighters.map(h => {
+ let nodeStr = h.currentNode.nodeName.toLowerCase();
+ if (h.currentNode.id) {
+ nodeStr = `${nodeStr}#${h.currentNode.id}`;
+ }
+ return `${nodeStr} : ${h.getElement("root").getTextContent()}`;
+ });
+ }
+}
+exports.HighlighterTestActor = HighlighterTestActor;
+
+class HighlighterTestFront extends protocol.FrontClassWithSpec(
+ highlighterTestSpec
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this.formAttributeName = "highlighterTestActor";
+ // The currently active highlighter is obtained by calling a custom getter
+ // provided manually after requesting TestFront. See `getHighlighterTestFront(toolbox)`
+ this._highlighter = null;
+ }
+
+ /**
+ * Override the highlighter getter with a custom method that returns
+ * the currently active highlighter instance.
+ *
+ * @param {Function|Highlighter} _customHighlighterGetter
+ */
+ set highlighter(_customHighlighterGetter) {
+ this._highlighter = _customHighlighterGetter;
+ }
+
+ /**
+ * The currently active highlighter instance.
+ * If there is a custom getter for the highlighter, return its result.
+ *
+ * @return {Highlighter|null}
+ */
+ get highlighter() {
+ return typeof this._highlighter === "function"
+ ? this._highlighter()
+ : this._highlighter;
+ }
+
+ /* eslint-disable max-len */
+ changeHighlightedNodeWaitForUpdate(name, value, highlighter) {
+ /* eslint-enable max-len */
+ return super.changeHighlightedNodeWaitForUpdate(
+ name,
+ value,
+ (highlighter || this.highlighter).actorID
+ );
+ }
+
+ /**
+ * Get the value of an attribute on one of the highlighter's node.
+ * @param {String} nodeID The Id of the node in the highlighter.
+ * @param {String} name The name of the attribute.
+ * @param {Object} highlighter Optional custom highlighter to target
+ * @return {String} value
+ */
+ getHighlighterNodeAttribute(nodeID, name, highlighter) {
+ return this.getHighlighterAttribute(
+ nodeID,
+ name,
+ (highlighter || this.highlighter).actorID
+ );
+ }
+
+ getHighlighterNodeTextContent(nodeID, highlighter) {
+ return super.getHighlighterNodeTextContent(
+ nodeID,
+ (highlighter || this.highlighter).actorID
+ );
+ }
+
+ /**
+ * Get the computed style of a property on one of the highlighter's node.
+ * @param {String} nodeID The Id of the node in the highlighter.
+ * @param {String} property The name of the property.
+ * @param {Object} highlighter Optional custom highlighter to target
+ * @return {String} value
+ */
+ getHighlighterComputedStyle(nodeID, property, highlighter) {
+ return super.getHighlighterComputedStyle(
+ nodeID,
+ property,
+ (highlighter || this.highlighter).actorID
+ );
+ }
+
+ /**
+ * Is the highlighter currently visible on the page?
+ */
+ async isHighlighting() {
+ // Once the highlighter is hidden, the reference to it is lost.
+ // Assume it is not highlighting.
+ if (!this.highlighter) {
+ return false;
+ }
+
+ try {
+ const hidden = await this.getHighlighterNodeAttribute(
+ "box-model-elements",
+ "hidden"
+ );
+ return hidden === null;
+ } catch (e) {
+ if (e.message.match(/Highlighter not found/)) {
+ return false;
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Get the current rect of the border region of the box-model highlighter
+ */
+ async getSimpleBorderRect() {
+ const { border } = await this.getBoxModelStatus();
+ const { p1, p2, p4 } = border.points;
+
+ return {
+ top: p1.y,
+ left: p1.x,
+ width: p2.x - p1.x,
+ height: p4.y - p1.y,
+ };
+ }
+
+ /**
+ * Get the current positions and visibility of the various box-model highlighter
+ * elements.
+ */
+ async getBoxModelStatus() {
+ const isVisible = await this.isHighlighting();
+
+ const ret = {
+ visible: isVisible,
+ };
+
+ for (const region of ["margin", "border", "padding", "content"]) {
+ const points = await this._getPointsForRegion(region);
+ const visible = await this._isRegionHidden(region);
+ ret[region] = { points, visible };
+ }
+
+ ret.guides = {};
+ for (const guide of ["top", "right", "bottom", "left"]) {
+ ret.guides[guide] = await this._getGuideStatus(guide);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Check that the box-model highlighter is currently highlighting the node matching the
+ * given selector.
+ * @param {String} selector
+ * @return {Boolean}
+ */
+ async assertHighlightedNode(selector) {
+ const rect = await this.getNodeRect(selector);
+ return this.isNodeRectHighlighted(rect);
+ }
+
+ /**
+ * Check that the box-model highlighter is currently highlighting the text node that can
+ * be found at a given index within the list of childNodes of a parent element matching
+ * the given selector.
+ * @param {String} parentSelector
+ * @param {Number} childNodeIndex
+ * @return {Boolean}
+ */
+ async assertHighlightedTextNode(parentSelector, childNodeIndex) {
+ const rect = await this.getTextNodeRect(parentSelector, childNodeIndex);
+ return this.isNodeRectHighlighted(rect);
+ }
+
+ /**
+ * Check that the box-model highlighter is currently highlighting the given rect.
+ * @param {Object} rect
+ * @return {Boolean}
+ */
+ async isNodeRectHighlighted({ left, top, width, height }) {
+ const { visible, border } = await this.getBoxModelStatus();
+ let points = border.points;
+ if (!visible) {
+ return false;
+ }
+
+ // Check that the node is within the box model
+ const right = left + width;
+ const bottom = top + height;
+
+ // Converts points dictionnary into an array
+ const list = [];
+ for (let i = 1; i <= 4; i++) {
+ const p = points["p" + i];
+ list.push([p.x, p.y]);
+ }
+ points = list;
+
+ // Check that each point of the node is within the box model
+ return (
+ isInside([left, top], points) &&
+ isInside([right, top], points) &&
+ isInside([right, bottom], points) &&
+ isInside([left, bottom], points)
+ );
+ }
+
+ /**
+ * Get the coordinate (points attribute) from one of the polygon elements in the
+ * box model highlighter.
+ */
+ async _getPointsForRegion(region) {
+ const d = await this.getHighlighterNodeAttribute(
+ "box-model-" + region,
+ "d"
+ );
+
+ if (!d) {
+ return null;
+ }
+
+ const polygons = d.match(/M[^M]+/g);
+ if (!polygons) {
+ return null;
+ }
+
+ const points = polygons[0]
+ .trim()
+ .split(" ")
+ .map(i => {
+ return i.replace(/M|L/, "").split(",");
+ });
+
+ return {
+ p1: {
+ x: parseFloat(points[0][0]),
+ y: parseFloat(points[0][1]),
+ },
+ p2: {
+ x: parseFloat(points[1][0]),
+ y: parseFloat(points[1][1]),
+ },
+ p3: {
+ x: parseFloat(points[2][0]),
+ y: parseFloat(points[2][1]),
+ },
+ p4: {
+ x: parseFloat(points[3][0]),
+ y: parseFloat(points[3][1]),
+ },
+ };
+ }
+
+ /**
+ * Is a given region polygon element of the box-model highlighter currently
+ * hidden?
+ */
+ async _isRegionHidden(region) {
+ const value = await this.getHighlighterNodeAttribute(
+ "box-model-" + region,
+ "hidden"
+ );
+ return value !== null;
+ }
+
+ async _getGuideStatus(location) {
+ const id = "box-model-guide-" + location;
+
+ const hidden = await this.getHighlighterNodeAttribute(id, "hidden");
+ const x1 = await this.getHighlighterNodeAttribute(id, "x1");
+ const y1 = await this.getHighlighterNodeAttribute(id, "y1");
+ const x2 = await this.getHighlighterNodeAttribute(id, "x2");
+ const y2 = await this.getHighlighterNodeAttribute(id, "y2");
+
+ return {
+ visible: !hidden,
+ x1,
+ y1,
+ x2,
+ y2,
+ };
+ }
+
+ /**
+ * Get the coordinates of the rectangle that is defined by the 4 guides displayed
+ * in the toolbox box-model highlighter.
+ * @return {Object} Null if at least one guide is hidden. Otherwise an object
+ * with p1, p2, p3, p4 properties being {x, y} objects.
+ */
+ async getGuidesRectangle() {
+ const tGuide = await this._getGuideStatus("top");
+ const rGuide = await this._getGuideStatus("right");
+ const bGuide = await this._getGuideStatus("bottom");
+ const lGuide = await this._getGuideStatus("left");
+
+ if (
+ !tGuide.visible ||
+ !rGuide.visible ||
+ !bGuide.visible ||
+ !lGuide.visible
+ ) {
+ return null;
+ }
+
+ return {
+ p1: { x: lGuide.x1, y: tGuide.y1 },
+ p2: { x: +rGuide.x1 + 1, y: tGuide.y1 },
+ p3: { x: +rGuide.x1 + 1, y: +bGuide.y1 + 1 },
+ p4: { x: lGuide.x1, y: +bGuide.y1 + 1 },
+ };
+ }
+
+ /**
+ * Get the "d" attribute value for one of the box-model highlighter's region
+ * <path> elements, and parse it to a list of points.
+ * @param {String} region The box model region name.
+ * @param {Front} highlighter The front of the highlighter.
+ * @return {Object} The object returned has the following form:
+ * - d {String} the d attribute value
+ * - points {Array} an array of all the polygons defined by the path. Each box
+ * is itself an Array of points, themselves being [x,y] coordinates arrays.
+ */
+ async getHighlighterRegionPath(region, highlighter) {
+ const d = await this.getHighlighterNodeAttribute(
+ `box-model-${region}`,
+ "d",
+ highlighter
+ );
+ if (!d) {
+ return { d: null };
+ }
+
+ const polygons = d.match(/M[^M]+/g);
+ if (!polygons) {
+ return { d };
+ }
+
+ const points = [];
+ for (const polygon of polygons) {
+ points.push(
+ polygon
+ .trim()
+ .split(" ")
+ .map(i => {
+ return i.replace(/M|L/, "").split(",");
+ })
+ );
+ }
+
+ return { d, points };
+ }
+}
+protocol.registerFront(HighlighterTestFront);
+/**
+ * Check whether a point is included in a polygon.
+ * Taken and tweaked from:
+ * https://github.com/iominh/point-in-polygon-extended/blob/master/src/index.js#L30-L85
+ * @param {Array} point [x,y] coordinates
+ * @param {Array} polygon An array of [x,y] points
+ * @return {Boolean}
+ */
+function isInside(point, polygon) {
+ if (polygon.length === 0) {
+ return false;
+ }
+
+ // Reduce the length of the fractional part because this is likely to cause errors when
+ // the point is on the edge of the polygon.
+ point = point.map(n => n.toFixed(2));
+ polygon = polygon.map(p => p.map(n => n.toFixed(2)));
+
+ const n = polygon.length;
+ const newPoints = polygon.slice(0);
+ newPoints.push(polygon[0]);
+ let wn = 0;
+
+ // loop through all edges of the polygon
+ for (let i = 0; i < n; i++) {
+ // Accept points on the edges
+ const r = isLeft(newPoints[i], newPoints[i + 1], point);
+ if (r === 0) {
+ return true;
+ }
+ if (newPoints[i][1] <= point[1]) {
+ if (newPoints[i + 1][1] > point[1] && r > 0) {
+ wn++;
+ }
+ } else if (newPoints[i + 1][1] <= point[1] && r < 0) {
+ wn--;
+ }
+ }
+ if (wn === 0) {
+ dumpn(JSON.stringify(point) + " is outside of " + JSON.stringify(polygon));
+ }
+ // the point is outside only when this winding number wn===0, otherwise it's inside
+ return wn !== 0;
+}
+
+function isLeft(p0, p1, p2) {
+ const l =
+ (p1[0] - p0[0]) * (p2[1] - p0[1]) - (p2[0] - p0[0]) * (p1[1] - p0[1]);
+ return l;
+}