summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/test/test-actor.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/test/test-actor.js')
-rw-r--r--devtools/client/shared/test/test-actor.js1138
1 files changed, 1138 insertions, 0 deletions
diff --git a/devtools/client/shared/test/test-actor.js b/devtools/client/shared/test/test-actor.js
new file mode 100644
index 000000000..3aab5287b
--- /dev/null
+++ b/devtools/client/shared/test/test-actor.js
@@ -0,0 +1,1138 @@
+/* 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
+ * <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.
+ */
+ 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;
+}