1 /* Any copyright is dedicated to the Public Domain.
2 http://creativecommons.org/publicdomain/zero/1.0/ */
4 /* exported TestActor, TestActorFront */
8 // A helper actor for inspector and markupview tests.
10 const { Ci, Cu } = require("chrome");
11 const Services = require("Services");
16 } = require("devtools/shared/layout/utils");
17 const defer = require("devtools/shared/defer");
21 } = require("devtools/shared/inspector/css-logic");
22 const InspectorUtils = require("InspectorUtils");
23 const Debugger = require("Debugger");
24 const ReplayInspector = require("devtools/server/actors/replay/inspector");
26 // Set up a dummy environment so that EventUtils works. We need to be careful to
27 // pass a window object into each EventUtils method we call rather than having
28 // it rely on the |window| global.
29 const EventUtils = {};
30 EventUtils.window = {};
31 EventUtils.parent = {};
32 /* eslint-disable camelcase */
33 EventUtils._EU_Ci = Ci;
34 EventUtils._EU_Cc = Cc;
35 /* eslint-disable camelcase */
36 Services.scriptloader.loadSubScript(
37 "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
41 const protocol = require("devtools/shared/protocol");
42 const { Arg, RetVal } = protocol;
44 const dumpn = msg => {
49 * Get the instance of CanvasFrameAnonymousContentHelper used by a given
51 * The instance provides methods to get/set attributes/text/style on nodes of
52 * the highlighter, inserted into the nsCanvasFrame.
53 * @see /devtools/server/actors/highlighters.js
54 * @param {String} actorID
56 function getHighlighterCanvasFrameHelper(conn, actorID) {
57 const actor = conn.getActor(actorID);
58 if (actor && actor._highlighter) {
59 return actor._highlighter.markup;
64 var testSpec = protocol.generateActorSpec({
65 typeName: "testActor",
68 getNumberOfElementMatches: {
70 selector: Arg(0, "string"),
73 value: RetVal("number"),
76 getHighlighterAttribute: {
78 nodeID: Arg(0, "string"),
79 name: Arg(1, "string"),
80 actorID: Arg(2, "string"),
83 value: RetVal("string"),
86 getHighlighterNodeTextContent: {
88 nodeID: Arg(0, "string"),
89 actorID: Arg(1, "string"),
92 value: RetVal("string"),
95 getSelectorHighlighterBoxNb: {
97 highlighter: Arg(0, "string"),
100 value: RetVal("number"),
103 changeHighlightedNodeWaitForUpdate: {
105 name: Arg(0, "string"),
106 value: Arg(1, "string"),
107 actorID: Arg(2, "string"),
111 waitForHighlighterEvent: {
113 event: Arg(0, "string"),
114 actorID: Arg(1, "string"),
118 waitForEventOnNode: {
120 eventName: Arg(0, "string"),
121 selector: Arg(1, "nullable:string"),
127 level: Arg(0, "string"),
128 actorID: Arg(1, "string"),
132 getAllAdjustedQuads: {
134 selector: Arg(0, "string"),
137 value: RetVal("json"),
142 object: Arg(0, "json"),
148 args: Arg(0, "json"),
154 args: Arg(0, "string"),
158 hasPseudoClassLock: {
160 selector: Arg(0, "string"),
161 pseudo: Arg(1, "string"),
164 value: RetVal("boolean"),
167 loadAndWaitForCustomEvent: {
169 url: Arg(0, "string"),
175 selector: Arg(0, "string"),
178 value: RetVal("boolean"),
181 getBoundingClientRect: {
183 selector: Arg(0, "string"),
186 value: RetVal("json"),
191 selector: Arg(0, "string"),
192 property: Arg(1, "string"),
193 value: Arg(2, "string"),
199 selector: Arg(0, "string"),
200 property: Arg(1, "string"),
203 value: RetVal("string"),
208 selector: Arg(0, "string"),
209 property: Arg(1, "string"),
212 value: RetVal("string"),
217 selector: Arg(0, "string"),
218 property: Arg(1, "string"),
219 value: Arg(2, "string"),
225 selector: Arg(0, "string"),
226 property: Arg(1, "string"),
236 selector: Arg(0, "string"),
242 js: Arg(0, "string"),
245 value: RetVal("nullable:json"),
252 relative: Arg(2, "nullable:boolean"),
255 value: RetVal("json"),
261 selector: Arg(0, "string"),
264 value: RetVal("json"),
269 parentSelector: Arg(0, "string"),
270 childNodeIndex: Arg(1, "number"),
273 value: RetVal("json"),
278 selector: Arg(0, "string"),
281 value: RetVal("json"),
284 getStyleSheetsInfoForNode: {
286 selector: Arg(0, "string"),
289 value: RetVal("json"),
292 getWindowDimensions: {
295 value: RetVal("json"),
301 var TestActor = (exports.TestActor = protocol.ActorClassWithSpec(testSpec, {
302 initialize: function(conn, targetActor, options) {
304 this.targetActor = targetActor;
308 // When replaying, the content window is in the replaying process. We can't
309 // use isReplaying here because this actor is loaded into its own sandbox.
310 if (Debugger.recordReplayProcessKind() == "Middleman") {
311 return ReplayInspector.window;
313 return this.targetActor.window;
317 * Helper to retrieve a DOM element.
318 * @param {string | array} selector Either a regular selector string
319 * or a selector array. If an array, each item, except the last one
320 * are considered matching an iframe, so that we can query element
321 * within deep iframes.
323 _querySelector: function(selector) {
324 let document = this.content.document;
325 if (Array.isArray(selector)) {
326 const fullSelector = selector.join(" >> ");
327 while (selector.length > 1) {
328 const str = selector.shift();
329 const iframe = document.querySelector(str);
332 'Unable to find element with selector "' +
340 if (!iframe.contentWindow) {
342 "Iframe selector doesn't target an iframe \"" +
350 document = iframe.contentWindow.document;
352 selector = selector.shift();
354 const node = document.querySelector(selector);
357 'Unable to find element with selector "' + selector + '"'
363 * Helper to get the number of elements matching a selector
364 * @param {string} CSS selector.
366 getNumberOfElementMatches: function(selector, root = this.content.document) {
367 return root.querySelectorAll(selector).length;
371 * Get a value for a given attribute name, on one of the elements of the box
372 * model highlighter, given its ID.
373 * @param {Object} msg The msg.data part expects the following properties
374 * - {String} nodeID The full ID of the element to get the attribute for
375 * - {String} name The name of the attribute to get
376 * - {String} actorID The highlighter actor ID
377 * @return {String} The value, if found, null otherwise
379 getHighlighterAttribute: function(nodeID, name, actorID) {
380 const helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
382 return helper.getAttributeForElement(nodeID, name);
388 * Get the textcontent of one of the elements of the box model highlighter,
390 * @param {String} nodeID The full ID of the element to get the attribute for
391 * @param {String} actorID The highlighter actor ID
392 * @return {String} The textcontent value
394 getHighlighterNodeTextContent: function(nodeID, actorID) {
396 const helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
398 value = helper.getTextContentForElement(nodeID);
404 * Get the number of box-model highlighters created by the SelectorHighlighter
405 * @param {String} actorID The highlighter actor ID
406 * @return {Number} The number of box-model highlighters created, or null if the
407 * SelectorHighlighter was not found.
409 getSelectorHighlighterBoxNb: function(actorID) {
410 const highlighter = this.conn.getActor(actorID);
411 const { _highlighter: h } = highlighter;
412 if (!h || !h._highlighters) {
415 return h._highlighters.length;
419 * Subscribe to the box-model highlighter's update event, modify an attribute of
420 * the currently highlighted node and send a message when the highlighter has
422 * @param {String} the name of the attribute to be changed
423 * @param {String} the new value for the attribute
424 * @param {String} actorID The highlighter actor ID
426 changeHighlightedNodeWaitForUpdate: function(name, value, actorID) {
427 return new Promise(resolve => {
428 const highlighter = this.conn.getActor(actorID);
429 const { _highlighter: h } = highlighter;
431 h.once("updated", resolve);
433 h.currentNode.setAttribute(name, value);
438 * Subscribe to a given highlighter event and respond when the event is received.
439 * @param {String} event The name of the highlighter event to listen to
440 * @param {String} actorID The highlighter actor ID
442 waitForHighlighterEvent: function(event, actorID) {
443 const highlighter = this.conn.getActor(actorID);
444 const { _highlighter: h } = highlighter;
446 return h.once(event);
450 * Wait for a specific event on a node matching the provided selector.
451 * @param {String} eventName The name of the event to listen to
452 * @param {String} selector Optional: css selector of the node which should
453 * trigger the event. If ommitted, target will be the content window
455 waitForEventOnNode: function(eventName, selector) {
456 return new Promise(resolve => {
457 const node = selector ? this._querySelector(selector) : this.content;
458 node.addEventListener(
469 * Change the zoom level of the page.
470 * Optionally subscribe to the box-model highlighter's update event and waiting
471 * for it to refresh before responding.
472 * @param {Number} level The new zoom level
473 * @param {String} actorID Optional. The highlighter actor ID
475 changeZoomLevel: function(level, actorID) {
476 dumpn("Zooming page to " + level);
477 return new Promise(resolve => {
479 const actor = this.conn.getActor(actorID);
480 const { _highlighter: h } = actor;
481 h.once("updated", resolve);
486 const docShell = this.content.docShell;
487 docShell.contentViewer.fullZoom = level;
492 * Get all box-model regions' adjusted boxquads for the given element
493 * @param {String} selector The node selector to target a given element
494 * @return {Object} An object with each property being a box-model region, each
495 * of them being an object with the p1/p2/p3/p4 properties
497 getAllAdjustedQuads: function(selector) {
499 const node = this._querySelector(selector);
500 for (const boxType of ["content", "padding", "border", "margin"]) {
501 regions[boxType] = getAdjustedQuads(this.content, node, boxType);
508 * Get the window which mouse events on node should be delivered to.
510 windowForMouseEvent: function(node) {
511 // When replaying, the node is a proxy for an element in the replaying
512 // process. Use the window which the server is running against, which is
513 // able to receive events. We can't use isReplaying here because this actor
514 // is loaded into its own sandbox.
515 if (Debugger.recordReplayProcessKind() == "Middleman") {
516 return this.targetActor.window;
518 return node.ownerDocument.defaultView;
522 * Synthesize a mouse event on an element, after ensuring that it is visible
523 * in the viewport. This handler doesn't send a message back. Consumers
524 * should listen to specific events on the inspector/highlighter to know when
525 * the event got synthesized.
526 * @param {String} selector The node selector to get the node target for the event
529 * @param {Boolean} center If set to true, x/y will be ignored and
530 * synthesizeMouseAtCenter will be used instead
531 * @param {Object} options Other event options
533 synthesizeMouse: function({ selector, x, y, center, options }) {
534 const node = this._querySelector(selector);
535 node.scrollIntoView();
537 EventUtils.synthesizeMouseAtCenter(
540 this.windowForMouseEvent(node)
543 EventUtils.synthesizeMouse(
548 this.windowForMouseEvent(node)
554 * Synthesize a key event for an element. This handler doesn't send a message
555 * back. Consumers should listen to specific events on the inspector/highlighter
556 * to know when the event got synthesized.
558 synthesizeKey: function({ key, options, content }) {
559 EventUtils.synthesizeKey(key, options, this.content);
563 * Scroll an element into view.
564 * @param {String} selector The selector for the node to scroll into view.
566 scrollIntoView: function(selector) {
567 const node = this._querySelector(selector);
568 node.scrollIntoView();
572 * Check that an element currently has a pseudo-class lock.
573 * @param {String} selector The node selector to get the pseudo-class from
574 * @param {String} pseudo The pseudoclass to check for
577 hasPseudoClassLock: function(selector, pseudo) {
578 const node = this._querySelector(selector);
579 return InspectorUtils.hasPseudoClassLock(node, pseudo);
582 loadAndWaitForCustomEvent: function(url) {
583 return new Promise(resolve => {
584 // Wait for DOMWindowCreated first, as listening on the current outerwindow
585 // doesn't allow receiving test-page-processing-done.
586 this.targetActor.chromeEventHandler.addEventListener(
589 this.content.addEventListener("test-page-processing-done", resolve, {
596 this.content.location = url;
600 hasNode: function(selector) {
602 // _querySelector throws if the node doesn't exists
603 this._querySelector(selector);
611 * Get the bounding rect for a given DOM node once.
612 * @param {String} selector selector identifier to select the DOM node
613 * @return {json} the bounding rect info
615 getBoundingClientRect: function(selector) {
616 const node = this._querySelector(selector);
617 const rect = node.getBoundingClientRect();
618 // DOMRect can't be stringified directly, so return a simple object instead.
632 * Set a JS property on a DOM Node.
633 * @param {String} selector The node selector
634 * @param {String} property The property name
635 * @param {String} value The attribute value
637 setProperty: function(selector, property, value) {
638 const node = this._querySelector(selector);
639 node[property] = value;
643 * Get a JS property on a DOM Node.
644 * @param {String} selector The node selector
645 * @param {String} property The property name
646 * @return {String} value The attribute value
648 getProperty: function(selector, property) {
649 const node = this._querySelector(selector);
650 return node[property];
654 * Get an attribute on a DOM Node.
655 * @param {String} selector The node selector
656 * @param {String} attribute The attribute name
657 * @return {String} value The attribute value
659 getAttribute: function(selector, attribute) {
660 const node = this._querySelector(selector);
661 return node.getAttribute(attribute);
665 * Set an attribute on a DOM Node.
666 * @param {String} selector The node selector
667 * @param {String} attribute The attribute name
668 * @param {String} value The attribute value
670 setAttribute: function(selector, attribute, value) {
671 const node = this._querySelector(selector);
672 node.setAttribute(attribute, value);
676 * Remove an attribute from a DOM Node.
677 * @param {String} selector The node selector
678 * @param {String} attribute The attribute name
680 removeAttribute: function(selector, attribute) {
681 const node = this._querySelector(selector);
682 node.removeAttribute(attribute);
686 * Reload the content window.
689 this.content.location.reload();
693 * Reload an iframe and wait for its load event.
694 * @param {String} selector The node selector
696 reloadFrame: function(selector) {
697 const node = this._querySelector(selector);
699 const deferred = defer();
701 const onLoad = function() {
702 node.removeEventListener("load", onLoad);
705 node.addEventListener("load", onLoad);
707 node.contentWindow.location.reload();
708 return deferred.promise;
712 * Evaluate a JS string in the context of the content document.
713 * @param {String} js JS string to evaluate
714 * @return {json} The evaluation result
717 // We have to use a sandbox, as CSP prevent us from using eval on apps...
718 const sb = Cu.Sandbox(this.content, { sandboxPrototype: this.content });
719 const result = Cu.evalInSandbox(js, sb);
721 // Ensure passing only serializable data to RDP
722 if (typeof result == "function") {
724 } else if (typeof result == "object") {
725 return JSON.parse(JSON.stringify(result));
731 * Scrolls the window to a particular set of coordinates in the document, or
732 * by the given amount if `relative` is set to `true`.
736 * @param {Boolean} relative
738 * @return {Object} An object with x / y properties, representing the number
739 * of pixels that the document has been scrolled horizontally and vertically.
741 scrollWindow: function(x, y, relative) {
742 if (isNaN(x) || isNaN(y)) {
746 const deferred = defer();
747 this.content.addEventListener(
750 const data = { x: this.content.scrollX, y: this.content.scrollY };
751 deferred.resolve(data);
756 this.content[relative ? "scrollBy" : "scrollTo"](x, y);
758 return deferred.promise;
762 * Forces the reflow and waits for the next repaint.
765 const deferred = defer();
766 this.content.document.documentElement.offsetWidth;
767 this.content.requestAnimationFrame(deferred.resolve);
769 return deferred.promise;
772 async getNodeRect(selector) {
773 const node = this._querySelector(selector);
774 return getRect(this.content, node, this.content);
777 async getTextNodeRect(parentSelector, childNodeIndex) {
778 const parentNode = this._querySelector(parentSelector);
779 const node = parentNode.childNodes[childNodeIndex];
780 return getAdjustedQuads(this.content, node)[0].bounds;
784 * Get information about a DOM element, identified by a selector.
785 * @param {String} selector The CSS selector to get the node (can be an array
786 * of selectors to get elements in an iframe).
787 * @return {Object} data Null if selector didn't match any node, otherwise:
788 * - {String} tagName.
789 * - {String} namespaceURI.
790 * - {Number} numChildren The number of children in the element.
791 * - {Array} attributes An array of {name, value, namespaceURI} objects.
792 * - {String} outerHTML.
793 * - {String} innerHTML.
794 * - {String} textContent.
796 getNodeInfo: function(selector) {
797 const node = this._querySelector(selector);
802 tagName: node.tagName,
803 namespaceURI: node.namespaceURI,
804 numChildren: node.children.length,
805 numNodes: node.childNodes.length,
806 attributes: [...node.attributes].map(
807 ({ name, value, namespaceURI }) => {
808 return { name, value, namespaceURI };
811 outerHTML: node.outerHTML,
812 innerHTML: node.innerHTML,
813 textContent: node.textContent,
821 * Get information about the stylesheets which have CSS rules that apply to a given DOM
822 * element, identified by a selector.
823 * @param {String} selector The CSS selector to get the node (can be an array
824 * of selectors to get elements in an iframe).
825 * @return {Array} A list of stylesheet objects, each having the following properties:
827 * - {Boolean} isContentSheet.
829 getStyleSheetsInfoForNode: function(selector) {
830 const node = this._querySelector(selector);
831 const domRules = getCSSStyleRules(node);
835 for (let i = 0, n = domRules.length; i < n; i++) {
836 const sheet = domRules[i].parentStyleSheet;
839 isContentSheet: isAuthorStylesheet(sheet),
847 * Returns the window's dimensions for the `window` given.
849 * @return {Object} An object with `width` and `height` properties, representing the
850 * number of pixels for the document's size.
852 getWindowDimensions: function() {
853 return getWindowDimensions(this.content);
857 class TestActorFront extends protocol.FrontClassWithSpec(testSpec) {
858 constructor(client, highlighter) {
860 this.highlighter = highlighter;
864 * Zoom the current page to a given level.
865 * @param {Number} level The new zoom level.
866 * @param {String} actorID Optional. The highlighter actor ID.
867 * @return {Promise} The returned promise will only resolve when the
868 * highlighter has updated to the new zoom level.
870 zoomPageTo(level, actorID = this.highlighter.actorID) {
871 return this.changeZoomLevel(level, actorID);
874 /* eslint-disable max-len */
875 changeHighlightedNodeWaitForUpdate(name, value, highlighter) {
876 /* eslint-enable max-len */
877 return super.changeHighlightedNodeWaitForUpdate(
880 (highlighter || this.highlighter).actorID
885 * Get the value of an attribute on one of the highlighter's node.
886 * @param {String} nodeID The Id of the node in the highlighter.
887 * @param {String} name The name of the attribute.
888 * @param {Object} highlighter Optional custom highlither to target
889 * @return {String} value
891 getHighlighterNodeAttribute(nodeID, name, highlighter) {
892 return this.getHighlighterAttribute(
895 (highlighter || this.highlighter).actorID
899 getHighlighterNodeTextContent(nodeID, highlighter) {
900 return super.getHighlighterNodeTextContent(
902 (highlighter || this.highlighter).actorID
907 * Is the highlighter currently visible on the page?
910 return this.getHighlighterNodeAttribute(
911 "box-model-elements",
913 ).then(value => value === null);
917 * Assert that the box-model highlighter's current position corresponds to the
918 * given node boxquads.
919 * @param {String} selector The node selector to get the boxQuads from
920 * @param {Function} is assertion function to call for equality checks
921 * @param {String} prefix An optional prefix for logging information to the
924 async isNodeCorrectlyHighlighted(selector, is, prefix = "") {
925 prefix += (prefix ? " " : "") + selector + " ";
927 const boxModel = await this._getBoxModelStatus();
928 const regions = await this.getAllAdjustedQuads(selector);
930 for (const boxType of ["content", "padding", "border", "margin"]) {
931 const [quad] = regions[boxType];
932 for (const point in boxModel[boxType].points) {
934 boxModel[boxType].points[point].x,
936 prefix + boxType + " point " + point + " x coordinate is correct"
939 boxModel[boxType].points[point].y,
941 prefix + boxType + " point " + point + " y coordinate is correct"
948 * Get the current rect of the border region of the box-model highlighter
950 async getSimpleBorderRect() {
951 const { border } = await this._getBoxModelStatus();
952 const { p1, p2, p4 } = border.points;
963 * Get the current positions and visibility of the various box-model highlighter
966 async _getBoxModelStatus() {
967 const isVisible = await this.isHighlighting();
973 for (const region of ["margin", "border", "padding", "content"]) {
974 const points = await this._getPointsForRegion(region);
975 const visible = await this._isRegionHidden(region);
976 ret[region] = { points, visible };
980 for (const guide of ["top", "right", "bottom", "left"]) {
981 ret.guides[guide] = await this._getGuideStatus(guide);
988 * Check that the box-model highlighter is currently highlighting the node matching the
990 * @param {String} selector
993 async assertHighlightedNode(selector) {
994 const rect = await this.getNodeRect(selector);
995 return this.isNodeRectHighlighted(rect);
999 * Check that the box-model highlighter is currently highlighting the text node that can
1000 * be found at a given index within the list of childNodes of a parent element matching
1001 * the given selector.
1002 * @param {String} parentSelector
1003 * @param {Number} childNodeIndex
1006 async assertHighlightedTextNode(parentSelector, childNodeIndex) {
1007 const rect = await this.getTextNodeRect(parentSelector, childNodeIndex);
1008 return this.isNodeRectHighlighted(rect);
1012 * Check that the box-model highlighter is currently highlighting the given rect.
1013 * @param {Object} rect
1016 async isNodeRectHighlighted({ left, top, width, height }) {
1017 const { visible, border } = await this._getBoxModelStatus();
1018 let points = border.points;
1023 // Check that the node is within the box model
1024 const right = left + width;
1025 const bottom = top + height;
1027 // Converts points dictionnary into an array
1029 for (let i = 1; i <= 4; i++) {
1030 const p = points["p" + i];
1031 list.push([p.x, p.y]);
1035 // Check that each point of the node is within the box model
1037 isInside([left, top], points) &&
1038 isInside([right, top], points) &&
1039 isInside([right, bottom], points) &&
1040 isInside([left, bottom], points)
1045 * Get the coordinate (points attribute) from one of the polygon elements in the
1046 * box model highlighter.
1048 async _getPointsForRegion(region) {
1049 const d = await this.getHighlighterNodeAttribute(
1050 "box-model-" + region,
1054 const polygons = d.match(/M[^M]+/g);
1059 const points = polygons[0]
1063 return i.replace(/M|L/, "").split(",");
1068 x: parseFloat(points[0][0]),
1069 y: parseFloat(points[0][1]),
1072 x: parseFloat(points[1][0]),
1073 y: parseFloat(points[1][1]),
1076 x: parseFloat(points[2][0]),
1077 y: parseFloat(points[2][1]),
1080 x: parseFloat(points[3][0]),
1081 y: parseFloat(points[3][1]),
1087 * Is a given region polygon element of the box-model highlighter currently
1090 async _isRegionHidden(region) {
1091 const value = await this.getHighlighterNodeAttribute(
1092 "box-model-" + region,
1095 return value !== null;
1098 async _getGuideStatus(location) {
1099 const id = "box-model-guide-" + location;
1101 const hidden = await this.getHighlighterNodeAttribute(id, "hidden");
1102 const x1 = await this.getHighlighterNodeAttribute(id, "x1");
1103 const y1 = await this.getHighlighterNodeAttribute(id, "y1");
1104 const x2 = await this.getHighlighterNodeAttribute(id, "x2");
1105 const y2 = await this.getHighlighterNodeAttribute(id, "y2");
1117 * Get the coordinates of the rectangle that is defined by the 4 guides displayed
1118 * in the toolbox box-model highlighter.
1119 * @return {Object} Null if at least one guide is hidden. Otherwise an object
1120 * with p1, p2, p3, p4 properties being {x, y} objects.
1122 async getGuidesRectangle() {
1123 const tGuide = await this._getGuideStatus("top");
1124 const rGuide = await this._getGuideStatus("right");
1125 const bGuide = await this._getGuideStatus("bottom");
1126 const lGuide = await this._getGuideStatus("left");
1138 p1: { x: lGuide.x1, y: tGuide.y1 },
1139 p2: { x: +rGuide.x1 + 1, y: tGuide.y1 },
1140 p3: { x: +rGuide.x1 + 1, y: +bGuide.y1 + 1 },
1141 p4: { x: lGuide.x1, y: +bGuide.y1 + 1 },
1145 waitForHighlighterEvent(event) {
1146 return super.waitForHighlighterEvent(event, this.highlighter.actorID);
1150 * Get the "d" attribute value for one of the box-model highlighter's region
1151 * <path> elements, and parse it to a list of points.
1152 * @param {String} region The box model region name.
1153 * @param {Front} highlighter The front of the highlighter.
1154 * @return {Object} The object returned has the following form:
1155 * - d {String} the d attribute value
1156 * - points {Array} an array of all the polygons defined by the path. Each box
1157 * is itself an Array of points, themselves being [x,y] coordinates arrays.
1159 async getHighlighterRegionPath(region, highlighter) {
1160 const d = await this.getHighlighterNodeAttribute(
1161 `box-model-${region}`,
1169 const polygons = d.match(/M[^M]+/g);
1175 for (const polygon of polygons) {
1181 return i.replace(/M|L/, "").split(",");
1186 return { d, points };
1189 exports.TestActorFront = TestActorFront;
1192 * Check whether a point is included in a polygon.
1193 * Taken and tweaked from:
1194 * https://github.com/iominh/point-in-polygon-extended/blob/master/src/index.js#L30-L85
1195 * @param {Array} point [x,y] coordinates
1196 * @param {Array} polygon An array of [x,y] points
1199 function isInside(point, polygon) {
1200 if (polygon.length === 0) {
1204 // Reduce the length of the fractional part because this is likely to cause errors when
1205 // the point is on the edge of the polygon.
1206 point = point.map(n => n.toFixed(2));
1207 polygon = polygon.map(p => p.map(n => n.toFixed(2)));
1209 const n = polygon.length;
1210 const newPoints = polygon.slice(0);
1211 newPoints.push(polygon[0]);
1214 // loop through all edges of the polygon
1215 for (let i = 0; i < n; i++) {
1216 // Accept points on the edges
1217 const r = isLeft(newPoints[i], newPoints[i + 1], point);
1221 if (newPoints[i][1] <= point[1]) {
1222 if (newPoints[i + 1][1] > point[1] && r > 0) {
1225 } else if (newPoints[i + 1][1] <= point[1] && r < 0) {
1230 dumpn(JSON.stringify(point) + " is outside of " + JSON.stringify(polygon));
1232 // the point is outside only when this winding number wn===0, otherwise it's inside
1236 function isLeft(p0, p1, p2) {
1238 (p1[0] - p0[0]) * (p2[1] - p0[1]) - (p2[0] - p0[0]) * (p1[1] - p0[1]);