Bug 1568157 - Part 3: Replace `toolbox.highlighter` with the contextual HighlighterFr...
[gecko.git] / devtools / client / shared / test / test-actor.js
blob7763207c63f1d309b501aafb15badcb6477b178d
1 /* Any copyright is dedicated to the Public Domain.
2  http://creativecommons.org/publicdomain/zero/1.0/ */
4 /* exported TestActor, TestActorFront */
6 "use strict";
8 // A helper actor for inspector and markupview tests.
10 const { Ci, Cu } = require("chrome");
11 const Services = require("Services");
12 const {
13   getRect,
14   getAdjustedQuads,
15   getWindowDimensions,
16 } = require("devtools/shared/layout/utils");
17 const defer = require("devtools/shared/defer");
18 const {
19   isAuthorStylesheet,
20   getCSSStyleRules,
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",
38   EventUtils
41 const protocol = require("devtools/shared/protocol");
42 const { Arg, RetVal } = protocol;
44 const dumpn = msg => {
45   dump(msg + "\n");
48 /**
49  * Get the instance of CanvasFrameAnonymousContentHelper used by a given
50  * highlighter actor.
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
55  */
56 function getHighlighterCanvasFrameHelper(conn, actorID) {
57   const actor = conn.getActor(actorID);
58   if (actor && actor._highlighter) {
59     return actor._highlighter.markup;
60   }
61   return null;
64 var testSpec = protocol.generateActorSpec({
65   typeName: "testActor",
67   methods: {
68     getNumberOfElementMatches: {
69       request: {
70         selector: Arg(0, "string"),
71       },
72       response: {
73         value: RetVal("number"),
74       },
75     },
76     getHighlighterAttribute: {
77       request: {
78         nodeID: Arg(0, "string"),
79         name: Arg(1, "string"),
80         actorID: Arg(2, "string"),
81       },
82       response: {
83         value: RetVal("string"),
84       },
85     },
86     getHighlighterNodeTextContent: {
87       request: {
88         nodeID: Arg(0, "string"),
89         actorID: Arg(1, "string"),
90       },
91       response: {
92         value: RetVal("string"),
93       },
94     },
95     getSelectorHighlighterBoxNb: {
96       request: {
97         highlighter: Arg(0, "string"),
98       },
99       response: {
100         value: RetVal("number"),
101       },
102     },
103     changeHighlightedNodeWaitForUpdate: {
104       request: {
105         name: Arg(0, "string"),
106         value: Arg(1, "string"),
107         actorID: Arg(2, "string"),
108       },
109       response: {},
110     },
111     waitForHighlighterEvent: {
112       request: {
113         event: Arg(0, "string"),
114         actorID: Arg(1, "string"),
115       },
116       response: {},
117     },
118     waitForEventOnNode: {
119       request: {
120         eventName: Arg(0, "string"),
121         selector: Arg(1, "nullable:string"),
122       },
123       response: {},
124     },
125     changeZoomLevel: {
126       request: {
127         level: Arg(0, "string"),
128         actorID: Arg(1, "string"),
129       },
130       response: {},
131     },
132     getAllAdjustedQuads: {
133       request: {
134         selector: Arg(0, "string"),
135       },
136       response: {
137         value: RetVal("json"),
138       },
139     },
140     synthesizeMouse: {
141       request: {
142         object: Arg(0, "json"),
143       },
144       response: {},
145     },
146     synthesizeKey: {
147       request: {
148         args: Arg(0, "json"),
149       },
150       response: {},
151     },
152     scrollIntoView: {
153       request: {
154         args: Arg(0, "string"),
155       },
156       response: {},
157     },
158     hasPseudoClassLock: {
159       request: {
160         selector: Arg(0, "string"),
161         pseudo: Arg(1, "string"),
162       },
163       response: {
164         value: RetVal("boolean"),
165       },
166     },
167     loadAndWaitForCustomEvent: {
168       request: {
169         url: Arg(0, "string"),
170       },
171       response: {},
172     },
173     hasNode: {
174       request: {
175         selector: Arg(0, "string"),
176       },
177       response: {
178         value: RetVal("boolean"),
179       },
180     },
181     getBoundingClientRect: {
182       request: {
183         selector: Arg(0, "string"),
184       },
185       response: {
186         value: RetVal("json"),
187       },
188     },
189     setProperty: {
190       request: {
191         selector: Arg(0, "string"),
192         property: Arg(1, "string"),
193         value: Arg(2, "string"),
194       },
195       response: {},
196     },
197     getProperty: {
198       request: {
199         selector: Arg(0, "string"),
200         property: Arg(1, "string"),
201       },
202       response: {
203         value: RetVal("string"),
204       },
205     },
206     getAttribute: {
207       request: {
208         selector: Arg(0, "string"),
209         property: Arg(1, "string"),
210       },
211       response: {
212         value: RetVal("string"),
213       },
214     },
215     setAttribute: {
216       request: {
217         selector: Arg(0, "string"),
218         property: Arg(1, "string"),
219         value: Arg(2, "string"),
220       },
221       response: {},
222     },
223     removeAttribute: {
224       request: {
225         selector: Arg(0, "string"),
226         property: Arg(1, "string"),
227       },
228       response: {},
229     },
230     reload: {
231       request: {},
232       response: {},
233     },
234     reloadFrame: {
235       request: {
236         selector: Arg(0, "string"),
237       },
238       response: {},
239     },
240     eval: {
241       request: {
242         js: Arg(0, "string"),
243       },
244       response: {
245         value: RetVal("nullable:json"),
246       },
247     },
248     scrollWindow: {
249       request: {
250         x: Arg(0, "number"),
251         y: Arg(1, "number"),
252         relative: Arg(2, "nullable:boolean"),
253       },
254       response: {
255         value: RetVal("json"),
256       },
257     },
258     reflow: {},
259     getNodeRect: {
260       request: {
261         selector: Arg(0, "string"),
262       },
263       response: {
264         value: RetVal("json"),
265       },
266     },
267     getTextNodeRect: {
268       request: {
269         parentSelector: Arg(0, "string"),
270         childNodeIndex: Arg(1, "number"),
271       },
272       response: {
273         value: RetVal("json"),
274       },
275     },
276     getNodeInfo: {
277       request: {
278         selector: Arg(0, "string"),
279       },
280       response: {
281         value: RetVal("json"),
282       },
283     },
284     getStyleSheetsInfoForNode: {
285       request: {
286         selector: Arg(0, "string"),
287       },
288       response: {
289         value: RetVal("json"),
290       },
291     },
292     getWindowDimensions: {
293       request: {},
294       response: {
295         value: RetVal("json"),
296       },
297     },
298   },
301 var TestActor = (exports.TestActor = protocol.ActorClassWithSpec(testSpec, {
302   initialize: function(conn, targetActor, options) {
303     this.conn = conn;
304     this.targetActor = targetActor;
305   },
307   get content() {
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;
312     }
313     return this.targetActor.window;
314   },
316   /**
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.
322    */
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);
330         if (!iframe) {
331           throw new Error(
332             'Unable to find element with selector "' +
333               str +
334               '"' +
335               " (full selector:" +
336               fullSelector +
337               ")"
338           );
339         }
340         if (!iframe.contentWindow) {
341           throw new Error(
342             "Iframe selector doesn't target an iframe \"" +
343               str +
344               '"' +
345               " (full selector:" +
346               fullSelector +
347               ")"
348           );
349         }
350         document = iframe.contentWindow.document;
351       }
352       selector = selector.shift();
353     }
354     const node = document.querySelector(selector);
355     if (!node) {
356       throw new Error(
357         'Unable to find element with selector "' + selector + '"'
358       );
359     }
360     return node;
361   },
362   /**
363    * Helper to get the number of elements matching a selector
364    * @param {string} CSS selector.
365    */
366   getNumberOfElementMatches: function(selector, root = this.content.document) {
367     return root.querySelectorAll(selector).length;
368   },
370   /**
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
378    */
379   getHighlighterAttribute: function(nodeID, name, actorID) {
380     const helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
381     if (helper) {
382       return helper.getAttributeForElement(nodeID, name);
383     }
384     return null;
385   },
387   /**
388    * Get the textcontent of one of the elements of the box model highlighter,
389    * given its ID.
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
393    */
394   getHighlighterNodeTextContent: function(nodeID, actorID) {
395     let value;
396     const helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
397     if (helper) {
398       value = helper.getTextContentForElement(nodeID);
399     }
400     return value;
401   },
403   /**
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.
408    */
409   getSelectorHighlighterBoxNb: function(actorID) {
410     const highlighter = this.conn.getActor(actorID);
411     const { _highlighter: h } = highlighter;
412     if (!h || !h._highlighters) {
413       return null;
414     }
415     return h._highlighters.length;
416   },
418   /**
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
421    * updated.
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
425    */
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);
434     });
435   },
437   /**
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
441    */
442   waitForHighlighterEvent: function(event, actorID) {
443     const highlighter = this.conn.getActor(actorID);
444     const { _highlighter: h } = highlighter;
446     return h.once(event);
447   },
449   /**
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
454    */
455   waitForEventOnNode: function(eventName, selector) {
456     return new Promise(resolve => {
457       const node = selector ? this._querySelector(selector) : this.content;
458       node.addEventListener(
459         eventName,
460         function() {
461           resolve();
462         },
463         { once: true }
464       );
465     });
466   },
468   /**
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
474    */
475   changeZoomLevel: function(level, actorID) {
476     dumpn("Zooming page to " + level);
477     return new Promise(resolve => {
478       if (actorID) {
479         const actor = this.conn.getActor(actorID);
480         const { _highlighter: h } = actor;
481         h.once("updated", resolve);
482       } else {
483         resolve();
484       }
486       const docShell = this.content.docShell;
487       docShell.contentViewer.fullZoom = level;
488     });
489   },
491   /**
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
496    */
497   getAllAdjustedQuads: function(selector) {
498     const regions = {};
499     const node = this._querySelector(selector);
500     for (const boxType of ["content", "padding", "border", "margin"]) {
501       regions[boxType] = getAdjustedQuads(this.content, node, boxType);
502     }
504     return regions;
505   },
507   /**
508    * Get the window which mouse events on node should be delivered to.
509    */
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;
517     }
518     return node.ownerDocument.defaultView;
519   },
521   /**
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
527    * @param {Number} x
528    * @param {Number} y
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
532    */
533   synthesizeMouse: function({ selector, x, y, center, options }) {
534     const node = this._querySelector(selector);
535     node.scrollIntoView();
536     if (center) {
537       EventUtils.synthesizeMouseAtCenter(
538         node,
539         options,
540         this.windowForMouseEvent(node)
541       );
542     } else {
543       EventUtils.synthesizeMouse(
544         node,
545         x,
546         y,
547         options,
548         this.windowForMouseEvent(node)
549       );
550     }
551   },
553   /**
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.
557    */
558   synthesizeKey: function({ key, options, content }) {
559     EventUtils.synthesizeKey(key, options, this.content);
560   },
562   /**
563    * Scroll an element into view.
564    * @param {String} selector The selector for the node to scroll into view.
565    */
566   scrollIntoView: function(selector) {
567     const node = this._querySelector(selector);
568     node.scrollIntoView();
569   },
571   /**
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
575    * @return {Boolean}
576    */
577   hasPseudoClassLock: function(selector, pseudo) {
578     const node = this._querySelector(selector);
579     return InspectorUtils.hasPseudoClassLock(node, pseudo);
580   },
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(
587         "DOMWindowCreated",
588         () => {
589           this.content.addEventListener("test-page-processing-done", resolve, {
590             once: true,
591           });
592         },
593         { once: true }
594       );
596       this.content.location = url;
597     });
598   },
600   hasNode: function(selector) {
601     try {
602       // _querySelector throws if the node doesn't exists
603       this._querySelector(selector);
604       return true;
605     } catch (e) {
606       return false;
607     }
608   },
610   /**
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
614    */
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.
619     return {
620       x: rect.x,
621       y: rect.y,
622       width: rect.width,
623       height: rect.height,
624       top: rect.top,
625       right: rect.right,
626       bottom: rect.bottom,
627       left: rect.left,
628     };
629   },
631   /**
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
636    */
637   setProperty: function(selector, property, value) {
638     const node = this._querySelector(selector);
639     node[property] = value;
640   },
642   /**
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
647    */
648   getProperty: function(selector, property) {
649     const node = this._querySelector(selector);
650     return node[property];
651   },
653   /**
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
658    */
659   getAttribute: function(selector, attribute) {
660     const node = this._querySelector(selector);
661     return node.getAttribute(attribute);
662   },
664   /**
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
669    */
670   setAttribute: function(selector, attribute, value) {
671     const node = this._querySelector(selector);
672     node.setAttribute(attribute, value);
673   },
675   /**
676    * Remove an attribute from a DOM Node.
677    * @param {String} selector The node selector
678    * @param {String} attribute The attribute name
679    */
680   removeAttribute: function(selector, attribute) {
681     const node = this._querySelector(selector);
682     node.removeAttribute(attribute);
683   },
685   /**
686    * Reload the content window.
687    */
688   reload: function() {
689     this.content.location.reload();
690   },
692   /**
693    * Reload an iframe and wait for its load event.
694    * @param {String} selector The node selector
695    */
696   reloadFrame: function(selector) {
697     const node = this._querySelector(selector);
699     const deferred = defer();
701     const onLoad = function() {
702       node.removeEventListener("load", onLoad);
703       deferred.resolve();
704     };
705     node.addEventListener("load", onLoad);
707     node.contentWindow.location.reload();
708     return deferred.promise;
709   },
711   /**
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
715    */
716   eval: function(js) {
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") {
723       return null;
724     } else if (typeof result == "object") {
725       return JSON.parse(JSON.stringify(result));
726     }
727     return result;
728   },
730   /**
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`.
733    *
734    * @param {Number} x
735    * @param {Number} y
736    * @param {Boolean} relative
737    *
738    * @return {Object} An object with x / y properties, representing the number
739    * of pixels that the document has been scrolled horizontally and vertically.
740    */
741   scrollWindow: function(x, y, relative) {
742     if (isNaN(x) || isNaN(y)) {
743       return {};
744     }
746     const deferred = defer();
747     this.content.addEventListener(
748       "scroll",
749       function(event) {
750         const data = { x: this.content.scrollX, y: this.content.scrollY };
751         deferred.resolve(data);
752       },
753       { once: true }
754     );
756     this.content[relative ? "scrollBy" : "scrollTo"](x, y);
758     return deferred.promise;
759   },
761   /**
762    * Forces the reflow and waits for the next repaint.
763    */
764   reflow: function() {
765     const deferred = defer();
766     this.content.document.documentElement.offsetWidth;
767     this.content.requestAnimationFrame(deferred.resolve);
769     return deferred.promise;
770   },
772   async getNodeRect(selector) {
773     const node = this._querySelector(selector);
774     return getRect(this.content, node, this.content);
775   },
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;
781   },
783   /**
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.
795    */
796   getNodeInfo: function(selector) {
797     const node = this._querySelector(selector);
798     let info = null;
800     if (node) {
801       info = {
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 };
809           }
810         ),
811         outerHTML: node.outerHTML,
812         innerHTML: node.innerHTML,
813         textContent: node.textContent,
814       };
815     }
817     return info;
818   },
820   /**
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:
826    * - {String} href.
827    * - {Boolean} isContentSheet.
828    */
829   getStyleSheetsInfoForNode: function(selector) {
830     const node = this._querySelector(selector);
831     const domRules = getCSSStyleRules(node);
833     const sheets = [];
835     for (let i = 0, n = domRules.length; i < n; i++) {
836       const sheet = domRules[i].parentStyleSheet;
837       sheets.push({
838         href: sheet.href,
839         isContentSheet: isAuthorStylesheet(sheet),
840       });
841     }
843     return sheets;
844   },
846   /**
847    * Returns the window's dimensions for the `window` given.
848    *
849    * @return {Object} An object with `width` and `height` properties, representing the
850    * number of pixels for the document's size.
851    */
852   getWindowDimensions: function() {
853     return getWindowDimensions(this.content);
854   },
855 }));
857 class TestActorFront extends protocol.FrontClassWithSpec(testSpec) {
858   constructor(client, highlighter) {
859     super(client);
860     this.highlighter = highlighter;
861   }
863   /**
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.
869    */
870   zoomPageTo(level, actorID = this.highlighter.actorID) {
871     return this.changeZoomLevel(level, actorID);
872   }
874   /* eslint-disable max-len */
875   changeHighlightedNodeWaitForUpdate(name, value, highlighter) {
876     /* eslint-enable max-len */
877     return super.changeHighlightedNodeWaitForUpdate(
878       name,
879       value,
880       (highlighter || this.highlighter).actorID
881     );
882   }
884   /**
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
890    */
891   getHighlighterNodeAttribute(nodeID, name, highlighter) {
892     return this.getHighlighterAttribute(
893       nodeID,
894       name,
895       (highlighter || this.highlighter).actorID
896     );
897   }
899   getHighlighterNodeTextContent(nodeID, highlighter) {
900     return super.getHighlighterNodeTextContent(
901       nodeID,
902       (highlighter || this.highlighter).actorID
903     );
904   }
906   /**
907    * Is the highlighter currently visible on the page?
908    */
909   isHighlighting() {
910     return this.getHighlighterNodeAttribute(
911       "box-model-elements",
912       "hidden"
913     ).then(value => value === null);
914   }
916   /**
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
922    * console.
923    */
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) {
933         is(
934           boxModel[boxType].points[point].x,
935           quad[point].x,
936           prefix + boxType + " point " + point + " x coordinate is correct"
937         );
938         is(
939           boxModel[boxType].points[point].y,
940           quad[point].y,
941           prefix + boxType + " point " + point + " y coordinate is correct"
942         );
943       }
944     }
945   }
947   /**
948    * Get the current rect of the border region of the box-model highlighter
949    */
950   async getSimpleBorderRect() {
951     const { border } = await this._getBoxModelStatus();
952     const { p1, p2, p4 } = border.points;
954     return {
955       top: p1.y,
956       left: p1.x,
957       width: p2.x - p1.x,
958       height: p4.y - p1.y,
959     };
960   }
962   /**
963    * Get the current positions and visibility of the various box-model highlighter
964    * elements.
965    */
966   async _getBoxModelStatus() {
967     const isVisible = await this.isHighlighting();
969     const ret = {
970       visible: isVisible,
971     };
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 };
977     }
979     ret.guides = {};
980     for (const guide of ["top", "right", "bottom", "left"]) {
981       ret.guides[guide] = await this._getGuideStatus(guide);
982     }
984     return ret;
985   }
987   /**
988    * Check that the box-model highlighter is currently highlighting the node matching the
989    * given selector.
990    * @param {String} selector
991    * @return {Boolean}
992    */
993   async assertHighlightedNode(selector) {
994     const rect = await this.getNodeRect(selector);
995     return this.isNodeRectHighlighted(rect);
996   }
998   /**
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
1004    * @return {Boolean}
1005    */
1006   async assertHighlightedTextNode(parentSelector, childNodeIndex) {
1007     const rect = await this.getTextNodeRect(parentSelector, childNodeIndex);
1008     return this.isNodeRectHighlighted(rect);
1009   }
1011   /**
1012    * Check that the box-model highlighter is currently highlighting the given rect.
1013    * @param {Object} rect
1014    * @return {Boolean}
1015    */
1016   async isNodeRectHighlighted({ left, top, width, height }) {
1017     const { visible, border } = await this._getBoxModelStatus();
1018     let points = border.points;
1019     if (!visible) {
1020       return false;
1021     }
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
1028     const list = [];
1029     for (let i = 1; i <= 4; i++) {
1030       const p = points["p" + i];
1031       list.push([p.x, p.y]);
1032     }
1033     points = list;
1035     // Check that each point of the node is within the box model
1036     return (
1037       isInside([left, top], points) &&
1038       isInside([right, top], points) &&
1039       isInside([right, bottom], points) &&
1040       isInside([left, bottom], points)
1041     );
1042   }
1044   /**
1045    * Get the coordinate (points attribute) from one of the polygon elements in the
1046    * box model highlighter.
1047    */
1048   async _getPointsForRegion(region) {
1049     const d = await this.getHighlighterNodeAttribute(
1050       "box-model-" + region,
1051       "d"
1052     );
1054     const polygons = d.match(/M[^M]+/g);
1055     if (!polygons) {
1056       return null;
1057     }
1059     const points = polygons[0]
1060       .trim()
1061       .split(" ")
1062       .map(i => {
1063         return i.replace(/M|L/, "").split(",");
1064       });
1066     return {
1067       p1: {
1068         x: parseFloat(points[0][0]),
1069         y: parseFloat(points[0][1]),
1070       },
1071       p2: {
1072         x: parseFloat(points[1][0]),
1073         y: parseFloat(points[1][1]),
1074       },
1075       p3: {
1076         x: parseFloat(points[2][0]),
1077         y: parseFloat(points[2][1]),
1078       },
1079       p4: {
1080         x: parseFloat(points[3][0]),
1081         y: parseFloat(points[3][1]),
1082       },
1083     };
1084   }
1086   /**
1087    * Is a given region polygon element of the box-model highlighter currently
1088    * hidden?
1089    */
1090   async _isRegionHidden(region) {
1091     const value = await this.getHighlighterNodeAttribute(
1092       "box-model-" + region,
1093       "hidden"
1094     );
1095     return value !== null;
1096   }
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");
1107     return {
1108       visible: !hidden,
1109       x1: x1,
1110       y1: y1,
1111       x2: x2,
1112       y2: y2,
1113     };
1114   }
1116   /**
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.
1121    */
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");
1128     if (
1129       !tGuide.visible ||
1130       !rGuide.visible ||
1131       !bGuide.visible ||
1132       !lGuide.visible
1133     ) {
1134       return null;
1135     }
1137     return {
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 },
1142     };
1143   }
1145   waitForHighlighterEvent(event) {
1146     return super.waitForHighlighterEvent(event, this.highlighter.actorID);
1147   }
1149   /**
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.
1158    */
1159   async getHighlighterRegionPath(region, highlighter) {
1160     const d = await this.getHighlighterNodeAttribute(
1161       `box-model-${region}`,
1162       "d",
1163       highlighter
1164     );
1165     if (!d) {
1166       return { d: null };
1167     }
1169     const polygons = d.match(/M[^M]+/g);
1170     if (!polygons) {
1171       return { d };
1172     }
1174     const points = [];
1175     for (const polygon of polygons) {
1176       points.push(
1177         polygon
1178           .trim()
1179           .split(" ")
1180           .map(i => {
1181             return i.replace(/M|L/, "").split(",");
1182           })
1183       );
1184     }
1186     return { d, points };
1187   }
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
1197  * @return {Boolean}
1198  */
1199 function isInside(point, polygon) {
1200   if (polygon.length === 0) {
1201     return false;
1202   }
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]);
1212   let wn = 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);
1218     if (r === 0) {
1219       return true;
1220     }
1221     if (newPoints[i][1] <= point[1]) {
1222       if (newPoints[i + 1][1] > point[1] && r > 0) {
1223         wn++;
1224       }
1225     } else if (newPoints[i + 1][1] <= point[1] && r < 0) {
1226       wn--;
1227     }
1228   }
1229   if (wn === 0) {
1230     dumpn(JSON.stringify(point) + " is outside of " + JSON.stringify(polygon));
1231   }
1232   // the point is outside only when this winding number wn===0, otherwise it's inside
1233   return wn !== 0;
1236 function isLeft(p0, p1, p2) {
1237   const l =
1238     (p1[0] - p0[0]) * (p2[1] - p0[1]) - (p2[0] - p0[0]) * (p1[1] - p0[1]);
1239   return l;