tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

highlighter-test-actor.js (27307B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 /* exported HighlighterTestActor, HighlighterTestFront */
      5 
      6 "use strict";
      7 
      8 // A helper actor for testing highlighters.
      9 // ⚠️ This should only be used for getting data for objects using CanvasFrameAnonymousContentHelper,
     10 // that we can't get directly from tests.
     11 const {
     12  getRect,
     13  getAdjustedQuads,
     14 } = require("resource://devtools/shared/layout/utils.js");
     15 
     16 // Set up a dummy environment so that EventUtils works. We need to be careful to
     17 // pass a window object into each EventUtils method we call rather than having
     18 // it rely on the |window| global.
     19 const EventUtils = {};
     20 EventUtils.window = {};
     21 EventUtils.parent = {};
     22 /* eslint-disable camelcase */
     23 EventUtils._EU_Ci = Ci;
     24 EventUtils._EU_Cc = Cc;
     25 /* eslint-disable camelcase */
     26 Services.scriptloader.loadSubScript(
     27  "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
     28  EventUtils
     29 );
     30 
     31 // We're an actor so we don't run in the browser test environment, so
     32 // we need to import TestUtils manually despite what the linter thinks.
     33 // eslint-disable-next-line mozilla/no-redeclare-with-import-autofix
     34 const { TestUtils } = ChromeUtils.importESModule(
     35  "resource://testing-common/TestUtils.sys.mjs"
     36 );
     37 
     38 const protocol = require("resource://devtools/shared/protocol.js");
     39 const { Arg, RetVal } = protocol;
     40 
     41 const dumpn = msg => {
     42  dump(msg + "\n");
     43 };
     44 
     45 /**
     46 * Get the instance of CanvasFrameAnonymousContentHelper used by a given
     47 * highlighter actor.
     48 * The instance provides methods to get/set attributes/text/style on nodes of
     49 * the highlighter, inserted into the nsCanvasFrame.
     50 *
     51 * @see /devtools/server/actors/highlighters.js
     52 * @param {string} actorID
     53 */
     54 function getHighlighterCanvasFrameHelper(conn, actorID) {
     55  // Retrieve the CustomHighlighterActor by its actorID:
     56  const actor = conn.getActor(actorID);
     57  if (!actor) {
     58    return null;
     59  }
     60 
     61  // Retrieve the sub class instance specific to each highlighter type:
     62  let highlighter = actor.instance;
     63 
     64  // SelectorHighlighter and TabbingOrderHighlighter can hold multiple highlighters.
     65  // For now, only retrieve the first highlighter.
     66  if (
     67    highlighter._highlighters &&
     68    Array.isArray(highlighter._highlighters) &&
     69    highlighter._highlighters.length
     70  ) {
     71    highlighter = highlighter._highlighters[0];
     72  }
     73 
     74  // Now, `highlighter` should be a final highlighter class, exposing
     75  // `CanvasFrameAnonymousContentHelper` via a `markup` attribute.
     76  if (highlighter.markup) {
     77    return highlighter.markup;
     78  }
     79 
     80  // Here we didn't find any highlighter; it can happen if the actor is a
     81  // FontsHighlighter (which does not use a CanvasFrameAnonymousContentHelper).
     82  return null;
     83 }
     84 
     85 var highlighterTestSpec = protocol.generateActorSpec({
     86  typeName: "highlighterTest",
     87 
     88  events: {
     89    "highlighter-updated": {},
     90  },
     91 
     92  methods: {
     93    getHighlighterAttribute: {
     94      request: {
     95        nodeID: Arg(0, "string"),
     96        name: Arg(1, "string"),
     97        actorID: Arg(2, "string"),
     98      },
     99      response: {
    100        value: RetVal("string"),
    101      },
    102    },
    103    getHighlighterBoundingClientRect: {
    104      request: {
    105        nodeID: Arg(0, "string"),
    106        actorID: Arg(1, "string"),
    107      },
    108      response: {
    109        value: RetVal("json"),
    110      },
    111    },
    112    getHighlighterComputedStyle: {
    113      request: {
    114        nodeID: Arg(0, "string"),
    115        property: Arg(1, "string"),
    116        actorID: Arg(2, "string"),
    117      },
    118      response: {
    119        value: RetVal("string"),
    120      },
    121    },
    122    getHighlighterNodeTextContent: {
    123      request: {
    124        nodeID: Arg(0, "string"),
    125        actorID: Arg(1, "string"),
    126      },
    127      response: {
    128        value: RetVal("string"),
    129      },
    130    },
    131    getSelectorHighlighterBoxNb: {
    132      request: {
    133        highlighter: Arg(0, "string"),
    134      },
    135      response: {
    136        value: RetVal("number"),
    137      },
    138    },
    139    changeHighlightedNodeWaitForUpdate: {
    140      request: {
    141        name: Arg(0, "string"),
    142        value: Arg(1, "string"),
    143        actorID: Arg(2, "string"),
    144      },
    145      response: {},
    146    },
    147    registerOneTimeHighlighterUpdate: {
    148      request: {
    149        actorID: Arg(0, "string"),
    150      },
    151      response: {},
    152    },
    153    getNodeRect: {
    154      request: {
    155        selector: Arg(0, "string"),
    156      },
    157      response: {
    158        value: RetVal("json"),
    159      },
    160    },
    161    getTextNodeRect: {
    162      request: {
    163        parentSelector: Arg(0, "string"),
    164        childNodeIndex: Arg(1, "number"),
    165      },
    166      response: {
    167        value: RetVal("json"),
    168      },
    169    },
    170    isPausedDebuggerOverlayVisible: {
    171      request: {},
    172      response: {
    173        value: RetVal("boolean"),
    174      },
    175    },
    176    clickPausedDebuggerOverlayButton: {
    177      request: {
    178        id: Arg(0, "string"),
    179      },
    180      response: {},
    181    },
    182    isEyeDropperVisible: {
    183      request: {},
    184      response: {
    185        value: RetVal("boolean"),
    186      },
    187    },
    188    getEyeDropperElementAttribute: {
    189      request: {
    190        elementId: Arg(0, "string"),
    191        attributeName: Arg(1, "string"),
    192      },
    193      response: {
    194        value: RetVal("string"),
    195      },
    196    },
    197    getEyeDropperColorValue: {
    198      request: {},
    199      response: {
    200        value: RetVal("string"),
    201      },
    202    },
    203    getTabbingOrderHighlighterData: {
    204      request: {},
    205      response: {
    206        value: RetVal("json"),
    207      },
    208    },
    209  },
    210 });
    211 
    212 class HighlighterTestActor extends protocol.Actor {
    213  constructor(conn, targetActor) {
    214    super(conn, highlighterTestSpec);
    215 
    216    this.targetActor = targetActor;
    217  }
    218 
    219  get content() {
    220    return this.targetActor.window;
    221  }
    222 
    223  /**
    224   * Helper to retrieve a DOM element.
    225   *
    226   * @param {string | Array} selector Either a regular selector string
    227   *   or a selector array. If an array, each item, except the last one
    228   *   are considered matching an iframe, so that we can query element
    229   *   within deep iframes.
    230   */
    231  _querySelector(selector) {
    232    let document = this.content.document;
    233    if (Array.isArray(selector)) {
    234      const fullSelector = selector.join(" >> ");
    235      while (selector.length > 1) {
    236        const str = selector.shift();
    237        const iframe = document.querySelector(str);
    238        if (!iframe) {
    239          throw new Error(
    240            'Unable to find element with selector "' +
    241              str +
    242              '"' +
    243              " (full selector:" +
    244              fullSelector +
    245              ")"
    246          );
    247        }
    248        if (!iframe.contentWindow) {
    249          throw new Error(
    250            "Iframe selector doesn't target an iframe \"" +
    251              str +
    252              '"' +
    253              " (full selector:" +
    254              fullSelector +
    255              ")"
    256          );
    257        }
    258        document = iframe.contentWindow.document;
    259      }
    260      selector = selector.shift();
    261    }
    262    const node = document.querySelector(selector);
    263    if (!node) {
    264      throw new Error(
    265        'Unable to find element with selector "' + selector + '"'
    266      );
    267    }
    268    return node;
    269  }
    270 
    271  /**
    272   * Get a value for a given attribute name, on one of the elements of the box
    273   * model highlighter, given its ID.
    274   *
    275   * @param {string} nodeID The full ID of the element to get the attribute for
    276   * @param {string} name The name of the attribute to get
    277   * @param {string} actorID The highlighter actor ID
    278   * @return {string} The value, if found, null otherwise
    279   */
    280  getHighlighterAttribute(nodeID, name, actorID) {
    281    const helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
    282 
    283    if (!helper) {
    284      throw new Error(`Highlighter not found`);
    285    }
    286 
    287    return helper.getAttributeForElement(nodeID, name);
    288  }
    289 
    290  /**
    291   * Get the bounding client rect for an highlighter element, given its ID.
    292   *
    293   * @param {string} nodeID The full ID of the element to get the DOMRect for
    294   * @param {string} actorID The highlighter actor ID
    295   * @return {DOMRect} The value, if found, null otherwise
    296   */
    297  getHighlighterBoundingClientRect(nodeID, actorID) {
    298    const helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
    299 
    300    if (!helper) {
    301      throw new Error(`Highlighter not found`);
    302    }
    303 
    304    return helper.getBoundingClientRect(nodeID);
    305  }
    306 
    307  /**
    308   * Get the computed style for a given property, on one of the elements of the
    309   * box model highlighter, given its ID.
    310   *
    311   * @param {string} nodeID The full ID of the element to get the attribute for
    312   * @param {string} property The name of the property
    313   * @param {string} actorID The highlighter actor ID
    314   * @return {string} The computed style of the property
    315   */
    316  getHighlighterComputedStyle(nodeID, property, actorID) {
    317    const helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
    318 
    319    if (!helper) {
    320      throw new Error(`Highlighter not found`);
    321    }
    322 
    323    return helper.getElement(nodeID).computedStyle.getPropertyValue(property);
    324  }
    325 
    326  /**
    327   * Get the textcontent of one of the elements of the box model highlighter,
    328   * given its ID.
    329   *
    330   * @param {string} nodeID The full ID of the element to get the attribute for
    331   * @param {string} actorID The highlighter actor ID
    332   * @return {string} The textcontent value
    333   */
    334  getHighlighterNodeTextContent(nodeID, actorID) {
    335    let value;
    336    const helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
    337    if (helper) {
    338      value = helper.getTextContentForElement(nodeID);
    339    }
    340    return value;
    341  }
    342 
    343  /**
    344   * Get the number of box-model highlighters created by the SelectorHighlighter
    345   *
    346   * @param {string} actorID The highlighter actor ID
    347   * @return {number} The number of box-model highlighters created, or null if the
    348   * SelectorHighlighter was not found.
    349   */
    350  getSelectorHighlighterBoxNb(actorID) {
    351    const highlighter = this.conn.getActor(actorID);
    352    const { _highlighter: h } = highlighter;
    353    if (!h || !h._highlighters) {
    354      return null;
    355    }
    356    return h._highlighters.length;
    357  }
    358 
    359  /**
    360   * Subscribe to the box-model highlighter's update event, modify an attribute of
    361   * the currently highlighted node and send a message when the highlighter has
    362   * updated.
    363   *
    364   * @param {string} the name of the attribute to be changed
    365   * @param {string} the new value for the attribute
    366   * @param {string} actorID The highlighter actor ID
    367   */
    368  changeHighlightedNodeWaitForUpdate(name, value, actorID) {
    369    return new Promise(resolve => {
    370      const highlighter = this.conn.getActor(actorID);
    371      const { _highlighter: h } = highlighter;
    372 
    373      h.once("updated", resolve);
    374 
    375      h.currentNode.setAttribute(name, value);
    376    });
    377  }
    378 
    379  /**
    380   * Register a one-time "updated" event listener.
    381   * The method does not wait for the "updated" event itself so the response can be sent
    382   * back and the client would know the event listener is properly set.
    383   * A separate event, "highlighter-updated", will be emitted when the highlighter updates.
    384   *
    385   * @param {string} actorID The highlighter actor ID
    386   */
    387  registerOneTimeHighlighterUpdate(actorID) {
    388    const { _highlighter } = this.conn.getActor(actorID);
    389    _highlighter.once("updated").then(() => this.emit("highlighter-updated"));
    390 
    391    // Return directly so the client knows the event listener is set
    392  }
    393 
    394  async getNodeRect(selector) {
    395    const node = this._querySelector(selector);
    396    return getRect(this.content, node, this.content);
    397  }
    398 
    399  async getTextNodeRect(parentSelector, childNodeIndex) {
    400    const parentNode = this._querySelector(parentSelector);
    401    const node = parentNode.childNodes[childNodeIndex];
    402    return getAdjustedQuads(this.content, node)[0].bounds;
    403  }
    404 
    405  /**
    406   * @returns {PausedDebuggerOverlay} The paused overlay instance
    407   */
    408  _getPausedDebuggerOverlay() {
    409    // We use `_pauseOverlay` since it's the cached value; `pauseOverlay` is a getter that
    410    // will create the overlay when called (if it does not exist yet).
    411    return this.targetActor?.threadActor?._pauseOverlay;
    412  }
    413 
    414  isPausedDebuggerOverlayVisible() {
    415    const pauseOverlay = this._getPausedDebuggerOverlay();
    416    if (!pauseOverlay) {
    417      return false;
    418    }
    419 
    420    const root = pauseOverlay.getElement("paused-dbg-root");
    421    const toolbar = pauseOverlay.getElement("paused-dbg-toolbar");
    422 
    423    return (
    424      !root.hasAttribute("hidden") &&
    425      root.getAttribute("overlay") == "true" &&
    426      !toolbar.hasAttribute("hidden") &&
    427      !!toolbar.getTextContent()
    428    );
    429  }
    430 
    431  /**
    432   * Simulates a click on a button of the debugger pause overlay.
    433   *
    434   * @param {string} id: The id of the element (e.g. "paused-dbg-resume-button").
    435   */
    436  async clickPausedDebuggerOverlayButton(id) {
    437    const pauseOverlay = this._getPausedDebuggerOverlay();
    438    if (!pauseOverlay) {
    439      return;
    440    }
    441 
    442    // Because the highlighter markup elements live inside an anonymous content frame which
    443    // does not expose an API to dispatch events to them, we can't directly dispatch
    444    // events to the nodes themselves.
    445    // We're directly calling `handleEvent` on the pause overlay, which is the mouse events
    446    // listener callback on the overlay.
    447    pauseOverlay.handleEvent({ type: "mousedown", target: { id } });
    448  }
    449 
    450  /**
    451   * @returns {EyeDropper}
    452   */
    453  _getEyeDropper() {
    454    const form = this.targetActor.form();
    455    const inspectorActor = this.conn._getOrCreateActor(form.inspectorActor);
    456    return inspectorActor?._eyeDropper;
    457  }
    458 
    459  isEyeDropperVisible() {
    460    const eyeDropper = this._getEyeDropper();
    461    if (!eyeDropper) {
    462      return false;
    463    }
    464 
    465    return (
    466      eyeDropper.getElement("eye-dropper-root").getAttribute("hidden") !==
    467      "true"
    468    );
    469  }
    470 
    471  getEyeDropperElementAttribute(elementId, attributeName) {
    472    const eyeDropper = this._getEyeDropper();
    473    if (!eyeDropper) {
    474      return null;
    475    }
    476 
    477    return eyeDropper.getElement(elementId).getAttribute(attributeName);
    478  }
    479 
    480  async getEyeDropperColorValue() {
    481    const eyeDropper = this._getEyeDropper();
    482    if (!eyeDropper) {
    483      return null;
    484    }
    485 
    486    // It might happen that while the eyedropper isn't hidden anymore, the color-value
    487    // is not set yet.
    488    const color = await TestUtils.waitForCondition(() => {
    489      const colorValueElement = eyeDropper.getElement(
    490        "eye-dropper-color-value"
    491      );
    492      const textContent = colorValueElement.getTextContent();
    493      return textContent;
    494    }, "Couldn't get a non-empty text content for the color-value element");
    495 
    496    return color;
    497  }
    498 
    499  /**
    500   * Get the TabbingOrderHighlighter for the associated targetActor
    501   *
    502   * @returns {TabbingOrderHighlighter}
    503   */
    504  _getTabbingOrderHighlighter() {
    505    const form = this.targetActor.form();
    506    const accessibilityActor = this.conn._getOrCreateActor(
    507      form.accessibilityActor
    508    );
    509 
    510    if (!accessibilityActor) {
    511      return null;
    512    }
    513    // We use `_tabbingOrderHighlighter` since it's the cached value; `tabbingOrderHighlighter`
    514    // is a getter that will create the highlighter when called (if it does not exist yet).
    515    return accessibilityActor.walker?._tabbingOrderHighlighter;
    516  }
    517 
    518  /**
    519   * Get a representation of the NodeTabbingOrderHighlighters created by the
    520   * TabbingOrderHighlighter of a given targetActor.
    521   *
    522   * @returns {Array<string>} An array which will contain as many entry as they are
    523   *          NodeTabbingOrderHighlighters displayed.
    524   *          Each item will be of the form `nodename[#id]: index`.
    525   *          For example:
    526   *          [
    527   *            `button#top-btn-1 : 1`,
    528   *            `html : 2`,
    529   *            `button#iframe-btn-1 : 3`,
    530   *            `button#iframe-btn-2 : 4`,
    531   *            `button#top-btn-2 : 5`,
    532   *          ]
    533   */
    534  getTabbingOrderHighlighterData() {
    535    const highlighter = this._getTabbingOrderHighlighter();
    536    if (!highlighter) {
    537      return [];
    538    }
    539 
    540    const nodeTabbingOrderHighlighters = [
    541      ...highlighter._highlighter._highlighters.values(),
    542    ].filter(h => !h.getElement("tabbing-order-root").hasAttribute("hidden"));
    543 
    544    return nodeTabbingOrderHighlighters.map(h => {
    545      let nodeStr = h.currentNode.nodeName.toLowerCase();
    546      if (h.currentNode.id) {
    547        nodeStr = `${nodeStr}#${h.currentNode.id}`;
    548      }
    549      return `${nodeStr} : ${h.getElement("tabbing-order-root").getTextContent()}`;
    550    });
    551  }
    552 }
    553 exports.HighlighterTestActor = HighlighterTestActor;
    554 
    555 class HighlighterTestFront extends protocol.FrontClassWithSpec(
    556  highlighterTestSpec
    557 ) {
    558  constructor(client, targetFront, parentFront) {
    559    super(client, targetFront, parentFront);
    560    this.formAttributeName = "highlighterTestActor";
    561    // The currently active highlighter is obtained by calling a custom getter
    562    // provided manually after requesting TestFront. See `getHighlighterTestFront(toolbox)`
    563    this._highlighter = null;
    564  }
    565 
    566  /**
    567   * Override the highlighter getter with a custom method that returns
    568   * the currently active highlighter instance.
    569   *
    570   * @param {Function|Highlighter} _customHighlighterGetter
    571   */
    572  set highlighter(_customHighlighterGetter) {
    573    this._highlighter = _customHighlighterGetter;
    574  }
    575 
    576  /**
    577   * The currently active highlighter instance.
    578   * If there is a custom getter for the highlighter, return its result.
    579   *
    580   * @return {Highlighter|null}
    581   */
    582  get highlighter() {
    583    return typeof this._highlighter === "function"
    584      ? this._highlighter()
    585      : this._highlighter;
    586  }
    587 
    588  /* eslint-disable max-len */
    589  changeHighlightedNodeWaitForUpdate(name, value, highlighter) {
    590    /* eslint-enable max-len */
    591    return super.changeHighlightedNodeWaitForUpdate(
    592      name,
    593      value,
    594      (highlighter || this.highlighter).actorID
    595    );
    596  }
    597 
    598  /**
    599   * Get the value of an attribute on one of the highlighter's node.
    600   *
    601   * @param {string} nodeID The Id of the node in the highlighter.
    602   * @param {string} name The name of the attribute.
    603   * @param {object} highlighter Optional custom highlighter to target
    604   * @return {string} value
    605   */
    606  getHighlighterNodeAttribute(nodeID, name, highlighter) {
    607    return this.getHighlighterAttribute(
    608      nodeID,
    609      name,
    610      (highlighter || this.highlighter).actorID
    611    );
    612  }
    613 
    614  getHighlighterNodeTextContent(nodeID, highlighter) {
    615    return super.getHighlighterNodeTextContent(
    616      nodeID,
    617      (highlighter || this.highlighter).actorID
    618    );
    619  }
    620 
    621  /**
    622   * Get the computed style of a property on one of the highlighter's node.
    623   *
    624   * @param {string} nodeID The Id of the node in the highlighter.
    625   * @param {string} property The name of the property.
    626   * @param {object} highlighter Optional custom highlighter to target
    627   * @return {string} value
    628   */
    629  getHighlighterComputedStyle(nodeID, property, highlighter) {
    630    return super.getHighlighterComputedStyle(
    631      nodeID,
    632      property,
    633      (highlighter || this.highlighter).actorID
    634    );
    635  }
    636 
    637  /**
    638   * Is the highlighter currently visible on the page?
    639   */
    640  async isHighlighting() {
    641    // Once the highlighter is hidden, the reference to it is lost.
    642    // Assume it is not highlighting.
    643    if (!this.highlighter) {
    644      return false;
    645    }
    646 
    647    try {
    648      const hidden = await this.getHighlighterNodeAttribute(
    649        "box-model-elements",
    650        "hidden"
    651      );
    652      return hidden === null;
    653    } catch (e) {
    654      if (e.message.match(/Highlighter not found/)) {
    655        return false;
    656      }
    657      throw e;
    658    }
    659  }
    660 
    661  /**
    662   * Get the current rect of the border region of the box-model highlighter
    663   */
    664  async getSimpleBorderRect() {
    665    const { border } = await this.getBoxModelStatus();
    666    const { p1, p2, p4 } = border.points;
    667 
    668    return {
    669      top: p1.y,
    670      left: p1.x,
    671      width: p2.x - p1.x,
    672      height: p4.y - p1.y,
    673    };
    674  }
    675 
    676  /**
    677   * Get the current positions and visibility of the various box-model highlighter
    678   * elements.
    679   */
    680  async getBoxModelStatus() {
    681    const isVisible = await this.isHighlighting();
    682 
    683    const ret = {
    684      visible: isVisible,
    685    };
    686 
    687    for (const region of ["margin", "border", "padding", "content"]) {
    688      const points = await this._getPointsForRegion(region);
    689      const visible = await this._isRegionHidden(region);
    690      ret[region] = { points, visible };
    691    }
    692 
    693    ret.guides = {};
    694    for (const guide of ["top", "right", "bottom", "left"]) {
    695      ret.guides[guide] = await this._getGuideStatus(guide);
    696    }
    697 
    698    return ret;
    699  }
    700 
    701  /**
    702   * Check that the box-model highlighter is currently highlighting the node matching the
    703   * given selector.
    704   *
    705   * @param {string} selector
    706   * @return {boolean}
    707   */
    708  async assertHighlightedNode(selector) {
    709    const rect = await this.getNodeRect(selector);
    710    return this.isNodeRectHighlighted(rect);
    711  }
    712 
    713  /**
    714   * Check that the box-model highlighter is currently highlighting the text node that can
    715   * be found at a given index within the list of childNodes of a parent element matching
    716   * the given selector.
    717   *
    718   * @param {string} parentSelector
    719   * @param {number} childNodeIndex
    720   * @return {boolean}
    721   */
    722  async assertHighlightedTextNode(parentSelector, childNodeIndex) {
    723    const rect = await this.getTextNodeRect(parentSelector, childNodeIndex);
    724    return this.isNodeRectHighlighted(rect);
    725  }
    726 
    727  /**
    728   * Check that the box-model highlighter is currently highlighting the given rect.
    729   *
    730   * @param {object} rect
    731   * @return {boolean}
    732   */
    733  async isNodeRectHighlighted({ left, top, width, height }) {
    734    const { visible, border } = await this.getBoxModelStatus();
    735    let points = border.points;
    736    if (!visible) {
    737      return false;
    738    }
    739 
    740    // Check that the node is within the box model
    741    const right = left + width;
    742    const bottom = top + height;
    743 
    744    // Converts points dictionnary into an array
    745    const list = [];
    746    for (let i = 1; i <= 4; i++) {
    747      const p = points["p" + i];
    748      list.push([p.x, p.y]);
    749    }
    750    points = list;
    751 
    752    // Check that each point of the node is within the box model
    753    return (
    754      isInside([left, top], points) &&
    755      isInside([right, top], points) &&
    756      isInside([right, bottom], points) &&
    757      isInside([left, bottom], points)
    758    );
    759  }
    760 
    761  /**
    762   * Get the coordinate (points attribute) from one of the polygon elements in the
    763   * box model highlighter.
    764   */
    765  async _getPointsForRegion(region) {
    766    const d = await this.getHighlighterNodeAttribute(
    767      "box-model-" + region,
    768      "d"
    769    );
    770 
    771    if (!d) {
    772      return null;
    773    }
    774 
    775    const polygons = d.match(/M[^M]+/g);
    776    if (!polygons) {
    777      return null;
    778    }
    779 
    780    const points = polygons[0]
    781      .trim()
    782      .split(" ")
    783      .map(i => {
    784        return i.replace(/M|L/, "").split(",");
    785      });
    786 
    787    return {
    788      p1: {
    789        x: parseFloat(points[0][0]),
    790        y: parseFloat(points[0][1]),
    791      },
    792      p2: {
    793        x: parseFloat(points[1][0]),
    794        y: parseFloat(points[1][1]),
    795      },
    796      p3: {
    797        x: parseFloat(points[2][0]),
    798        y: parseFloat(points[2][1]),
    799      },
    800      p4: {
    801        x: parseFloat(points[3][0]),
    802        y: parseFloat(points[3][1]),
    803      },
    804    };
    805  }
    806 
    807  /**
    808   * Is a given region polygon element of the box-model highlighter currently
    809   * hidden?
    810   */
    811  async _isRegionHidden(region) {
    812    const value = await this.getHighlighterNodeAttribute(
    813      "box-model-" + region,
    814      "hidden"
    815    );
    816    return value !== null;
    817  }
    818 
    819  async _getGuideStatus(location) {
    820    const id = "box-model-guide-" + location;
    821 
    822    const hidden = await this.getHighlighterNodeAttribute(id, "hidden");
    823    const x1 = await this.getHighlighterNodeAttribute(id, "x1");
    824    const y1 = await this.getHighlighterNodeAttribute(id, "y1");
    825    const x2 = await this.getHighlighterNodeAttribute(id, "x2");
    826    const y2 = await this.getHighlighterNodeAttribute(id, "y2");
    827 
    828    return {
    829      visible: !hidden,
    830      x1,
    831      y1,
    832      x2,
    833      y2,
    834    };
    835  }
    836 
    837  /**
    838   * Get the coordinates of the rectangle that is defined by the 4 guides displayed
    839   * in the toolbox box-model highlighter.
    840   *
    841   * @return {object} Null if at least one guide is hidden. Otherwise an object
    842   * with p1, p2, p3, p4 properties being {x, y} objects.
    843   */
    844  async getGuidesRectangle() {
    845    const tGuide = await this._getGuideStatus("top");
    846    const rGuide = await this._getGuideStatus("right");
    847    const bGuide = await this._getGuideStatus("bottom");
    848    const lGuide = await this._getGuideStatus("left");
    849 
    850    if (
    851      !tGuide.visible ||
    852      !rGuide.visible ||
    853      !bGuide.visible ||
    854      !lGuide.visible
    855    ) {
    856      return null;
    857    }
    858 
    859    return {
    860      p1: { x: lGuide.x1, y: tGuide.y1 },
    861      p2: { x: +rGuide.x1 + 1, y: tGuide.y1 },
    862      p3: { x: +rGuide.x1 + 1, y: +bGuide.y1 + 1 },
    863      p4: { x: lGuide.x1, y: +bGuide.y1 + 1 },
    864    };
    865  }
    866 
    867  /**
    868   * Get the "d" attribute value for one of the box-model highlighter's region
    869   * <path> elements, and parse it to a list of points.
    870   *
    871   * @param {string} region The box model region name.
    872   * @param {Front} highlighter The front of the highlighter.
    873   * @return {object} The object returned has the following form:
    874   * - d {String} the d attribute value
    875   * - points {Array} an array of all the polygons defined by the path. Each box
    876   *   is itself an Array of points, themselves being [x,y] coordinates arrays.
    877   */
    878  async getHighlighterRegionPath(region, highlighter) {
    879    const d = await this.getHighlighterNodeAttribute(
    880      `box-model-${region}`,
    881      "d",
    882      highlighter
    883    );
    884    if (!d) {
    885      return { d: null };
    886    }
    887 
    888    const polygons = d.match(/M[^M]+/g);
    889    if (!polygons) {
    890      return { d };
    891    }
    892 
    893    const points = [];
    894    for (const polygon of polygons) {
    895      points.push(
    896        polygon
    897          .trim()
    898          .split(" ")
    899          .map(i => {
    900            return i.replace(/M|L/, "").split(",");
    901          })
    902      );
    903    }
    904 
    905    return { d, points };
    906  }
    907 }
    908 protocol.registerFront(HighlighterTestFront);
    909 /**
    910 * Check whether a point is included in a polygon.
    911 * Taken and tweaked from:
    912 * https://github.com/iominh/point-in-polygon-extended/blob/master/src/index.js#L30-L85
    913 *
    914 * @param {Array} point [x,y] coordinates
    915 * @param {Array} polygon An array of [x,y] points
    916 * @return {boolean}
    917 */
    918 function isInside(point, polygon) {
    919  if (polygon.length === 0) {
    920    return false;
    921  }
    922 
    923  // Reduce the length of the fractional part because this is likely to cause errors when
    924  // the point is on the edge of the polygon.
    925  point = point.map(n => n.toFixed(2));
    926  polygon = polygon.map(p => p.map(n => n.toFixed(2)));
    927 
    928  const n = polygon.length;
    929  const newPoints = polygon.slice(0);
    930  newPoints.push(polygon[0]);
    931  let wn = 0;
    932 
    933  // loop through all edges of the polygon
    934  for (let i = 0; i < n; i++) {
    935    // Accept points on the edges
    936    const r = isLeft(newPoints[i], newPoints[i + 1], point);
    937    if (r === 0) {
    938      return true;
    939    }
    940    if (newPoints[i][1] <= point[1]) {
    941      if (newPoints[i + 1][1] > point[1] && r > 0) {
    942        wn++;
    943      }
    944    } else if (newPoints[i + 1][1] <= point[1] && r < 0) {
    945      wn--;
    946    }
    947  }
    948  if (wn === 0) {
    949    dumpn(JSON.stringify(point) + " is outside of " + JSON.stringify(polygon));
    950  }
    951  // the point is outside only when this winding number wn===0, otherwise it's inside
    952  return wn !== 0;
    953 }
    954 
    955 function isLeft(p0, p1, p2) {
    956  const l =
    957    (p1[0] - p0[0]) * (p2[1] - p0[1]) - (p2[0] - p0[0]) * (p1[1] - p0[1]);
    958  return l;
    959 }