/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ /* exported TestActor, TestActorFront */ "use strict"; // A helper actor for inspector and markupview tests. const { Cc, Ci, Cu } = require("chrome"); const {getRect, getElementFromPoint, getAdjustedQuads} = require("devtools/shared/layout/utils"); const defer = require("devtools/shared/defer"); const {Task} = require("devtools/shared/task"); const {isContentStylesheet} = require("devtools/shared/inspector/css-logic"); const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); const loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] .getService(Ci.mozIJSSubScriptLoader); // 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. let EventUtils = {}; EventUtils.window = {}; EventUtils.parent = {}; /* eslint-disable camelcase */ EventUtils._EU_Ci = Components.interfaces; EventUtils._EU_Cc = Components.classes; /* eslint-disable camelcase */ loader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils); const protocol = require("devtools/shared/protocol"); 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) { let actor = conn.getActor(actorID); if (actor && actor._highlighter) { return actor._highlighter.markup; } return null; } var testSpec = protocol.generateActorSpec({ typeName: "testActor", methods: { getNumberOfElementMatches: { request: { selector: Arg(0, "string"), }, response: { value: RetVal("number") } }, getHighlighterAttribute: { request: { nodeID: Arg(0, "string"), name: 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: {} }, waitForHighlighterEvent: { request: { event: Arg(0, "string"), actorID: Arg(1, "string") }, response: {} }, waitForEventOnNode: { request: { eventName: Arg(0, "string"), selector: Arg(1, "nullable:string") }, response: {} }, changeZoomLevel: { request: { level: Arg(0, "string"), actorID: Arg(1, "string"), }, response: {} }, assertElementAtPoint: { request: { x: Arg(0, "number"), y: Arg(1, "number"), selector: Arg(2, "string") }, response: { value: RetVal("boolean") } }, getAllAdjustedQuads: { request: { selector: Arg(0, "string") }, response: { value: RetVal("json") } }, synthesizeMouse: { request: { object: Arg(0, "json") }, response: {} }, synthesizeKey: { request: { args: Arg(0, "json") }, response: {} }, scrollIntoView: { request: { args: Arg(0, "string") }, response: {} }, hasPseudoClassLock: { request: { selector: Arg(0, "string"), pseudo: Arg(1, "string") }, response: { value: RetVal("boolean") } }, loadAndWaitForCustomEvent: { request: { url: Arg(0, "string") }, response: {} }, hasNode: { request: { selector: Arg(0, "string") }, response: { value: RetVal("boolean") } }, getBoundingClientRect: { request: { selector: Arg(0, "string"), }, response: { value: RetVal("json") } }, setProperty: { request: { selector: Arg(0, "string"), property: Arg(1, "string"), value: Arg(2, "string") }, response: {} }, getProperty: { request: { selector: Arg(0, "string"), property: Arg(1, "string") }, response: { value: RetVal("string") } }, getAttribute: { request: { selector: Arg(0, "string"), property: Arg(1, "string") }, response: { value: RetVal("string") } }, setAttribute: { request: { selector: Arg(0, "string"), property: Arg(1, "string"), value: Arg(2, "string") }, response: {} }, removeAttribute: { request: { selector: Arg(0, "string"), property: Arg(1, "string") }, response: {} }, reload: { request: {}, response: {} }, reloadFrame: { request: { selector: Arg(0, "string"), }, response: {} }, eval: { request: { js: Arg(0, "string") }, response: { value: RetVal("nullable:json") } }, scrollWindow: { request: { x: Arg(0, "number"), y: Arg(1, "number"), relative: Arg(2, "nullable:boolean"), }, response: { value: RetVal("json") } }, reflow: {}, getNodeRect: { request: { selector: Arg(0, "string") }, response: { value: RetVal("json") } }, getTextNodeRect: { request: { parentSelector: Arg(0, "string"), childNodeIndex: Arg(1, "number") }, response: { value: RetVal("json") } }, getNodeInfo: { request: { selector: Arg(0, "string") }, response: { value: RetVal("json") } }, getStyleSheetsInfoForNode: { request: { selector: Arg(0, "string") }, response: { value: RetVal("json") } } } }); var TestActor = exports.TestActor = protocol.ActorClassWithSpec(testSpec, { initialize: function (conn, tabActor, options) { this.conn = conn; this.tabActor = tabActor; }, get content() { return this.tabActor.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: function (selector) { let document = this.content.document; if (Array.isArray(selector)) { let fullSelector = selector.join(" >> "); while (selector.length > 1) { let str = selector.shift(); let 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(); } let node = document.querySelector(selector); if (!node) { throw new Error("Unable to find element with selector \"" + selector + "\""); } return node; }, /** * Helper to get the number of elements matching a selector * @param {string} CSS selector. */ getNumberOfElementMatches: function (selector, root = this.content.document) { return root.querySelectorAll(selector).length; }, /** * Get a value for a given attribute name, on one of the elements of the box * model highlighter, given its ID. * @param {Object} msg The msg.data part expects the following properties * - {String} nodeID The full ID of the element to get the attribute for * - {String} name The name of the attribute to get * - {String} actorID The highlighter actor ID * @return {String} The value, if found, null otherwise */ getHighlighterAttribute: function (nodeID, name, actorID) { let helper = getHighlighterCanvasFrameHelper(this.conn, actorID); if (helper) { return helper.getAttributeForElement(nodeID, name); } return null; }, /** * 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: function (nodeID, actorID) { let value; let 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: function (actorID) { let highlighter = this.conn.getActor(actorID); let {_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: function (name, value, actorID) { return new Promise(resolve => { let highlighter = this.conn.getActor(actorID); let {_highlighter: h} = highlighter; h.once("updated", resolve); h.currentNode.setAttribute(name, value); }); }, /** * Subscribe to a given highlighter event and respond when the event is received. * @param {String} event The name of the highlighter event to listen to * @param {String} actorID The highlighter actor ID */ waitForHighlighterEvent: function (event, actorID) { let highlighter = this.conn.getActor(actorID); let {_highlighter: h} = highlighter; return h.once(event); }, /** * Wait for a specific event on a node matching the provided selector. * @param {String} eventName The name of the event to listen to * @param {String} selector Optional: css selector of the node which should * trigger the event. If ommitted, target will be the content window */ waitForEventOnNode: function (eventName, selector) { return new Promise(resolve => { let node = selector ? this._querySelector(selector) : this.content; node.addEventListener(eventName, function onEvent() { node.removeEventListener(eventName, onEvent); resolve(); }); }); }, /** * Change the zoom level of the page. * Optionally subscribe to the box-model highlighter's update event and waiting * for it to refresh before responding. * @param {Number} level The new zoom level * @param {String} actorID Optional. The highlighter actor ID */ changeZoomLevel: function (level, actorID) { dumpn("Zooming page to " + level); return new Promise(resolve => { if (actorID) { let actor = this.conn.getActor(actorID); let {_highlighter: h} = actor; h.once("updated", resolve); } else { resolve(); } let docShell = this.content.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShell); docShell.contentViewer.fullZoom = level; }); }, assertElementAtPoint: function (x, y, selector) { let elementAtPoint = getElementFromPoint(this.content.document, x, y); if (!elementAtPoint) { throw new Error("Unable to find element at (" + x + ", " + y + ")"); } let node = this._querySelector(selector); return node == elementAtPoint; }, /** * Get all box-model regions' adjusted boxquads for the given element * @param {String} selector The node selector to target a given element * @return {Object} An object with each property being a box-model region, each * of them being an object with the p1/p2/p3/p4 properties */ getAllAdjustedQuads: function (selector) { let regions = {}; let node = this._querySelector(selector); for (let boxType of ["content", "padding", "border", "margin"]) { regions[boxType] = getAdjustedQuads(this.content, node, boxType); } return regions; }, /** * Synthesize a mouse event on an element, after ensuring that it is visible * in the viewport. This handler doesn't send a message back. Consumers * should listen to specific events on the inspector/highlighter to know when * the event got synthesized. * @param {String} selector The node selector to get the node target for the event * @param {Number} x * @param {Number} y * @param {Boolean} center If set to true, x/y will be ignored and * synthesizeMouseAtCenter will be used instead * @param {Object} options Other event options */ synthesizeMouse: function ({ selector, x, y, center, options }) { let node = this._querySelector(selector); node.scrollIntoView(); if (center) { EventUtils.synthesizeMouseAtCenter(node, options, node.ownerDocument.defaultView); } else { EventUtils.synthesizeMouse(node, x, y, options, node.ownerDocument.defaultView); } }, /** * Synthesize a key event for an element. This handler doesn't send a message * back. Consumers should listen to specific events on the inspector/highlighter * to know when the event got synthesized. */ synthesizeKey: function ({key, options, content}) { EventUtils.synthesizeKey(key, options, this.content); }, /** * Scroll an element into view. * @param {String} selector The selector for the node to scroll into view. */ scrollIntoView: function (selector) { let node = this._querySelector(selector); node.scrollIntoView(); }, /** * Check that an element currently has a pseudo-class lock. * @param {String} selector The node selector to get the pseudo-class from * @param {String} pseudo The pseudoclass to check for * @return {Boolean} */ hasPseudoClassLock: function (selector, pseudo) { let node = this._querySelector(selector); return DOMUtils.hasPseudoClassLock(node, pseudo); }, loadAndWaitForCustomEvent: function (url) { return new Promise(resolve => { // Wait for DOMWindowCreated first, as listening on the current outerwindow // doesn't allow receiving test-page-processing-done. this.tabActor.chromeEventHandler.addEventListener("DOMWindowCreated", () => { this.content.addEventListener( "test-page-processing-done", resolve, { once: true } ); }, { once: true }); this.content.location = url; }); }, hasNode: function (selector) { try { // _querySelector throws if the node doesn't exists this._querySelector(selector); return true; } catch (e) { return false; } }, /** * Get the bounding rect for a given DOM node once. * @param {String} selector selector identifier to select the DOM node * @return {json} the bounding rect info */ getBoundingClientRect: function (selector) { let node = this._querySelector(selector); let rect = node.getBoundingClientRect(); // DOMRect can't be stringified directly, so return a simple object instead. return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left }; }, /** * Set a JS property on a DOM Node. * @param {String} selector The node selector * @param {String} property The property name * @param {String} value The attribute value */ setProperty: function (selector, property, value) { let node = this._querySelector(selector); node[property] = value; }, /** * Get a JS property on a DOM Node. * @param {String} selector The node selector * @param {String} property The property name * @return {String} value The attribute value */ getProperty: function (selector, property) { let node = this._querySelector(selector); return node[property]; }, /** * Get an attribute on a DOM Node. * @param {String} selector The node selector * @param {String} attribute The attribute name * @return {String} value The attribute value */ getAttribute: function (selector, attribute) { let node = this._querySelector(selector); return node.getAttribute(attribute); }, /** * Set an attribute on a DOM Node. * @param {String} selector The node selector * @param {String} attribute The attribute name * @param {String} value The attribute value */ setAttribute: function (selector, attribute, value) { let node = this._querySelector(selector); node.setAttribute(attribute, value); }, /** * Remove an attribute from a DOM Node. * @param {String} selector The node selector * @param {String} attribute The attribute name */ removeAttribute: function (selector, attribute) { let node = this._querySelector(selector); node.removeAttribute(attribute); }, /** * Reload the content window. */ reload: function () { this.content.location.reload(); }, /** * Reload an iframe and wait for its load event. * @param {String} selector The node selector */ reloadFrame: function (selector) { let node = this._querySelector(selector); let deferred = defer(); let onLoad = function () { node.removeEventListener("load", onLoad); deferred.resolve(); }; node.addEventListener("load", onLoad); node.contentWindow.location.reload(); return deferred.promise; }, /** * Evaluate a JS string in the context of the content document. * @param {String} js JS string to evaluate * @return {json} The evaluation result */ eval: function (js) { // We have to use a sandbox, as CSP prevent us from using eval on apps... let sb = Cu.Sandbox(this.content, { sandboxPrototype: this.content }); return Cu.evalInSandbox(js, sb); }, /** * Scrolls the window to a particular set of coordinates in the document, or * by the given amount if `relative` is set to `true`. * * @param {Number} x * @param {Number} y * @param {Boolean} relative * * @return {Object} An object with x / y properties, representing the number * of pixels that the document has been scrolled horizontally and vertically. */ scrollWindow: function (x, y, relative) { if (isNaN(x) || isNaN(y)) { return {}; } let deferred = defer(); this.content.addEventListener("scroll", function onScroll(event) { this.removeEventListener("scroll", onScroll); let data = {x: this.content.scrollX, y: this.content.scrollY}; deferred.resolve(data); }); this.content[relative ? "scrollBy" : "scrollTo"](x, y); return deferred.promise; }, /** * Forces the reflow and waits for the next repaint. */ reflow: function () { let deferred = defer(); this.content.document.documentElement.offsetWidth; this.content.requestAnimationFrame(deferred.resolve); return deferred.promise; }, getNodeRect: Task.async(function* (selector) { let node = this._querySelector(selector); return getRect(this.content, node, this.content); }), getTextNodeRect: Task.async(function* (parentSelector, childNodeIndex) { let parentNode = this._querySelector(parentSelector); let node = parentNode.childNodes[childNodeIndex]; return getAdjustedQuads(this.content, node)[0].bounds; }), /** * Get information about a DOM element, identified by a selector. * @param {String} selector The CSS selector to get the node (can be an array * of selectors to get elements in an iframe). * @return {Object} data Null if selector didn't match any node, otherwise: * - {String} tagName. * - {String} namespaceURI. * - {Number} numChildren The number of children in the element. * - {Array} attributes An array of {name, value, namespaceURI} objects. * - {String} outerHTML. * - {String} innerHTML. * - {String} textContent. */ getNodeInfo: function (selector) { let node = this._querySelector(selector); let info = null; if (node) { info = { tagName: node.tagName, namespaceURI: node.namespaceURI, numChildren: node.children.length, numNodes: node.childNodes.length, attributes: [...node.attributes].map(({name, value, namespaceURI}) => { return {name, value, namespaceURI}; }), outerHTML: node.outerHTML, innerHTML: node.innerHTML, textContent: node.textContent }; } return info; }, /** * Get information about the stylesheets which have CSS rules that apply to a given DOM * element, identified by a selector. * @param {String} selector The CSS selector to get the node (can be an array * of selectors to get elements in an iframe). * @return {Array} A list of stylesheet objects, each having the following properties: * - {String} href. * - {Boolean} isContentSheet. */ getStyleSheetsInfoForNode: function (selector) { let node = this._querySelector(selector); let domRules = DOMUtils.getCSSStyleRules(node); let sheets = []; for (let i = 0, n = domRules.Count(); i < n; i++) { let sheet = domRules.GetElementAt(i).parentStyleSheet; sheets.push({ href: sheet.href, isContentSheet: isContentStylesheet(sheet) }); } return sheets; } }); var TestActorFront = exports.TestActorFront = protocol.FrontClassWithSpec(testSpec, { initialize: function (client, { testActor }, toolbox) { protocol.Front.prototype.initialize.call(this, client, { actor: testActor }); this.manage(this); this.toolbox = toolbox; }, /** * Zoom the current page to a given level. * @param {Number} level The new zoom level. * @return {Promise} The returned promise will only resolve when the * highlighter has updated to the new zoom level. */ zoomPageTo: function (level) { return this.changeZoomLevel(level, this.toolbox.highlighter.actorID); }, /* eslint-disable max-len */ changeHighlightedNodeWaitForUpdate: protocol.custom(function (name, value, highlighter) { /* eslint-enable max-len */ return this._changeHighlightedNodeWaitForUpdate( name, value, (highlighter || this.toolbox.highlighter).actorID ); }, { impl: "_changeHighlightedNodeWaitForUpdate" }), /** * 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 highlither to target * @return {String} value */ getHighlighterNodeAttribute: function (nodeID, name, highlighter) { return this.getHighlighterAttribute( nodeID, name, (highlighter || this.toolbox.highlighter).actorID ); }, getHighlighterNodeTextContent: protocol.custom(function (nodeID, highlighter) { return this._getHighlighterNodeTextContent( nodeID, (highlighter || this.toolbox.highlighter).actorID ); }, { impl: "_getHighlighterNodeTextContent" }), /** * Is the highlighter currently visible on the page? */ isHighlighting: function () { return this.getHighlighterNodeAttribute("box-model-elements", "hidden") .then(value => value === null); }, /** * Assert that the box-model highlighter's current position corresponds to the * given node boxquads. * @param {String} selector The node selector to get the boxQuads from * @param {Function} is assertion function to call for equality checks * @param {String} prefix An optional prefix for logging information to the * console. */ isNodeCorrectlyHighlighted: Task.async(function* (selector, is, prefix = "") { prefix += (prefix ? " " : "") + selector + " "; let boxModel = yield this._getBoxModelStatus(); let regions = yield this.getAllAdjustedQuads(selector); for (let boxType of ["content", "padding", "border", "margin"]) { let [quad] = regions[boxType]; for (let point in boxModel[boxType].points) { is(boxModel[boxType].points[point].x, quad[point].x, prefix + boxType + " point " + point + " x coordinate is correct"); is(boxModel[boxType].points[point].y, quad[point].y, prefix + boxType + " point " + point + " y coordinate is correct"); } } }), /** * Get the current rect of the border region of the box-model highlighter */ getSimpleBorderRect: Task.async(function* (toolbox) { let {border} = yield this._getBoxModelStatus(toolbox); let {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. */ _getBoxModelStatus: Task.async(function* () { let isVisible = yield this.isHighlighting(); let ret = { visible: isVisible }; for (let region of ["margin", "border", "padding", "content"]) { let points = yield this._getPointsForRegion(region); let visible = yield this._isRegionHidden(region); ret[region] = {points, visible}; } ret.guides = {}; for (let guide of ["top", "right", "bottom", "left"]) { ret.guides[guide] = yield 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} */ assertHighlightedNode: Task.async(function* (selector) { let rect = yield this.getNodeRect(selector); return yield 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} */ assertHighlightedTextNode: Task.async(function* (parentSelector, childNodeIndex) { let rect = yield this.getTextNodeRect(parentSelector, childNodeIndex); return yield this.isNodeRectHighlighted(rect); }), /** * Check that the box-model highlighter is currently highlighting the given rect. * @param {Object} rect * @return {Boolean} */ isNodeRectHighlighted: Task.async(function* ({ left, top, width, height }) { let {visible, border} = yield this._getBoxModelStatus(); let points = border.points; if (!visible) { return false; } // Check that the node is within the box model let right = left + width; let bottom = top + height; // Converts points dictionnary into an array let list = []; for (let i = 1; i <= 4; i++) { let 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. */ _getPointsForRegion: Task.async(function* (region) { let d = yield this.getHighlighterNodeAttribute("box-model-" + region, "d"); let polygons = d.match(/M[^M]+/g); if (!polygons) { return null; } let 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? */ _isRegionHidden: Task.async(function* (region) { let value = yield this.getHighlighterNodeAttribute("box-model-" + region, "hidden"); return value !== null; }), _getGuideStatus: Task.async(function* (location) { let id = "box-model-guide-" + location; let hidden = yield this.getHighlighterNodeAttribute(id, "hidden"); let x1 = yield this.getHighlighterNodeAttribute(id, "x1"); let y1 = yield this.getHighlighterNodeAttribute(id, "y1"); let x2 = yield this.getHighlighterNodeAttribute(id, "x2"); let y2 = yield this.getHighlighterNodeAttribute(id, "y2"); return { visible: !hidden, x1: x1, y1: y1, x2: x2, y2: 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. */ getGuidesRectangle: Task.async(function* () { let tGuide = yield this._getGuideStatus("top"); let rGuide = yield this._getGuideStatus("right"); let bGuide = yield this._getGuideStatus("bottom"); let lGuide = yield 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, y: tGuide. y1}, p3: {x: rGuide.x1, y: bGuide.y1}, p4: {x: lGuide.x1, y: bGuide.y1} }; }), waitForHighlighterEvent: protocol.custom(function (event) { return this._waitForHighlighterEvent(event, this.toolbox.highlighter.actorID); }, { impl: "_waitForHighlighterEvent" }), /** * Get the "d" attribute value for one of the box-model highlighter's region * 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. */ getHighlighterRegionPath: Task.async(function* (region, highlighter) { let d = yield this.getHighlighterNodeAttribute( `box-model-${region}`, "d", highlighter ); if (!d) { return {d: null}; } let polygons = d.match(/M[^M]+/g); if (!polygons) { return {d}; } let points = []; for (let polygon of polygons) { points.push(polygon.trim().split(" ").map(i => { return i.replace(/M|L/, "").split(","); })); } return {d, points}; }) }); /** * 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; } 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 let 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) { let l = ((p1[0] - p0[0]) * (p2[1] - p0[1])) - ((p2[0] - p0[0]) * (p1[1] - p0[1])); return l; }