tor-browser

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

walker.js (86451B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const { Actor } = require("resource://devtools/shared/protocol.js");
      8 const { walkerSpec } = require("resource://devtools/shared/specs/walker.js");
      9 
     10 const {
     11  LongStringActor,
     12 } = require("resource://devtools/server/actors/string.js");
     13 const {
     14  EXCLUDED_LISTENER,
     15 } = require("resource://devtools/server/actors/inspector/constants.js");
     16 
     17 loader.lazyRequireGetter(
     18  this,
     19  "nodeFilterConstants",
     20  "resource://devtools/shared/dom-node-filter-constants.js"
     21 );
     22 
     23 loader.lazyRequireGetter(
     24  this,
     25  [
     26    "getFrameElement",
     27    "isDirectShadowHostChild",
     28    "isFrameBlockedByCSP",
     29    "isFrameWithChildTarget",
     30    "isShadowHost",
     31    "isShadowRoot",
     32    "loadSheet",
     33  ],
     34  "resource://devtools/shared/layout/utils.js",
     35  true
     36 );
     37 
     38 loader.lazyRequireGetter(
     39  this,
     40  "throttle",
     41  "resource://devtools/shared/throttle.js",
     42  true
     43 );
     44 
     45 loader.lazyRequireGetter(
     46  this,
     47  [
     48    "allAnonymousContentTreeWalkerFilter",
     49    "findGridParentContainerForNode",
     50    "isNodeDead",
     51    "noAnonymousContentTreeWalkerFilter",
     52    "nodeDocument",
     53    "standardTreeWalkerFilter",
     54  ],
     55  "resource://devtools/server/actors/inspector/utils.js",
     56  true
     57 );
     58 
     59 loader.lazyRequireGetter(
     60  this,
     61  "CustomElementWatcher",
     62  "resource://devtools/server/actors/inspector/custom-element-watcher.js",
     63  true
     64 );
     65 loader.lazyRequireGetter(
     66  this,
     67  ["DocumentWalker", "SKIP_TO_SIBLING"],
     68  "resource://devtools/server/actors/inspector/document-walker.js",
     69  true
     70 );
     71 loader.lazyRequireGetter(
     72  this,
     73  ["NodeActor", "NodeListActor"],
     74  "resource://devtools/server/actors/inspector/node.js",
     75  true
     76 );
     77 loader.lazyRequireGetter(
     78  this,
     79  "NodePicker",
     80  "resource://devtools/server/actors/inspector/node-picker.js",
     81  true
     82 );
     83 loader.lazyRequireGetter(
     84  this,
     85  "LayoutActor",
     86  "resource://devtools/server/actors/layout.js",
     87  true
     88 );
     89 loader.lazyRequireGetter(
     90  this,
     91  ["getLayoutChangesObserver", "releaseLayoutChangesObserver"],
     92  "resource://devtools/server/actors/reflow.js",
     93  true
     94 );
     95 loader.lazyRequireGetter(
     96  this,
     97  "WalkerSearch",
     98  "resource://devtools/server/actors/utils/walker-search.js",
     99  true
    100 );
    101 
    102 // ContentDOMReference requires ChromeUtils, which isn't available in worker context.
    103 const lazy = {};
    104 if (!isWorker) {
    105  loader.lazyGetter(
    106    lazy,
    107    "ContentDOMReference",
    108    () =>
    109      ChromeUtils.importESModule(
    110        "resource://gre/modules/ContentDOMReference.sys.mjs",
    111        // ContentDOMReference needs to be retrieved from the shared global
    112        // since it is a shared singleton.
    113        { global: "shared" }
    114      ).ContentDOMReference
    115  );
    116 }
    117 
    118 loader.lazyServiceGetter(
    119  this,
    120  "eventListenerService",
    121  "@mozilla.org/eventlistenerservice;1",
    122  "nsIEventListenerService"
    123 );
    124 
    125 // Minimum delay between two "new-mutations" events.
    126 const MUTATIONS_THROTTLING_DELAY = 100;
    127 // List of mutation types that should -not- be throttled.
    128 const IMMEDIATE_MUTATIONS = ["pseudoClassLock"];
    129 
    130 const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
    131 
    132 // The possible completions to a ':'
    133 const PSEUDO_SELECTORS = [
    134  "::marker",
    135  "::selection",
    136  ":active",
    137  ":after",
    138  ":before",
    139  ":checked",
    140  ":disabled",
    141  ":empty",
    142  ":enabled",
    143  ":first-child",
    144  ":first-letter",
    145  ":first-of-type",
    146  ":focus",
    147  ":hover",
    148  ":lang(",
    149  ":last-child",
    150  ":last-of-type",
    151  ":link",
    152  ":not(",
    153  ":nth-child(",
    154  ":nth-last-child(",
    155  ":nth-last-of-type(",
    156  ":nth-of-type(",
    157  ":only-child",
    158  ":only-of-type",
    159  ":root",
    160  ":target",
    161  ":visited",
    162 ];
    163 
    164 const HELPER_SHEET =
    165  "data:text/css;charset=utf-8," +
    166  encodeURIComponent(`
    167  .__fx-devtools-hide-shortcut__ {
    168    visibility: hidden !important;
    169  }
    170 `);
    171 
    172 /**
    173 * We only send nodeValue up to a certain size by default.  This stuff
    174 * controls that size.
    175 */
    176 exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50;
    177 var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH;
    178 
    179 exports.getValueSummaryLength = function () {
    180  return gValueSummaryLength;
    181 };
    182 
    183 exports.setValueSummaryLength = function (val) {
    184  gValueSummaryLength = val;
    185 };
    186 
    187 /**
    188 * Server side of the DOM walker.
    189 */
    190 class WalkerActor extends Actor {
    191  /**
    192   * Create the WalkerActor
    193   *
    194   * @param {DevToolsServerConnection} conn
    195   *        The server connection.
    196   * @param {TargetActor} targetActor
    197   *        The top-level Actor for this tab.
    198   * @param {object} options
    199   *        - {Boolean} showAllAnonymousContent: Show all native anonymous content
    200   */
    201  constructor(conn, targetActor, options) {
    202    super(conn, walkerSpec);
    203    this.targetActor = targetActor;
    204    this.rootWin = targetActor.window;
    205    this.rootDoc = this.rootWin.document;
    206 
    207    // Map of already created node actors, keyed by their corresponding DOMNode.
    208    this._nodeActorsMap = new Map();
    209 
    210    this._pendingMutations = [];
    211    this._activePseudoClassLocks = new Set();
    212    this._mutationBreakpoints = new WeakMap();
    213    this._anonParents = new WeakMap();
    214    this.customElementWatcher = new CustomElementWatcher(
    215      targetActor.chromeEventHandler
    216    );
    217 
    218    // In this map, the key-value pairs are the overflow causing elements and their
    219    // respective ancestor scrollable node actor.
    220    this.overflowCausingElementsMap = new Map();
    221 
    222    this.showAllAnonymousContent = options.showAllAnonymousContent;
    223    // Allow native anonymous content (like <video> controls) if preffed on
    224    this.documentWalkerFilter = this.showAllAnonymousContent
    225      ? allAnonymousContentTreeWalkerFilter
    226      : standardTreeWalkerFilter;
    227 
    228    this.walkerSearch = new WalkerSearch(this);
    229 
    230    // Nodes which have been removed from the client's known
    231    // ownership tree are considered "orphaned", and stored in
    232    // this set.
    233    this._orphaned = new Set();
    234 
    235    // The client can tell the walker that it is interested in a node
    236    // even when it is orphaned with the `retainNode` method.  This
    237    // list contains orphaned nodes that were so retained.
    238    this._retainedOrphans = new Set();
    239 
    240    this.onSubtreeModified = this.onSubtreeModified.bind(this);
    241    this.onSubtreeModified[EXCLUDED_LISTENER] = true;
    242    this.onNodeRemoved = this.onNodeRemoved.bind(this);
    243    this.onNodeRemoved[EXCLUDED_LISTENER] = true;
    244    this.onAttributeModified = this.onAttributeModified.bind(this);
    245    this.onAttributeModified[EXCLUDED_LISTENER] = true;
    246 
    247    this.onMutations = this.onMutations.bind(this);
    248    this.onSlotchange = this.onSlotchange.bind(this);
    249    this.onShadowrootattached = this.onShadowrootattached.bind(this);
    250    this.onAnonymousrootcreated = this.onAnonymousrootcreated.bind(this);
    251    this.onAnonymousrootremoved = this.onAnonymousrootremoved.bind(this);
    252    this.onFrameLoad = this.onFrameLoad.bind(this);
    253    this.onFrameUnload = this.onFrameUnload.bind(this);
    254    this.onCustomElementDefined = this.onCustomElementDefined.bind(this);
    255    this._throttledEmitNewMutations = throttle(
    256      this._emitNewMutations.bind(this),
    257      MUTATIONS_THROTTLING_DELAY
    258    );
    259 
    260    targetActor.on("will-navigate", this.onFrameUnload);
    261    targetActor.on("window-ready", this.onFrameLoad);
    262 
    263    this.customElementWatcher.on(
    264      "element-defined",
    265      this.onCustomElementDefined
    266    );
    267 
    268    // Keep a reference to the chromeEventHandler for the current targetActor, to make
    269    // sure we will be able to remove the listener during the WalkerActor destroy().
    270    this.chromeEventHandler = targetActor.chromeEventHandler;
    271    // shadowrootattached is a chrome-only event. We enable it below.
    272    this.chromeEventHandler.addEventListener(
    273      "shadowrootattached",
    274      this.onShadowrootattached
    275    );
    276    // anonymousrootcreated is a chrome-only event. We enable it below.
    277    this.chromeEventHandler.addEventListener(
    278      "anonymousrootcreated",
    279      this.onAnonymousrootcreated
    280    );
    281    this.chromeEventHandler.addEventListener(
    282      "anonymousrootremoved",
    283      this.onAnonymousrootremoved
    284    );
    285    for (const { document } of this.targetActor.windows) {
    286      document.devToolsAnonymousAndShadowEventsEnabled = true;
    287    }
    288 
    289    // Ensure that the root document node actor is ready and
    290    // managed.
    291    this.rootNode = this.document();
    292 
    293    this.layoutChangeObserver = getLayoutChangesObserver(this.targetActor);
    294    this._onReflows = this._onReflows.bind(this);
    295    this.layoutChangeObserver.on("reflows", this._onReflows);
    296    this._onResize = this._onResize.bind(this);
    297    this.layoutChangeObserver.on("resize", this._onResize);
    298 
    299    this._onEventListenerChange = this._onEventListenerChange.bind(this);
    300    eventListenerService.addListenerChangeListener(this._onEventListenerChange);
    301  }
    302 
    303  get nodePicker() {
    304    if (!this._nodePicker) {
    305      this._nodePicker = new NodePicker(this, this.targetActor);
    306    }
    307 
    308    return this._nodePicker;
    309  }
    310 
    311  watchRootNode() {
    312    if (this.rootNode) {
    313      this.emit("root-available", this.rootNode);
    314    }
    315  }
    316 
    317  /**
    318   * Callback for eventListenerService.addListenerChangeListener
    319   *
    320   * @param nsISimpleEnumerator changesEnum
    321   *    enumerator of nsIEventListenerChange
    322   */
    323  _onEventListenerChange(changesEnum) {
    324    for (const current of changesEnum.enumerate(Ci.nsIEventListenerChange)) {
    325      const target = current.target;
    326 
    327      if (this._nodeActorsMap.has(target)) {
    328        const actor = this.getNode(target);
    329        const mutation = {
    330          type: "events",
    331          target: actor.actorID,
    332          hasEventListeners: actor.hasEventListeners(/* refreshCache */ true),
    333        };
    334        this.queueMutation(mutation);
    335      }
    336    }
    337  }
    338 
    339  // Returns the JSON representation of this object over the wire.
    340  form() {
    341    return {
    342      actor: this.actorID,
    343      root: this.rootNode.form(),
    344      rfpCSSColorScheme: ChromeUtils.shouldResistFingerprinting(
    345        "CSSPrefersColorScheme",
    346        null
    347      ),
    348      traits: {},
    349    };
    350  }
    351 
    352  toString() {
    353    return "[WalkerActor " + this.actorID + "]";
    354  }
    355 
    356  getDocumentWalker(node, skipTo) {
    357    return new DocumentWalker(node, this.rootWin, {
    358      filter: this.documentWalkerFilter,
    359      skipTo,
    360      showAnonymousContent: true,
    361    });
    362  }
    363 
    364  destroy() {
    365    if (this._destroyed) {
    366      return;
    367    }
    368    this._destroyed = true;
    369    super.destroy();
    370    try {
    371      this.clearPseudoClassLocks();
    372      this._activePseudoClassLocks = null;
    373 
    374      this.overflowCausingElementsMap.clear();
    375      this.overflowCausingElementsMap = null;
    376 
    377      this._hoveredNode = null;
    378      this.rootWin = null;
    379      this.rootDoc = null;
    380      this.rootNode = null;
    381      this.layoutHelpers = null;
    382      this._orphaned = null;
    383      this._retainedOrphans = null;
    384 
    385      this.targetActor.off("will-navigate", this.onFrameUnload);
    386      this.targetActor.off("window-ready", this.onFrameLoad);
    387      this.customElementWatcher.off(
    388        "element-defined",
    389        this.onCustomElementDefined
    390      );
    391 
    392      this.chromeEventHandler.removeEventListener(
    393        "shadowrootattached",
    394        this.onShadowrootattached
    395      );
    396      this.chromeEventHandler.removeEventListener(
    397        "anonymousrootcreated",
    398        this.onAnonymousrootcreated
    399      );
    400      this.chromeEventHandler.removeEventListener(
    401        "anonymousrootremoved",
    402        this.onAnonymousrootremoved
    403      );
    404 
    405      // This attribute is just for devtools, so we can unset once we're done.
    406      for (const { document } of this.targetActor.windows) {
    407        document.devToolsAnonymousAndShadowEventsEnabled = false;
    408      }
    409 
    410      this.onFrameLoad = null;
    411      this.onFrameUnload = null;
    412 
    413      this.customElementWatcher.destroy();
    414      this.customElementWatcher = null;
    415 
    416      this.walkerSearch.destroy();
    417 
    418      if (this._nodePicker) {
    419        this._nodePicker.destroy();
    420        this._nodePicker = null;
    421      }
    422 
    423      this.layoutChangeObserver.off("reflows", this._onReflows);
    424      this.layoutChangeObserver.off("resize", this._onResize);
    425      this.layoutChangeObserver = null;
    426      releaseLayoutChangesObserver(this.targetActor);
    427 
    428      eventListenerService.removeListenerChangeListener(
    429        this._onEventListenerChange
    430      );
    431 
    432      // Only nullify some key attributes after having removed all the listeners
    433      // as they may still be used in the related listeners.
    434      this._nodeActorsMap = null;
    435      this.onMutations = null;
    436 
    437      this.layoutActor = null;
    438      this.targetActor = null;
    439      this.chromeEventHandler = null;
    440      this.documentWalkerFilter = null;
    441 
    442      this.emit("destroyed");
    443    } catch (e) {
    444      console.error(e);
    445    }
    446  }
    447 
    448  release() {}
    449 
    450  unmanage(actor) {
    451    if (actor instanceof NodeActor) {
    452      if (
    453        this._activePseudoClassLocks &&
    454        this._activePseudoClassLocks.has(actor)
    455      ) {
    456        this.clearPseudoClassLocks(actor);
    457      }
    458 
    459      this.customElementWatcher.unmanageNode(actor);
    460 
    461      this._nodeActorsMap.delete(actor.rawNode);
    462    }
    463    super.unmanage(actor);
    464  }
    465 
    466  /**
    467   * Determine if the walker has come across this DOM node before.
    468   *
    469   * @param {DOMNode} rawNode
    470   * @return {boolean}
    471   */
    472  hasNode(rawNode) {
    473    return this._nodeActorsMap.has(rawNode);
    474  }
    475 
    476  /**
    477   * If the walker has come across this DOM node before, then get the
    478   * corresponding node actor.
    479   *
    480   * @param {DOMNode} rawNode
    481   * @return {NodeActor}
    482   */
    483  getNode(rawNode) {
    484    return this._nodeActorsMap.get(rawNode);
    485  }
    486 
    487  /**
    488   * Internal helper that will either retrieve the existing NodeActor for the
    489   * provided node or create the actor on the fly if it doesn't exist.
    490   * This method should only be called when we are sure that the node should be
    491   * known by the client and that the parent node is already known.
    492   *
    493   * Otherwise prefer `getNode` to only retrieve known actors or `attachElement`
    494   * to create node actors recursively.
    495   *
    496   * @param  {DOMNode} node
    497   *         The node for which we want to create or get an actor
    498   * @return {NodeActor} The corresponding NodeActor
    499   */
    500  _getOrCreateNodeActor(node) {
    501    let actor = this.getNode(node);
    502    if (actor) {
    503      return actor;
    504    }
    505 
    506    actor = new NodeActor(this, node);
    507 
    508    // Add the node actor as a child of this walker actor, assigning
    509    // it an actorID.
    510    this.manage(actor);
    511    this._nodeActorsMap.set(node, actor);
    512 
    513    if (node.nodeType === Node.DOCUMENT_NODE) {
    514      actor.watchDocument(node, this.onMutations);
    515    }
    516 
    517    if (isShadowRoot(actor.rawNode)) {
    518      actor.watchDocument(node.ownerDocument, this.onMutations);
    519      actor.watchSlotchange(this.onSlotchange);
    520    }
    521 
    522    this.customElementWatcher.manageNode(actor);
    523 
    524    return actor;
    525  }
    526 
    527  /**
    528   * When a custom element is defined, send a customElementDefined mutation for all the
    529   * NodeActors using this tag name.
    530   */
    531  onCustomElementDefined({ actors }) {
    532    actors.forEach(actor =>
    533      this.queueMutation({
    534        target: actor.actorID,
    535        type: "customElementDefined",
    536        customElementLocation: actor.getCustomElementLocation(),
    537      })
    538    );
    539  }
    540 
    541  _onReflows() {
    542    // Going through the nodes the walker knows about, see which ones have had their
    543    // containerType, display, scrollable or overflow state changed and send events if any.
    544    const containerTypeChanges = [];
    545    const displayTypeChanges = [];
    546    const scrollableStateChanges = [];
    547    const anchorNameChanges = [];
    548 
    549    const currentOverflowCausingElementsMap = new Map();
    550 
    551    for (const [node, actor] of this._nodeActorsMap) {
    552      if (Cu.isDeadWrapper(node)) {
    553        continue;
    554      }
    555 
    556      const displayType = actor.displayType;
    557      const isDisplayed = actor.isDisplayed;
    558 
    559      if (
    560        displayType !== actor.currentDisplayType ||
    561        isDisplayed !== actor.wasDisplayed
    562      ) {
    563        displayTypeChanges.push(actor);
    564 
    565        // Updating the original value
    566        actor.currentDisplayType = displayType;
    567        actor.wasDisplayed = isDisplayed;
    568      }
    569 
    570      const isScrollable = actor.isScrollable;
    571      if (isScrollable !== actor.wasScrollable) {
    572        scrollableStateChanges.push(actor);
    573        actor.wasScrollable = isScrollable;
    574      }
    575 
    576      if (isScrollable) {
    577        this.updateOverflowCausingElements(
    578          actor,
    579          currentOverflowCausingElementsMap
    580        );
    581      }
    582 
    583      const containerType = actor.containerType;
    584      if (containerType !== actor.currentContainerType) {
    585        containerTypeChanges.push(actor);
    586        actor.currentContainerType = containerType;
    587      }
    588 
    589      const anchorName = actor.anchorName;
    590      if (anchorName !== actor.currentAnchorName) {
    591        anchorNameChanges.push(actor);
    592        actor.currentAnchorName = anchorName;
    593      }
    594    }
    595 
    596    // Get the NodeActor for each node in the symmetric difference of
    597    // currentOverflowCausingElementsMap and this.overflowCausingElementsMap
    598    const overflowStateChanges = [...currentOverflowCausingElementsMap.keys()]
    599      .filter(node => !this.overflowCausingElementsMap.has(node))
    600      .concat(
    601        [...this.overflowCausingElementsMap.keys()].filter(
    602          node => !currentOverflowCausingElementsMap.has(node)
    603        )
    604      )
    605      .filter(node => this.hasNode(node))
    606      .map(node => this.getNode(node));
    607 
    608    this.overflowCausingElementsMap = currentOverflowCausingElementsMap;
    609 
    610    if (overflowStateChanges.length) {
    611      this.emit("overflow-change", overflowStateChanges);
    612    }
    613 
    614    if (displayTypeChanges.length) {
    615      this.emit("display-change", displayTypeChanges);
    616    }
    617 
    618    if (scrollableStateChanges.length) {
    619      this.emit("scrollable-change", scrollableStateChanges);
    620    }
    621 
    622    if (containerTypeChanges.length) {
    623      this.emit("container-type-change", containerTypeChanges);
    624    }
    625 
    626    if (anchorNameChanges.length) {
    627      this.emit("anchor-name-change", anchorNameChanges);
    628    }
    629  }
    630 
    631  /**
    632   * When the browser window gets resized, relay the event to the front.
    633   */
    634  _onResize() {
    635    this.emit("resize");
    636  }
    637 
    638  /**
    639   * Ensures that the node is attached and it can be accessed from the root.
    640   *
    641   * @param {(Node|NodeActor)} nodes The nodes
    642   * @return {object} An object compatible with the disconnectedNode type.
    643   */
    644  attachElement(node) {
    645    const { nodes, newParents } = this.attachElements([node]);
    646    return {
    647      node: nodes[0],
    648      newParents,
    649    };
    650  }
    651 
    652  /**
    653   * Ensures that the nodes are attached and they can be accessed from the root.
    654   *
    655   * @param {(Node[]|NodeActor[])} nodes The nodes
    656   * @return {object} An object compatible with the disconnectedNodeArray type.
    657   */
    658  attachElements(nodes) {
    659    const nodeActors = [];
    660    const newParents = new Set();
    661    for (let node of nodes) {
    662      if (!(node instanceof NodeActor)) {
    663        // If the provided node doesn't match the filter, use the closest ancestor
    664        while (
    665          node &&
    666          this.documentWalkerFilter(node) != nodeFilterConstants.FILTER_ACCEPT
    667        ) {
    668          node = this.rawParentNode(node);
    669        }
    670        if (!node) {
    671          continue;
    672        }
    673 
    674        node = this._getOrCreateNodeActor(node);
    675      }
    676 
    677      this.ensurePathToRoot(node, newParents);
    678      // If nodes may be an array of raw nodes, we're sure to only have
    679      // NodeActors with the following array.
    680      nodeActors.push(node);
    681    }
    682 
    683    return {
    684      nodes: nodeActors,
    685      newParents: [...newParents],
    686    };
    687  }
    688 
    689  /**
    690   * Return the document node that contains the given node,
    691   * or the root node if no node is specified.
    692   *
    693   * @param NodeActor node
    694   *        The node whose document is needed, or null to
    695   *        return the root.
    696   */
    697  document(node) {
    698    const doc = isNodeDead(node) ? this.rootDoc : nodeDocument(node.rawNode);
    699    return this._getOrCreateNodeActor(doc);
    700  }
    701 
    702  /**
    703   * Return the documentElement for the document containing the
    704   * given node.
    705   *
    706   * @param NodeActor node
    707   *        The node whose documentElement is requested, or null
    708   *        to use the root document.
    709   */
    710  documentElement(node) {
    711    const elt = isNodeDead(node)
    712      ? this.rootDoc.documentElement
    713      : nodeDocument(node.rawNode).documentElement;
    714    return this._getOrCreateNodeActor(elt);
    715  }
    716 
    717  parentNode(node) {
    718    const parent = this.rawParentNode(node);
    719    if (parent) {
    720      return this._getOrCreateNodeActor(parent);
    721    }
    722 
    723    return null;
    724  }
    725 
    726  rawParentNode(node) {
    727    const rawNode = node instanceof NodeActor ? node.rawNode : node;
    728    if (rawNode == this.rootDoc) {
    729      return null;
    730    }
    731    const parentNode = InspectorUtils.getParentForNode(
    732      rawNode,
    733      /* anonymous = */ true
    734    );
    735 
    736    if (!parentNode) {
    737      return null;
    738    }
    739 
    740    // If the parent node is one we should ignore (e.g. :-moz-snapshot-containing-block,
    741    // which is the root node for ::view-transition pseudo elements), we want to return
    742    // the closest non-ignored parent.
    743    if (
    744      this.documentWalkerFilter(parentNode) ===
    745      nodeFilterConstants.FILTER_ACCEPT_CHILDREN
    746    ) {
    747      return this.rawParentNode(parentNode);
    748    }
    749 
    750    return parentNode;
    751  }
    752 
    753  /**
    754   * If the given NodeActor only has a single text node as a child with a text
    755   * content small enough to be inlined, return that child's NodeActor.
    756   *
    757   * @param Element rawNode
    758   */
    759  inlineTextChild(rawNode) {
    760    // Quick checks to prevent creating a new walker if possible.
    761    if (
    762      !!rawNode.implementedPseudoElement ||
    763      isShadowHost(rawNode) ||
    764      rawNode.nodeType != Node.ELEMENT_NODE ||
    765      !!rawNode.children.length ||
    766      isFrameWithChildTarget(this.targetActor, rawNode) ||
    767      isFrameBlockedByCSP(rawNode)
    768    ) {
    769      return undefined;
    770    }
    771 
    772    const children = this._rawChildren(rawNode, /* includeAssigned = */ true);
    773    const firstChild = children[0];
    774 
    775    // Bail out if:
    776    // - more than one child
    777    // - unique child is not a text node
    778    // - unique child is a text node, but is too long to be inlined
    779    // - we are a slot -> these are always represented on their own lines with
    780    //                    a link to the original node.
    781    // - we are a flex item -> these are always shown on their own lines so they can be
    782    //                         selected by the flexbox inspector.
    783    const isAssignedToSlot =
    784      firstChild &&
    785      rawNode.nodeName === "SLOT" &&
    786      isDirectShadowHostChild(firstChild);
    787 
    788    const isFlexItem = !!firstChild?.parentFlexElement;
    789 
    790    if (
    791      !firstChild ||
    792      children.length > 1 ||
    793      firstChild.nodeType !== Node.TEXT_NODE ||
    794      firstChild.nodeValue.length > gValueSummaryLength ||
    795      isAssignedToSlot ||
    796      isFlexItem
    797    ) {
    798      return undefined;
    799    }
    800 
    801    return this._getOrCreateNodeActor(firstChild);
    802  }
    803 
    804  /**
    805   * Mark a node as 'retained'.
    806   *
    807   * A retained node is not released when `releaseNode` is called on its
    808   * parent, or when a parent is released with the `cleanup` option to
    809   * `getMutations`.
    810   *
    811   * When a retained node's parent is released, a retained mode is added to
    812   * the walker's "retained orphans" list.
    813   *
    814   * Retained nodes can be deleted by providing the `force` option to
    815   * `releaseNode`.  They will also be released when their document
    816   * has been destroyed.
    817   *
    818   * Retaining a node makes no promise about its children;  They can
    819   * still be removed by normal means.
    820   */
    821  retainNode(node) {
    822    node.retained = true;
    823  }
    824 
    825  /**
    826   * Remove the 'retained' mark from a node.  If the node was a
    827   * retained orphan, release it.
    828   */
    829  unretainNode(node) {
    830    node.retained = false;
    831    if (this._retainedOrphans.has(node)) {
    832      this._retainedOrphans.delete(node);
    833      this.releaseNode(node);
    834    }
    835  }
    836 
    837  /**
    838   * Release actors for a node and all child nodes.
    839   */
    840  releaseNode(node, options = {}) {
    841    if (isNodeDead(node)) {
    842      return;
    843    }
    844 
    845    if (node.retained && !options.force) {
    846      this._retainedOrphans.add(node);
    847      return;
    848    }
    849 
    850    if (node.retained) {
    851      // Forcing a retained node to go away.
    852      this._retainedOrphans.delete(node);
    853    }
    854 
    855    for (const child of this._rawChildren(node.rawNode)) {
    856      const childActor = this.getNode(child);
    857      if (childActor) {
    858        this.releaseNode(childActor, options);
    859      }
    860    }
    861 
    862    node.destroy();
    863  }
    864 
    865  /**
    866   * Add any nodes between `node` and the walker's root node that have not
    867   * yet been seen by the client.
    868   */
    869  ensurePathToRoot(node, newParents = new Set()) {
    870    if (!node) {
    871      return newParents;
    872    }
    873    let parent = this.rawParentNode(node);
    874    while (parent) {
    875      let parentActor = this.getNode(parent);
    876      if (parentActor) {
    877        // This parent did exist, so the client knows about it.
    878        return newParents;
    879      }
    880      // This parent didn't exist, so hasn't been seen by the client yet.
    881      parentActor = this._getOrCreateNodeActor(parent);
    882      newParents.add(parentActor);
    883      parent = this.rawParentNode(parentActor);
    884    }
    885    return newParents;
    886  }
    887 
    888  /**
    889   * Return the number of children under the provided NodeActor.
    890   *
    891   * @param NodeActor node
    892   *    See JSDoc for children()
    893   * @param object options
    894   *    See JSDoc for children()
    895   * @return Number the number of children
    896   */
    897  countChildren(node, options = {}) {
    898    return this._getChildren(node, options).nodes.length;
    899  }
    900 
    901  /**
    902   * Return children of the given node.  By default this method will return
    903   * all children of the node, but there are options that can restrict this
    904   * to a more manageable subset.
    905   *
    906   * @param NodeActor node
    907   *    The node whose children you're curious about.
    908   * @param object options
    909   *    Named options:
    910   *    `maxNodes`: The set of nodes returned by the method will be no longer
    911   *       than maxNodes.
    912   *    `start`: If a node is specified, the list of nodes will start
    913   *       with the given child.  Mutally exclusive with `center`.
    914   *    `center`: If a node is specified, the given node will be as centered
    915   *       as possible in the list, given how close to the ends of the child
    916   *       list it is.  Mutually exclusive with `start`.
    917   *
    918   * @returns an object with three items:
    919   *    hasFirst: true if the first child of the node is included in the list.
    920   *    hasLast: true if the last child of the node is included in the list.
    921   *    nodes: Array of NodeActor representing the nodes returned by the request.
    922   */
    923  children(node, options = {}) {
    924    const { hasFirst, hasLast, nodes } = this._getChildren(node, options);
    925    return {
    926      hasFirst,
    927      hasLast,
    928      nodes: nodes.map(n => this._getOrCreateNodeActor(n)),
    929    };
    930  }
    931 
    932  /**
    933   * Returns the raw children of the DOM node, with anonymous content filtered as needed
    934   *
    935   * @param Node rawNode.
    936   * @param boolean includeAssigned
    937   *   Whether <slot> assigned children should be returned. See
    938   *   HTMLSlotElement.assignedNodes().
    939   * @returns Array<Node> the list of children.
    940   */
    941  _rawChildren(rawNode, includeAssigned) {
    942    const ret = [];
    943    const children = InspectorUtils.getChildrenForNode(
    944      rawNode,
    945      /* anonymous = */ true,
    946      includeAssigned
    947    );
    948    for (const child of children) {
    949      const filterResult = this.documentWalkerFilter(child);
    950      if (filterResult == nodeFilterConstants.FILTER_ACCEPT) {
    951        ret.push(child);
    952      } else if (filterResult == nodeFilterConstants.FILTER_ACCEPT_CHILDREN) {
    953        // In some cases, we want to completly ignore a node, and display its children
    954        // instead (e.g. for `<div type="::-moz-snapshot-containing-block">`,
    955        // we don't want it displayed in the markup view,
    956        // but we do want to have its `::view-transition` child)
    957        ret.push(...this._rawChildren(child, includeAssigned));
    958      }
    959    }
    960    return ret;
    961  }
    962 
    963  /**
    964   * Return chidlren of the given node. Contrary to children children(), this method only
    965   * returns DOMNodes. Therefore it will not create NodeActor wrappers and will not
    966   * update the nodeActors map for the discovered nodes either. This makes this method
    967   * safe to call when you are not sure if the discovered nodes will be communicated to
    968   * the client.
    969   *
    970   * @param NodeActor node
    971   *    See JSDoc for children()
    972   * @param object options
    973   *    See JSDoc for children()
    974   * @return  an object with three items:
    975   *    hasFirst: true if the first child of the node is included in the list.
    976   *    hasLast: true if the last child of the node is included in the list.
    977   *    nodes: Array of DOMNodes.
    978   */
    979  // eslint-disable-next-line complexity
    980  _getChildren(node, options = {}) {
    981    if (isNodeDead(node) || isFrameBlockedByCSP(node.rawNode)) {
    982      return { hasFirst: true, hasLast: true, nodes: [] };
    983    }
    984 
    985    if (options.center && options.start) {
    986      throw Error("Can't specify both 'center' and 'start' options.");
    987    }
    988 
    989    let maxNodes = options.maxNodes || -1;
    990    if (maxNodes == -1) {
    991      maxNodes = Number.MAX_VALUE;
    992    }
    993 
    994    let nodes = this._rawChildren(node.rawNode, /* includeAssigned = */ true);
    995    let hasFirst = true;
    996    let hasLast = true;
    997    if (nodes.length > maxNodes) {
    998      let startIndex;
    999      if (options.center) {
   1000        const centerIndex = nodes.indexOf(options.center.rawNode);
   1001        const backwardCount = Math.floor(maxNodes / 2);
   1002        // If centering would hit the end, just read the last maxNodes nodes.
   1003        if (centerIndex - backwardCount + maxNodes >= nodes.length) {
   1004          startIndex = nodes.length - maxNodes;
   1005        } else {
   1006          startIndex = Math.max(0, centerIndex - backwardCount);
   1007        }
   1008      } else if (options.start) {
   1009        startIndex = Math.max(0, nodes.indexOf(options.start.rawNode));
   1010      } else {
   1011        startIndex = 0;
   1012      }
   1013      const endIndex = Math.min(startIndex + maxNodes, nodes.length);
   1014      hasFirst = startIndex == 0;
   1015      hasLast = endIndex >= nodes.length;
   1016      nodes = nodes.slice(startIndex, endIndex);
   1017    }
   1018 
   1019    return { hasFirst, hasLast, nodes };
   1020  }
   1021 
   1022  /**
   1023   * Get the next sibling of a given node.  Getting nodes one at a time
   1024   * might be inefficient, be careful.
   1025   */
   1026  nextSibling(node) {
   1027    if (isNodeDead(node)) {
   1028      return null;
   1029    }
   1030 
   1031    const walker = this.getDocumentWalker(node.rawNode);
   1032    const sibling = walker.nextSibling();
   1033    return sibling ? this._getOrCreateNodeActor(sibling) : null;
   1034  }
   1035 
   1036  /**
   1037   * Get the previous sibling of a given node.  Getting nodes one at a time
   1038   * might be inefficient, be careful.
   1039   */
   1040  previousSibling(node) {
   1041    if (isNodeDead(node)) {
   1042      return null;
   1043    }
   1044 
   1045    const walker = this.getDocumentWalker(node.rawNode);
   1046    const sibling = walker.previousSibling();
   1047    return sibling ? this._getOrCreateNodeActor(sibling) : null;
   1048  }
   1049 
   1050  /**
   1051   * Helper function for the `children` method: Read forward in the sibling
   1052   * list into an array with `count` items, including the current node.
   1053   */
   1054  _readForward(walker, count) {
   1055    const ret = [];
   1056 
   1057    let node = walker.currentNode;
   1058    do {
   1059      if (!walker.isSkippedNode(node)) {
   1060        // The walker can be on a node that would be filtered out if it didn't find any
   1061        // other node to fallback to.
   1062        ret.push(node);
   1063      }
   1064      node = walker.nextSibling();
   1065    } while (node && --count);
   1066    return ret;
   1067  }
   1068 
   1069  /**
   1070   * Return the first node in the document that matches the given selector.
   1071   * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector
   1072   *
   1073   * @param NodeActor baseNode
   1074   * @param string selector
   1075   */
   1076  querySelector(baseNode, selector) {
   1077    if (isNodeDead(baseNode)) {
   1078      return {};
   1079    }
   1080 
   1081    const node = baseNode.rawNode.querySelector(selector);
   1082    if (!node) {
   1083      return {};
   1084    }
   1085 
   1086    return this.attachElement(node);
   1087  }
   1088 
   1089  /**
   1090   * Return a NodeListActor with all nodes that match the given selector.
   1091   * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll
   1092   *
   1093   * @param NodeActor baseNode
   1094   * @param string selector
   1095   */
   1096  querySelectorAll(baseNode, selector) {
   1097    let nodeList = null;
   1098 
   1099    try {
   1100      nodeList = baseNode.rawNode.querySelectorAll(selector);
   1101    } catch (e) {
   1102      // Bad selector. Do nothing as the selector can come from a searchbox.
   1103    }
   1104 
   1105    return new NodeListActor(this, nodeList);
   1106  }
   1107 
   1108  /**
   1109   * Return the node in the baseNode rootNode matching the passed id referenced in a
   1110   * idref/idreflist attribute, as those are scoped within a shadow root.
   1111   *
   1112   * @param NodeActor baseNode
   1113   * @param string id
   1114   */
   1115  getIdrefNode(baseNode, id) {
   1116    if (isNodeDead(baseNode)) {
   1117      return {};
   1118    }
   1119 
   1120    // Get the document or the shadow root for baseNode
   1121    const rootNode = baseNode.rawNode.getRootNode({ composed: false });
   1122    if (!rootNode) {
   1123      return {};
   1124    }
   1125 
   1126    const node = rootNode.getElementById(id);
   1127    if (!node) {
   1128      return {};
   1129    }
   1130 
   1131    return this.attachElement(node);
   1132  }
   1133 
   1134  /**
   1135   * Get a list of nodes that match the given selector in all known frames of
   1136   * the current content page.
   1137   *
   1138   * @param {string} selector.
   1139   * @return {Array}
   1140   */
   1141  _multiFrameQuerySelectorAll(selector) {
   1142    let nodes = [];
   1143 
   1144    for (const { document } of this.targetActor.windows) {
   1145      try {
   1146        nodes = [...nodes, ...document.querySelectorAll(selector)];
   1147      } catch (e) {
   1148        // Bad selector. Do nothing as the selector can come from a searchbox.
   1149      }
   1150    }
   1151 
   1152    return nodes;
   1153  }
   1154 
   1155  /**
   1156   * Get a list of nodes that match the given XPath in all known frames of
   1157   * the current content page.
   1158   *
   1159   * @param {string} xPath.
   1160   * @return {Array}
   1161   */
   1162  _multiFrameXPath(xPath) {
   1163    const nodes = [];
   1164 
   1165    for (const window of this.targetActor.windows) {
   1166      const document = window.document;
   1167      try {
   1168        const result = document.evaluate(
   1169          xPath,
   1170          document.documentElement,
   1171          null,
   1172          window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
   1173          null
   1174        );
   1175 
   1176        for (let i = 0; i < result.snapshotLength; i++) {
   1177          nodes.push(result.snapshotItem(i));
   1178        }
   1179      } catch (e) {
   1180        // Bad XPath. Do nothing as the XPath can come from a searchbox.
   1181      }
   1182    }
   1183 
   1184    return nodes;
   1185  }
   1186 
   1187  /**
   1188   * Return a NodeListActor with all nodes that match the given XPath in all
   1189   * frames of the current content page.
   1190   *
   1191   * @param {string} xPath
   1192   */
   1193  multiFrameXPath(xPath) {
   1194    return new NodeListActor(this, this._multiFrameXPath(xPath));
   1195  }
   1196 
   1197  /**
   1198   * Search the document for a given string.
   1199   * Results will be searched with the walker-search module (searches through
   1200   * tag names, attribute names and values, and text contents).
   1201   *
   1202   * @returns {searchresult}
   1203   *            - {NodeList} list
   1204   *            - {Array<Object>} metadata. Extra information with indices that
   1205   *                              match up with node list.
   1206   */
   1207  search(query) {
   1208    const results = this.walkerSearch.search(query);
   1209 
   1210    // For now, only return each node once, since the frontend doesn't have a way to
   1211    // highlight each result individually (This would change if we fix Bug 1976634).
   1212    const seenNodes = new Set();
   1213    for (const { node } of results) {
   1214      const isInlinedTextNode =
   1215        node.nodeType === Node.TEXT_NODE &&
   1216        node.parentElement &&
   1217        this.inlineTextChild(node.parentElement);
   1218 
   1219      // If this is a text node that will be inlined in its parent element, let's directly
   1220      // return the parent element already so we don't get multiple results for the same
   1221      // Markup view tree node.
   1222      seenNodes.add(isInlinedTextNode ? node.parentElement : node);
   1223    }
   1224 
   1225    const nodeList = new NodeListActor(this, Array.from(seenNodes));
   1226 
   1227    return {
   1228      list: nodeList,
   1229      metadata: [],
   1230    };
   1231  }
   1232 
   1233  /**
   1234   * Returns a list of matching results for CSS selector autocompletion.
   1235   *
   1236   * @param string query
   1237   *        The selector query being completed
   1238   * @param string completing
   1239   *        The exact token being completed out of the query
   1240   * @param string selectorState
   1241   *        One of "pseudo", "id", "tag", "class", "null"
   1242   */
   1243  // eslint-disable-next-line complexity
   1244  getSuggestionsForQuery(query, completing, selectorState) {
   1245    const sugs = {
   1246      classes: new Set(),
   1247      tags: new Set(),
   1248      ids: new Set(),
   1249    };
   1250    let result = [];
   1251    let nodes = null;
   1252    // Filtering and sorting the results so that protocol transfer is minimal.
   1253    switch (selectorState) {
   1254      case "pseudo": {
   1255        const colonPrefixedCompleting = ":" + completing;
   1256        for (const pseudo of PSEUDO_SELECTORS) {
   1257          if (pseudo.startsWith(colonPrefixedCompleting)) {
   1258            result.push([pseudo]);
   1259          }
   1260        }
   1261        break;
   1262      }
   1263 
   1264      case "class":
   1265        if (!query) {
   1266          nodes = this._multiFrameQuerySelectorAll("[class]");
   1267        } else {
   1268          nodes = this._multiFrameQuerySelectorAll(query);
   1269        }
   1270        for (const node of nodes) {
   1271          for (const className of node.classList) {
   1272            sugs.classes.add(className);
   1273          }
   1274        }
   1275        sugs.classes.delete("");
   1276        sugs.classes.delete(HIDDEN_CLASS);
   1277        for (const className of sugs.classes) {
   1278          if (className.startsWith(completing)) {
   1279            result.push(["." + CSS.escape(className), selectorState]);
   1280          }
   1281        }
   1282        break;
   1283 
   1284      case "id":
   1285        if (!query) {
   1286          nodes = this._multiFrameQuerySelectorAll("[id]");
   1287        } else {
   1288          nodes = this._multiFrameQuerySelectorAll(query);
   1289        }
   1290        for (const node of nodes) {
   1291          sugs.ids.add(node.id);
   1292        }
   1293        for (const id of sugs.ids) {
   1294          if (id.startsWith(completing) && id !== "") {
   1295            result.push(["#" + CSS.escape(id), selectorState]);
   1296          }
   1297        }
   1298        break;
   1299 
   1300      case "tag":
   1301        if (!query) {
   1302          nodes = this._multiFrameQuerySelectorAll("*");
   1303        } else {
   1304          nodes = this._multiFrameQuerySelectorAll(query);
   1305        }
   1306        for (const node of nodes) {
   1307          const tag = node.localName;
   1308          sugs.tags.add(tag);
   1309        }
   1310        for (const tag of sugs.tags) {
   1311          if (new RegExp("^" + completing + ".*", "i").test(tag)) {
   1312            result.push([tag, selectorState]);
   1313          }
   1314        }
   1315 
   1316        // For state 'tag' (no preceding # or .) and when there's no query (i.e.
   1317        // only one word) then search for the matching classes and ids
   1318        if (!query) {
   1319          result = [
   1320            ...result,
   1321            ...this.getSuggestionsForQuery(null, completing, "class")
   1322              .suggestions,
   1323            ...this.getSuggestionsForQuery(null, completing, "id").suggestions,
   1324          ];
   1325        }
   1326 
   1327        break;
   1328 
   1329      case "null":
   1330        nodes = this._multiFrameQuerySelectorAll(query);
   1331        for (const node of nodes) {
   1332          sugs.ids.add(node.id);
   1333          const tag = node.localName;
   1334          sugs.tags.add(tag);
   1335          for (const className of node.classList) {
   1336            sugs.classes.add(className);
   1337          }
   1338        }
   1339        for (const tag of sugs.tags) {
   1340          tag && result.push([tag]);
   1341        }
   1342        for (const id of sugs.ids) {
   1343          id && result.push(["#" + id]);
   1344        }
   1345        sugs.classes.delete("");
   1346        sugs.classes.delete(HIDDEN_CLASS);
   1347        for (const className of sugs.classes) {
   1348          className && result.push(["." + className]);
   1349        }
   1350    }
   1351 
   1352    // Sort by type (id, class, tag) and name (asc)
   1353    result = result.sort((a, b) => {
   1354      // Prefixing ids, classes and tags, to group results
   1355      const firstA = a[0].substring(0, 1);
   1356      const firstB = b[0].substring(0, 1);
   1357 
   1358      const getSortKeyPrefix = firstLetter => {
   1359        if (firstLetter === "#") {
   1360          return "2";
   1361        }
   1362        if (firstLetter === ".") {
   1363          return "1";
   1364        }
   1365        return "0";
   1366      };
   1367 
   1368      const sortA = getSortKeyPrefix(firstA) + a[0];
   1369      const sortB = getSortKeyPrefix(firstB) + b[0];
   1370 
   1371      // String compare
   1372      return sortA.localeCompare(sortB);
   1373    });
   1374 
   1375    result = result.slice(0, 25);
   1376 
   1377    return {
   1378      query,
   1379      suggestions: result,
   1380    };
   1381  }
   1382 
   1383  /**
   1384   * Add a pseudo-class lock to a node.
   1385   *
   1386   * @param NodeActor node
   1387   * @param string pseudo
   1388   *    A pseudoclass: ':hover', ':active', ':focus', ':focus-within'
   1389   * @param options
   1390   *    Options object:
   1391   *    `parents`: True if the pseudo-class should be added
   1392   *      to parent nodes.
   1393   *    `enabled`: False if the pseudo-class should be locked
   1394   *      to 'off'. Defaults to true.
   1395   *
   1396   * @returns An empty packet.  A "pseudoClassLock" mutation will
   1397   *    be queued for any changed nodes.
   1398   */
   1399  addPseudoClassLock(node, pseudo, options = {}) {
   1400    if (isNodeDead(node)) {
   1401      return;
   1402    }
   1403 
   1404    // There can be only one node locked per pseudo, so dismiss all existing
   1405    // ones
   1406    for (const locked of this._activePseudoClassLocks) {
   1407      if (InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)) {
   1408        this._removePseudoClassLock(locked, pseudo);
   1409      }
   1410    }
   1411 
   1412    const enabled = options.enabled === undefined || options.enabled;
   1413    this._addPseudoClassLock(node, pseudo, enabled);
   1414 
   1415    if (!options.parents) {
   1416      return;
   1417    }
   1418 
   1419    const walker = this.getDocumentWalker(node.rawNode);
   1420    let cur;
   1421    while ((cur = walker.parentNode())) {
   1422      const curNode = this._getOrCreateNodeActor(cur);
   1423      this._addPseudoClassLock(curNode, pseudo, enabled);
   1424    }
   1425  }
   1426 
   1427  _queuePseudoClassMutation(node) {
   1428    this.queueMutation({
   1429      target: node.actorID,
   1430      type: "pseudoClassLock",
   1431      pseudoClassLocks: node.writePseudoClassLocks(),
   1432    });
   1433  }
   1434 
   1435  _addPseudoClassLock(node, pseudo, enabled) {
   1436    if (node.rawNode.nodeType !== Node.ELEMENT_NODE) {
   1437      return false;
   1438    }
   1439    InspectorUtils.addPseudoClassLock(node.rawNode, pseudo, enabled);
   1440    this._activePseudoClassLocks.add(node);
   1441    this._queuePseudoClassMutation(node);
   1442    return true;
   1443  }
   1444 
   1445  hideNode(node) {
   1446    if (isNodeDead(node)) {
   1447      return;
   1448    }
   1449 
   1450    loadSheet(node.rawNode.ownerGlobal, HELPER_SHEET);
   1451    node.rawNode.classList.add(HIDDEN_CLASS);
   1452  }
   1453 
   1454  unhideNode(node) {
   1455    if (isNodeDead(node)) {
   1456      return;
   1457    }
   1458 
   1459    node.rawNode.classList.remove(HIDDEN_CLASS);
   1460  }
   1461 
   1462  /**
   1463   * Remove a pseudo-class lock from a node.
   1464   *
   1465   * @param NodeActor node
   1466   * @param string pseudo
   1467   *    A pseudoclass: ':hover', ':active', ':focus', ':focus-within'
   1468   * @param options
   1469   *    Options object:
   1470   *    `parents`: True if the pseudo-class should be removed
   1471   *      from parent nodes.
   1472   *
   1473   * @returns An empty response.  "pseudoClassLock" mutations
   1474   *    will be emitted for any changed nodes.
   1475   */
   1476  removePseudoClassLock(node, pseudo, options = {}) {
   1477    if (isNodeDead(node)) {
   1478      return;
   1479    }
   1480 
   1481    this._removePseudoClassLock(node, pseudo);
   1482 
   1483    // Remove pseudo class for children as we don't want to allow
   1484    // turning it on for some childs without setting it on some parents
   1485    for (const locked of this._activePseudoClassLocks) {
   1486      if (
   1487        node.rawNode.contains(locked.rawNode) &&
   1488        InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)
   1489      ) {
   1490        this._removePseudoClassLock(locked, pseudo);
   1491      }
   1492    }
   1493 
   1494    if (!options.parents) {
   1495      return;
   1496    }
   1497 
   1498    const walker = this.getDocumentWalker(node.rawNode);
   1499    let cur;
   1500    while ((cur = walker.parentNode())) {
   1501      const curNode = this._getOrCreateNodeActor(cur);
   1502      this._removePseudoClassLock(curNode, pseudo);
   1503    }
   1504  }
   1505 
   1506  _removePseudoClassLock(node, pseudo) {
   1507    if (node.rawNode.nodeType != Node.ELEMENT_NODE) {
   1508      return false;
   1509    }
   1510    InspectorUtils.removePseudoClassLock(node.rawNode, pseudo);
   1511    if (!node.writePseudoClassLocks()) {
   1512      this._activePseudoClassLocks.delete(node);
   1513    }
   1514 
   1515    this._queuePseudoClassMutation(node);
   1516    return true;
   1517  }
   1518 
   1519  /**
   1520   * Clear all the pseudo-classes on a given node or all nodes.
   1521   *
   1522   * @param {NodeActor} node Optional node to clear pseudo-classes on
   1523   */
   1524  clearPseudoClassLocks(node) {
   1525    if (node && isNodeDead(node)) {
   1526      return;
   1527    }
   1528 
   1529    if (node) {
   1530      InspectorUtils.clearPseudoClassLocks(node.rawNode);
   1531      this._activePseudoClassLocks.delete(node);
   1532      this._queuePseudoClassMutation(node);
   1533    } else {
   1534      for (const locked of this._activePseudoClassLocks) {
   1535        InspectorUtils.clearPseudoClassLocks(locked.rawNode);
   1536        this._activePseudoClassLocks.delete(locked);
   1537        this._queuePseudoClassMutation(locked);
   1538      }
   1539    }
   1540  }
   1541 
   1542  /**
   1543   * Get a node's innerHTML property.
   1544   */
   1545  innerHTML(node) {
   1546    let html = "";
   1547    if (!isNodeDead(node)) {
   1548      html = node.rawNode.innerHTML;
   1549    }
   1550    return new LongStringActor(this.conn, html);
   1551  }
   1552 
   1553  /**
   1554   * Set a node's innerHTML property.
   1555   *
   1556   * @param {NodeActor} node The node.
   1557   * @param {string} value The piece of HTML content.
   1558   */
   1559  setInnerHTML(node, value) {
   1560    if (isNodeDead(node)) {
   1561      return;
   1562    }
   1563 
   1564    const rawNode = node.rawNode;
   1565    if (
   1566      rawNode.nodeType !== rawNode.ownerDocument.ELEMENT_NODE &&
   1567      rawNode.nodeType !== rawNode.ownerDocument.DOCUMENT_FRAGMENT_NODE
   1568    ) {
   1569      throw new Error("Can only change innerHTML to element or fragment nodes");
   1570    }
   1571    // eslint-disable-next-line no-unsanitized/property
   1572    rawNode.innerHTML = value;
   1573  }
   1574 
   1575  /**
   1576   * Get a node's outerHTML property.
   1577   *
   1578   * @param {NodeActor} node The node.
   1579   */
   1580  outerHTML(node) {
   1581    let outerHTML = "";
   1582    if (!isNodeDead(node)) {
   1583      outerHTML = node.rawNode.outerHTML;
   1584    }
   1585    return new LongStringActor(this.conn, outerHTML);
   1586  }
   1587 
   1588  /**
   1589   * Set a node's outerHTML property.
   1590   *
   1591   * @param {NodeActor} node The node.
   1592   * @param {string} value The piece of HTML content.
   1593   */
   1594  setOuterHTML(node, value) {
   1595    if (isNodeDead(node)) {
   1596      return;
   1597    }
   1598 
   1599    const rawNode = node.rawNode;
   1600    const doc = nodeDocument(rawNode);
   1601    const win = doc.defaultView;
   1602    let parser;
   1603    if (!win) {
   1604      throw new Error("The window object shouldn't be null");
   1605    } else {
   1606      // We create DOMParser under window object because we want a content
   1607      // DOMParser, which means all the DOM objects created by this DOMParser
   1608      // will be in the same DocGroup as rawNode.parentNode. Then the newly
   1609      // created nodes can be adopted into rawNode.parentNode.
   1610      parser = new win.DOMParser();
   1611    }
   1612 
   1613    const mimeType = rawNode.tagName === "svg" ? "image/svg+xml" : "text/html";
   1614    const parsedDOM = parser.parseFromString(value, mimeType);
   1615    const parentNode = rawNode.parentNode;
   1616 
   1617    // Special case for head and body.  Setting document.body.outerHTML
   1618    // creates an extra <head> tag, and document.head.outerHTML creates
   1619    // an extra <body>.  So instead we will call replaceChild with the
   1620    // parsed DOM, assuming that they aren't trying to set both tags at once.
   1621    if (rawNode.tagName === "BODY") {
   1622      if (parsedDOM.head.innerHTML === "") {
   1623        parentNode.replaceChild(parsedDOM.body, rawNode);
   1624      } else {
   1625        // eslint-disable-next-line no-unsanitized/property
   1626        rawNode.outerHTML = value;
   1627      }
   1628    } else if (rawNode.tagName === "HEAD") {
   1629      if (parsedDOM.body.innerHTML === "") {
   1630        parentNode.replaceChild(parsedDOM.head, rawNode);
   1631      } else {
   1632        // eslint-disable-next-line no-unsanitized/property
   1633        rawNode.outerHTML = value;
   1634      }
   1635    } else if (node.isDocumentElement()) {
   1636      // Unable to set outerHTML on the document element. Fall back by
   1637      // setting attributes manually. Then replace all the child nodes.
   1638      const finalAttributeModifications = [];
   1639      const attributeModifications = {};
   1640      for (const attribute of rawNode.attributes) {
   1641        attributeModifications[attribute.name] = null;
   1642      }
   1643      for (const attribute of parsedDOM.documentElement.attributes) {
   1644        attributeModifications[attribute.name] = attribute.value;
   1645      }
   1646      for (const key in attributeModifications) {
   1647        finalAttributeModifications.push({
   1648          attributeName: key,
   1649          newValue: attributeModifications[key],
   1650        });
   1651      }
   1652      node.modifyAttributes(finalAttributeModifications);
   1653 
   1654      rawNode.replaceChildren(...parsedDOM.firstElementChild.childNodes);
   1655    } else {
   1656      // eslint-disable-next-line no-unsanitized/property
   1657      rawNode.outerHTML = value;
   1658    }
   1659  }
   1660 
   1661  /**
   1662   * Insert adjacent HTML to a node.
   1663   *
   1664   * @param {Node} node
   1665   * @param {string} position One of "beforeBegin", "afterBegin", "beforeEnd",
   1666   *                          "afterEnd" (see Element.insertAdjacentHTML).
   1667   * @param {string} value The HTML content.
   1668   */
   1669  insertAdjacentHTML(node, position, value) {
   1670    if (isNodeDead(node)) {
   1671      return { node: [], newParents: [] };
   1672    }
   1673 
   1674    const rawNode = node.rawNode;
   1675    const isInsertAsSibling =
   1676      position === "beforeBegin" || position === "afterEnd";
   1677 
   1678    // Don't insert anything adjacent to the document element.
   1679    if (isInsertAsSibling && node.isDocumentElement()) {
   1680      throw new Error("Can't insert adjacent element to the root.");
   1681    }
   1682 
   1683    const rawParentNode = rawNode.parentNode;
   1684    if (!rawParentNode && isInsertAsSibling) {
   1685      throw new Error("Can't insert as sibling without parent node.");
   1686    }
   1687 
   1688    // We can't use insertAdjacentHTML, because we want to return the nodes
   1689    // being created (so the front can remove them if the user undoes
   1690    // the change). So instead, use Range.createContextualFragment().
   1691    const range = rawNode.ownerDocument.createRange();
   1692    if (position === "beforeBegin" || position === "afterEnd") {
   1693      range.selectNode(rawNode);
   1694    } else {
   1695      range.selectNodeContents(rawNode);
   1696    }
   1697    // eslint-disable-next-line no-unsanitized/method
   1698    const docFrag = range.createContextualFragment(value);
   1699    const newRawNodes = Array.from(docFrag.childNodes);
   1700    switch (position) {
   1701      case "beforeBegin":
   1702        rawParentNode.insertBefore(docFrag, rawNode);
   1703        break;
   1704      case "afterEnd":
   1705        // Note: if the second argument is null, rawParentNode.insertBefore
   1706        // behaves like rawParentNode.appendChild.
   1707        rawParentNode.insertBefore(docFrag, rawNode.nextSibling);
   1708        break;
   1709      case "afterBegin":
   1710        rawNode.insertBefore(docFrag, rawNode.firstChild);
   1711        break;
   1712      case "beforeEnd":
   1713        rawNode.appendChild(docFrag);
   1714        break;
   1715      default:
   1716        throw new Error(
   1717          "Invalid position value. Must be either " +
   1718            "'beforeBegin', 'beforeEnd', 'afterBegin' or 'afterEnd'."
   1719        );
   1720    }
   1721 
   1722    return this.attachElements(newRawNodes);
   1723  }
   1724 
   1725  /**
   1726   * Duplicate a specified node
   1727   *
   1728   * @param {NodeActor} node The node to duplicate.
   1729   */
   1730  duplicateNode({ rawNode }) {
   1731    const clonedNode = rawNode.cloneNode(true);
   1732    rawNode.parentNode.insertBefore(clonedNode, rawNode.nextSibling);
   1733  }
   1734 
   1735  /**
   1736   * Test whether a node is a document or a document element.
   1737   *
   1738   * @param {NodeActor} node The node to remove.
   1739   * @return {boolean} True if the node is a document or a document element.
   1740   */
   1741  isDocumentOrDocumentElementNode(node) {
   1742    return (
   1743      (node.rawNode.ownerDocument &&
   1744        node.rawNode.ownerDocument.documentElement === this.rawNode) ||
   1745      node.rawNode.nodeType === Node.DOCUMENT_NODE
   1746    );
   1747  }
   1748 
   1749  /**
   1750   * Removes a node from its parent node.
   1751   *
   1752   * @param {NodeActor} node The node to remove.
   1753   * @returns The node's nextSibling before it was removed.
   1754   */
   1755  removeNode(node) {
   1756    if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
   1757      throw Error("Cannot remove document, document elements or dead nodes.");
   1758    }
   1759 
   1760    const nextSibling = this.nextSibling(node);
   1761    node.rawNode.remove();
   1762    // Mutation events will take care of the rest.
   1763    return nextSibling;
   1764  }
   1765 
   1766  /**
   1767   * Removes an array of nodes from their parent node.
   1768   *
   1769   * @param {NodeActor[]} nodes The nodes to remove.
   1770   */
   1771  removeNodes(nodes) {
   1772    // Check that all nodes are valid before processing the removals.
   1773    for (const node of nodes) {
   1774      if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
   1775        throw Error("Cannot remove document, document elements or dead nodes");
   1776      }
   1777    }
   1778 
   1779    for (const node of nodes) {
   1780      node.rawNode.remove();
   1781      // Mutation events will take care of the rest.
   1782    }
   1783  }
   1784 
   1785  /**
   1786   * Insert a node into the DOM.
   1787   */
   1788  insertBefore(node, parent, sibling) {
   1789    if (
   1790      isNodeDead(node) ||
   1791      isNodeDead(parent) ||
   1792      (sibling && isNodeDead(sibling))
   1793    ) {
   1794      return;
   1795    }
   1796 
   1797    const rawNode = node.rawNode;
   1798    const rawParent = parent.rawNode;
   1799    const rawSibling = sibling ? sibling.rawNode : null;
   1800 
   1801    // Don't bother inserting a node if the document position isn't going
   1802    // to change. This prevents needless iframes reloading and mutations.
   1803    if (rawNode.parentNode === rawParent) {
   1804      let currentNextSibling = this.nextSibling(node);
   1805      currentNextSibling = currentNextSibling
   1806        ? currentNextSibling.rawNode
   1807        : null;
   1808 
   1809      if (rawNode === rawSibling || currentNextSibling === rawSibling) {
   1810        return;
   1811      }
   1812    }
   1813 
   1814    rawParent.insertBefore(rawNode, rawSibling);
   1815  }
   1816 
   1817  /**
   1818   * Editing a node's tagname actually means creating a new node with the same
   1819   * attributes, removing the node and inserting the new one instead.
   1820   * This method does not return anything as mutation events are taking care of
   1821   * informing the consumers about changes.
   1822   */
   1823  editTagName(node, tagName) {
   1824    if (isNodeDead(node)) {
   1825      return null;
   1826    }
   1827 
   1828    const oldNode = node.rawNode;
   1829 
   1830    // Create a new element with the same attributes as the current element and
   1831    // prepare to replace the current node with it.
   1832    let newNode;
   1833    try {
   1834      newNode = nodeDocument(oldNode).createElement(tagName);
   1835    } catch (x) {
   1836      // Failed to create a new element with that tag name, ignore the change,
   1837      // and signal the error to the front.
   1838      return Promise.reject(
   1839        new Error("Could not change node's tagName to " + tagName)
   1840      );
   1841    }
   1842 
   1843    const attrs = oldNode.attributes;
   1844    for (let i = 0; i < attrs.length; i++) {
   1845      newNode.setAttribute(attrs[i].name, attrs[i].value);
   1846    }
   1847 
   1848    // Insert the new node, and transfer the old node's children.
   1849    oldNode.parentNode.insertBefore(newNode, oldNode);
   1850    while (oldNode.firstChild) {
   1851      newNode.appendChild(oldNode.firstChild);
   1852    }
   1853 
   1854    oldNode.remove();
   1855    return null;
   1856  }
   1857 
   1858  /**
   1859   * Gets the state of the mutation breakpoint types for this actor.
   1860   *
   1861   * @param {NodeActor} node The node to get breakpoint info for.
   1862   */
   1863  getMutationBreakpoints(node) {
   1864    let bps;
   1865    if (!isNodeDead(node)) {
   1866      bps = this._breakpointInfoForNode(node.rawNode);
   1867    }
   1868 
   1869    return (
   1870      bps || {
   1871        subtree: false,
   1872        removal: false,
   1873        attribute: false,
   1874      }
   1875    );
   1876  }
   1877 
   1878  /**
   1879   * Set the state of some subset of mutation breakpoint types for this actor.
   1880   *
   1881   * @param {NodeActor} node The node to set breakpoint info for.
   1882   * @param {object} bps A subset of the breakpoints for this actor that
   1883   *                            should be updated to new states.
   1884   */
   1885  setMutationBreakpoints(node, bps) {
   1886    if (isNodeDead(node)) {
   1887      return;
   1888    }
   1889    const rawNode = node.rawNode;
   1890 
   1891    if (
   1892      rawNode.ownerDocument &&
   1893      rawNode.getRootNode({ composed: true }) != rawNode.ownerDocument
   1894    ) {
   1895      // We only allow watching for mutations on nodes that are attached to
   1896      // documents. That allows us to clean up our mutation listeners when all
   1897      // of the watched nodes have been removed from the document.
   1898      return;
   1899    }
   1900 
   1901    // This argument has nullable fields so we want to only update boolean
   1902    // field values.
   1903    const bpsForNode = Object.keys(bps).reduce((obj, bp) => {
   1904      if (typeof bps[bp] === "boolean") {
   1905        obj[bp] = bps[bp];
   1906      }
   1907      return obj;
   1908    }, {});
   1909 
   1910    this._updateMutationBreakpointState("api", rawNode, {
   1911      ...this.getMutationBreakpoints(node),
   1912      ...bpsForNode,
   1913    });
   1914  }
   1915 
   1916  /**
   1917   * Update the mutation breakpoint state for the given DOM node.
   1918   *
   1919   * @param {Node} rawNode The DOM node.
   1920   * @param {object} bpsForNode The state of each mutation bp type we support.
   1921   */
   1922  _updateMutationBreakpointState(mutationReason, rawNode, bpsForNode) {
   1923    const rawDoc = rawNode.ownerDocument || rawNode;
   1924 
   1925    const docMutationBreakpoints = this._mutationBreakpointsForDoc(
   1926      rawDoc,
   1927      true /* createIfNeeded */
   1928    );
   1929    let originalBpsForNode = this._breakpointInfoForNode(rawNode);
   1930 
   1931    if (!bpsForNode && !originalBpsForNode) {
   1932      return;
   1933    }
   1934 
   1935    bpsForNode = bpsForNode || {};
   1936    originalBpsForNode = originalBpsForNode || {};
   1937 
   1938    if (Object.values(bpsForNode).some(Boolean)) {
   1939      docMutationBreakpoints.nodes.set(rawNode, bpsForNode);
   1940    } else {
   1941      docMutationBreakpoints.nodes.delete(rawNode);
   1942    }
   1943    if (originalBpsForNode.subtree && !bpsForNode.subtree) {
   1944      docMutationBreakpoints.counts.subtree -= 1;
   1945    } else if (!originalBpsForNode.subtree && bpsForNode.subtree) {
   1946      docMutationBreakpoints.counts.subtree += 1;
   1947    }
   1948 
   1949    if (originalBpsForNode.removal && !bpsForNode.removal) {
   1950      docMutationBreakpoints.counts.removal -= 1;
   1951    } else if (!originalBpsForNode.removal && bpsForNode.removal) {
   1952      docMutationBreakpoints.counts.removal += 1;
   1953    }
   1954 
   1955    if (originalBpsForNode.attribute && !bpsForNode.attribute) {
   1956      docMutationBreakpoints.counts.attribute -= 1;
   1957    } else if (!originalBpsForNode.attribute && bpsForNode.attribute) {
   1958      docMutationBreakpoints.counts.attribute += 1;
   1959    }
   1960 
   1961    this._updateDocumentMutationListeners(rawDoc);
   1962 
   1963    const actor = this.getNode(rawNode);
   1964    if (actor) {
   1965      this.queueMutation({
   1966        target: actor.actorID,
   1967        type: "mutationBreakpoint",
   1968        mutationBreakpoints: this.getMutationBreakpoints(actor),
   1969        mutationReason,
   1970      });
   1971    }
   1972  }
   1973 
   1974  /**
   1975   * Controls whether this DOM document has event listeners attached for
   1976   * handling of DOM mutation breakpoints.
   1977   *
   1978   * @param {Document} rawDoc The DOM document.
   1979   */
   1980  _updateDocumentMutationListeners(rawDoc) {
   1981    const docMutationBreakpoints = this._mutationBreakpointsForDoc(rawDoc);
   1982    if (!docMutationBreakpoints) {
   1983      rawDoc.devToolsWatchingDOMMutations = false;
   1984      return;
   1985    }
   1986 
   1987    const anyBreakpoint =
   1988      docMutationBreakpoints.counts.subtree > 0 ||
   1989      docMutationBreakpoints.counts.removal > 0 ||
   1990      docMutationBreakpoints.counts.attribute > 0;
   1991 
   1992    rawDoc.devToolsWatchingDOMMutations = anyBreakpoint;
   1993 
   1994    if (docMutationBreakpoints.counts.subtree > 0) {
   1995      this.chromeEventHandler.addEventListener(
   1996        "devtoolschildinserted",
   1997        this.onSubtreeModified,
   1998        true /* capture */
   1999      );
   2000    } else {
   2001      this.chromeEventHandler.removeEventListener(
   2002        "devtoolschildinserted",
   2003        this.onSubtreeModified,
   2004        true /* capture */
   2005      );
   2006    }
   2007 
   2008    if (anyBreakpoint) {
   2009      this.chromeEventHandler.addEventListener(
   2010        "devtoolschildremoved",
   2011        this.onNodeRemoved,
   2012        true /* capture */
   2013      );
   2014    } else {
   2015      this.chromeEventHandler.removeEventListener(
   2016        "devtoolschildremoved",
   2017        this.onNodeRemoved,
   2018        true /* capture */
   2019      );
   2020    }
   2021 
   2022    if (docMutationBreakpoints.counts.attribute > 0) {
   2023      this.chromeEventHandler.addEventListener(
   2024        "devtoolsattrmodified",
   2025        this.onAttributeModified,
   2026        true /* capture */
   2027      );
   2028    } else {
   2029      this.chromeEventHandler.removeEventListener(
   2030        "devtoolsattrmodified",
   2031        this.onAttributeModified,
   2032        true /* capture */
   2033      );
   2034    }
   2035  }
   2036 
   2037  _breakOnMutation(mutationType, targetNode, ancestorNode, action) {
   2038    this.targetActor.threadActor.pauseForMutationBreakpoint(
   2039      mutationType,
   2040      targetNode,
   2041      ancestorNode,
   2042      action
   2043    );
   2044  }
   2045 
   2046  _mutationBreakpointsForDoc(rawDoc, createIfNeeded = false) {
   2047    let docMutationBreakpoints = this._mutationBreakpoints.get(rawDoc);
   2048    if (!docMutationBreakpoints && createIfNeeded) {
   2049      docMutationBreakpoints = {
   2050        counts: {
   2051          subtree: 0,
   2052          removal: 0,
   2053          attribute: 0,
   2054        },
   2055        nodes: new Map(),
   2056      };
   2057      this._mutationBreakpoints.set(rawDoc, docMutationBreakpoints);
   2058    }
   2059    return docMutationBreakpoints;
   2060  }
   2061 
   2062  _breakpointInfoForNode(target) {
   2063    const docMutationBreakpoints = this._mutationBreakpointsForDoc(
   2064      target.ownerDocument || target
   2065    );
   2066    return (
   2067      (docMutationBreakpoints && docMutationBreakpoints.nodes.get(target)) ||
   2068      null
   2069    );
   2070  }
   2071 
   2072  onNodeRemoved(evt) {
   2073    const mutationBpInfo = this._breakpointInfoForNode(evt.target);
   2074    const hasNodeRemovalEvent = mutationBpInfo?.removal;
   2075 
   2076    this._clearMutationBreakpointsFromSubtree(evt.target);
   2077    if (hasNodeRemovalEvent) {
   2078      this._breakOnMutation("nodeRemoved", evt.target);
   2079    } else {
   2080      this.onSubtreeModified(evt);
   2081    }
   2082  }
   2083 
   2084  onAttributeModified(evt) {
   2085    const mutationBpInfo = this._breakpointInfoForNode(evt.target);
   2086    if (mutationBpInfo?.attribute) {
   2087      this._breakOnMutation("attributeModified", evt.target);
   2088    }
   2089  }
   2090 
   2091  onSubtreeModified(evt) {
   2092    const action = evt.type === "devtoolschildinserted" ? "add" : "remove";
   2093    let node = evt.target;
   2094    if (node.isNativeAnonymous && !this.showAllAnonymousContent) {
   2095      return;
   2096    }
   2097    while ((node = node.parentNode) !== null) {
   2098      const mutationBpInfo = this._breakpointInfoForNode(node);
   2099      if (mutationBpInfo?.subtree) {
   2100        this._breakOnMutation("subtreeModified", evt.target, node, action);
   2101        break;
   2102      }
   2103    }
   2104  }
   2105 
   2106  _clearMutationBreakpointsFromSubtree(targetNode) {
   2107    const targetDoc = targetNode.ownerDocument || targetNode;
   2108    const docMutationBreakpoints = this._mutationBreakpointsForDoc(targetDoc);
   2109    if (!docMutationBreakpoints || docMutationBreakpoints.nodes.size === 0) {
   2110      // Bail early for performance. If the doc has no mutation BPs, there is
   2111      // no reason to iterate through the children looking for things to detach.
   2112      return;
   2113    }
   2114 
   2115    // The walker is not limited to the subtree of the argument node, so we
   2116    // need to ensure that we stop walking when we leave the subtree.
   2117    const nextWalkerSibling = this._getNextTraversalSibling(targetNode);
   2118 
   2119    const walker = new DocumentWalker(targetNode, this.rootWin, {
   2120      filter: noAnonymousContentTreeWalkerFilter,
   2121      skipTo: SKIP_TO_SIBLING,
   2122    });
   2123 
   2124    do {
   2125      this._updateMutationBreakpointState("detach", walker.currentNode, null);
   2126    } while (walker.nextNode() && walker.currentNode !== nextWalkerSibling);
   2127  }
   2128 
   2129  _getNextTraversalSibling(targetNode) {
   2130    const walker = new DocumentWalker(targetNode, this.rootWin, {
   2131      filter: noAnonymousContentTreeWalkerFilter,
   2132      skipTo: SKIP_TO_SIBLING,
   2133    });
   2134 
   2135    while (!walker.nextSibling()) {
   2136      if (!walker.parentNode()) {
   2137        // If we try to step past the walker root, there is no next sibling.
   2138        return null;
   2139      }
   2140    }
   2141    return walker.currentNode;
   2142  }
   2143 
   2144  /**
   2145   * Get any pending mutation records.  Must be called by the client after
   2146   * the `new-mutations` notification is received.  Returns an array of
   2147   * mutation records.
   2148   *
   2149   * Mutation records have a basic structure:
   2150   *
   2151   * {
   2152   *   type: attributes|characterData|childList,
   2153   *   target: <domnode actor ID>,
   2154   * }
   2155   *
   2156   * And additional attributes based on the mutation type:
   2157   *
   2158   * `attributes` type:
   2159   *   attributeName: <string> - the attribute that changed
   2160   *   attributeNamespace: <string> - the attribute's namespace URI, if any.
   2161   *   newValue: <string> - The new value of the attribute, if any.
   2162   *
   2163   * `characterData` type:
   2164   *   newValue: <string> - the new nodeValue for the node
   2165   *
   2166   * `childList` type is returned when the set of children for a node
   2167   * has changed.  Includes extra data, which can be used by the client to
   2168   * maintain its ownership subtree.
   2169   *
   2170   *   added: array of <domnode actor ID> - The list of actors *previously
   2171   *     seen by the client* that were added to the target node.
   2172   *   removed: array of <domnode actor ID> The list of actors *previously
   2173   *     seen by the client* that were removed from the target node.
   2174   *   inlineTextChild: If the node now has a single text child, it will
   2175   *     be sent here.
   2176   *
   2177   * Actors that are included in a MutationRecord's `removed` but
   2178   * not in an `added` have been removed from the client's ownership
   2179   * tree (either by being moved under a node the client has seen yet
   2180   * or by being removed from the tree entirely), and is considered
   2181   * 'orphaned'.
   2182   *
   2183   * Keep in mind that if a node that the client hasn't seen is moved
   2184   * into or out of the target node, it will not be included in the
   2185   * removedNodes and addedNodes list, so if the client is interested
   2186   * in the new set of children it needs to issue a `children` request.
   2187   */
   2188  getMutations(options = {}) {
   2189    const pending = this._pendingMutations || [];
   2190    this._pendingMutations = [];
   2191    this._waitingForGetMutations = false;
   2192 
   2193    if (options.cleanup) {
   2194      for (const node of this._orphaned) {
   2195        // Release the orphaned node.  Nodes or children that have been
   2196        // retained will be moved to this._retainedOrphans.
   2197        this.releaseNode(node);
   2198      }
   2199      this._orphaned = new Set();
   2200    }
   2201 
   2202    return pending;
   2203  }
   2204 
   2205  queueMutation(mutation) {
   2206    if (!this.actorID || this._destroyed) {
   2207      // We've been destroyed, don't bother queueing this mutation.
   2208      return;
   2209    }
   2210 
   2211    // Add the mutation to the list of mutations to be retrieved next.
   2212    this._pendingMutations.push(mutation);
   2213 
   2214    // Bail out if we already emitted a new-mutations event and are waiting for a client
   2215    // to retrieve them.
   2216    if (this._waitingForGetMutations) {
   2217      return;
   2218    }
   2219 
   2220    if (IMMEDIATE_MUTATIONS.includes(mutation.type)) {
   2221      this._emitNewMutations();
   2222    } else {
   2223      /**
   2224       * If many mutations are fired at the same time, clients might sequentially request
   2225       * children/siblings for updated nodes, which can be costly. By throttling the calls
   2226       * to getMutations, duplicated mutations will be ignored.
   2227       */
   2228      this._throttledEmitNewMutations();
   2229    }
   2230  }
   2231 
   2232  _emitNewMutations() {
   2233    if (!this.actorID || this._destroyed) {
   2234      // Bail out if the actor was destroyed after throttling this call.
   2235      return;
   2236    }
   2237 
   2238    if (this._waitingForGetMutations || !this._pendingMutations.length) {
   2239      // Bail out if we already fired the new-mutation event or if no mutations are
   2240      // waiting to be retrieved.
   2241      return;
   2242    }
   2243 
   2244    this._waitingForGetMutations = true;
   2245    this.emit("new-mutations");
   2246  }
   2247 
   2248  /**
   2249   * Handles mutations from the DOM mutation observer API.
   2250   *
   2251   * @param array[MutationRecord] mutations
   2252   *    See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord
   2253   */
   2254  onMutations(mutations) {
   2255    // Don't send a mutation event if the mutation target would be ignored by the walker
   2256    // filter function.
   2257    if (
   2258      mutations.every(
   2259        mutation =>
   2260          this.documentWalkerFilter(mutation.target) ===
   2261          nodeFilterConstants.FILTER_SKIP
   2262      )
   2263    ) {
   2264      return;
   2265    }
   2266 
   2267    // Notify any observers that want *all* mutations (even on nodes that aren't
   2268    // referenced).  This is not sent over the protocol so can only be used by
   2269    // scripts running in the server process.
   2270    this.emit("any-mutation");
   2271 
   2272    for (const change of mutations) {
   2273      const targetActor = this.getNode(change.target);
   2274      if (!targetActor) {
   2275        continue;
   2276      }
   2277      const targetNode = change.target;
   2278      const type = change.type;
   2279      const mutation = {
   2280        type,
   2281        target: targetActor.actorID,
   2282      };
   2283 
   2284      if (type === "attributes") {
   2285        mutation.attributeName = change.attributeName;
   2286        mutation.attributeNamespace = change.attributeNamespace || undefined;
   2287        mutation.newValue = targetNode.hasAttribute(mutation.attributeName)
   2288          ? targetNode.getAttribute(mutation.attributeName)
   2289          : null;
   2290      } else if (type === "characterData") {
   2291        mutation.newValue = targetNode.nodeValue;
   2292        this._maybeQueueInlineTextChildMutation(change, targetNode);
   2293      } else if (type === "childList") {
   2294        // Get the list of removed and added actors that the client has seen
   2295        // so that it can keep its ownership tree up to date.
   2296        const removedActors = [];
   2297        const addedActors = [];
   2298        for (const removed of change.removedNodes) {
   2299          this._onMutationsNode(removed, removedActors, "removed");
   2300        }
   2301        for (const added of change.addedNodes) {
   2302          this._onMutationsNode(added, addedActors, "added");
   2303        }
   2304 
   2305        mutation.numChildren = targetActor.numChildren;
   2306        mutation.removed = removedActors;
   2307        mutation.added = addedActors;
   2308 
   2309        const inlineTextChild = this.inlineTextChild(targetActor.rawNode);
   2310        if (inlineTextChild) {
   2311          mutation.inlineTextChild = inlineTextChild.form();
   2312        }
   2313      }
   2314      this.queueMutation(mutation);
   2315    }
   2316  }
   2317 
   2318  /**
   2319   * Handle a mutation on a node
   2320   *
   2321   * @param {Element} node
   2322   *        The element that is added/removed in the mutation
   2323   * @param {NodeActor[]} actors
   2324   *        An array that will be populated by this function with the node actors that
   2325   *        were added
   2326   * @param {string} mutationType
   2327   *        The type of mutation we're handlign ("added" or "removed")
   2328   */
   2329  _onMutationsNode(node, actors, mutationType) {
   2330    if (mutationType !== "added" && mutationType !== "removed") {
   2331      console.error("Unknown mutation type", mutationType);
   2332      return;
   2333    }
   2334 
   2335    const actor = this.getNode(node);
   2336    if (actor) {
   2337      actors.push(actor.actorID);
   2338      if (mutationType === "added") {
   2339        // The actor is reconnected to the ownership tree, unorphan
   2340        // it and let the client know so that its ownership tree is up
   2341        // to date.
   2342        this._orphaned.delete(actor);
   2343        return;
   2344      }
   2345      if (mutationType === "removed") {
   2346        // While removed from the tree, nodes are saved as orphaned.
   2347        this._orphaned.add(actor);
   2348        return;
   2349      }
   2350    }
   2351 
   2352    // Here, we might be in a case where a node is remove/added for which we don't have an
   2353    // actor for, but do have actors for its children.
   2354    if (
   2355      this.documentWalkerFilter(node) !==
   2356      nodeFilterConstants.FILTER_ACCEPT_CHILDREN
   2357    ) {
   2358      // At this point, the client never encountered this actor and the node wasn't ignored,
   2359      // so we don't need to tell it about this mutation.
   2360      // For added node, if the client wants to see the new nodes it can ask for children.
   2361      return;
   2362    }
   2363 
   2364    // Otherwise, the node was ignored, so we need to go over its children to find
   2365    // actor references we might have.
   2366    for (const child of this._rawChildren(node)) {
   2367      this._onMutationsNode(child, actors, mutationType);
   2368    }
   2369  }
   2370 
   2371  /**
   2372   * Check if the provided mutation could change the way the target element is
   2373   * inlined with its parent node. If it might, a custom mutation of type
   2374   * "inlineTextChild" will be queued.
   2375   *
   2376   * @param {MutationRecord} mutation
   2377   *        A characterData type mutation
   2378   */
   2379  _maybeQueueInlineTextChildMutation(mutation) {
   2380    const { oldValue, target } = mutation;
   2381    const newValue = target.nodeValue;
   2382    const limit = gValueSummaryLength;
   2383 
   2384    if (
   2385      (oldValue.length <= limit && newValue.length <= limit) ||
   2386      (oldValue.length > limit && newValue.length > limit)
   2387    ) {
   2388      // Bail out if the new & old values are both below/above the size limit.
   2389      return;
   2390    }
   2391 
   2392    const parentActor = this.getNode(target.parentNode);
   2393    if (!parentActor || parentActor.rawNode.children.length) {
   2394      // If the parent node has other children, a character data mutation will
   2395      // not change anything regarding inlining text nodes.
   2396      return;
   2397    }
   2398 
   2399    const inlineTextChild = this.inlineTextChild(parentActor.rawNode);
   2400    this.queueMutation({
   2401      type: "inlineTextChild",
   2402      target: parentActor.actorID,
   2403      inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,
   2404    });
   2405  }
   2406 
   2407  onSlotchange(event) {
   2408    const target = event.target;
   2409    const targetActor = this.getNode(target);
   2410    if (!targetActor) {
   2411      return;
   2412    }
   2413 
   2414    this.queueMutation({
   2415      type: "slotchange",
   2416      target: targetActor.actorID,
   2417    });
   2418  }
   2419 
   2420  /**
   2421   * Fires when an anonymous root is created.
   2422   * This is needed because regular mutation observers don't fire on some kinds
   2423   * of NAC creation. We want to treat this like a regular insertion.
   2424   */
   2425  onAnonymousrootcreated(event) {
   2426    const root = event.target;
   2427 
   2428    // Don't trigger a mutation if the document walker would filter out the element.
   2429    if (this.documentWalkerFilter(root) === nodeFilterConstants.FILTER_SKIP) {
   2430      return;
   2431    }
   2432 
   2433    const parent = this.rawParentNode(root);
   2434    if (!parent) {
   2435      // These events are async. The node might have been removed already, in
   2436      // which case there's nothing to do anymore.
   2437      return;
   2438    }
   2439    // By the time onAnonymousrootremoved fires, the node is already detached
   2440    // from its parent, so we need to remember it by hand.
   2441    this._anonParents.set(root, parent);
   2442    this.onMutations([
   2443      {
   2444        type: "childList",
   2445        target: parent,
   2446        addedNodes: [root],
   2447        removedNodes: [],
   2448      },
   2449    ]);
   2450  }
   2451 
   2452  /**
   2453   * @see onAnonymousrootcreated
   2454   */
   2455  onAnonymousrootremoved(event) {
   2456    const root = event.target;
   2457    const parent = this._anonParents.get(root);
   2458    if (!parent) {
   2459      return;
   2460    }
   2461    this._anonParents.delete(root);
   2462    this.onMutations([
   2463      {
   2464        type: "childList",
   2465        target: parent,
   2466        addedNodes: [],
   2467        removedNodes: [root],
   2468      },
   2469    ]);
   2470  }
   2471 
   2472  onShadowrootattached(event) {
   2473    const actor = this.getNode(event.target);
   2474    if (!actor) {
   2475      return;
   2476    }
   2477 
   2478    const mutation = {
   2479      type: "shadowRootAttached",
   2480      target: actor.actorID,
   2481    };
   2482    this.queueMutation(mutation);
   2483  }
   2484 
   2485  onFrameLoad({ window, isTopLevel }) {
   2486    // By the time we receive the DOMContentLoaded event, we might have been destroyed
   2487    if (this._destroyed) {
   2488      return;
   2489    }
   2490    const { readyState } = window.document;
   2491    if (readyState != "interactive" && readyState != "complete") {
   2492      // The document is not loaded, so we want to register to fire again when the
   2493      // DOM has been loaded.
   2494      window.addEventListener(
   2495        "DOMContentLoaded",
   2496        this.onFrameLoad.bind(this, { window, isTopLevel }),
   2497        { once: true }
   2498      );
   2499      return;
   2500    }
   2501 
   2502    window.document.shadowRootAttachedEventEnabled = true;
   2503 
   2504    if (isTopLevel) {
   2505      // If we initialize the inspector while the document is loading,
   2506      // we may already have a root document set in the constructor.
   2507      if (
   2508        this.rootDoc &&
   2509        this.rootDoc !== window.document &&
   2510        !Cu.isDeadWrapper(this.rootDoc) &&
   2511        this.rootDoc.defaultView
   2512      ) {
   2513        this.onFrameUnload({ window: this.rootDoc.defaultView });
   2514      }
   2515      // Update all DOM objects references to target the new document.
   2516      this.rootWin = window;
   2517      this.rootDoc = window.document;
   2518      this.rootNode = this.document();
   2519      this.emit("root-available", this.rootNode);
   2520    } else {
   2521      const frame = getFrameElement(window);
   2522      const frameActor = this.getNode(frame);
   2523      if (frameActor) {
   2524        // If the parent frame is in the map of known node actors, create the
   2525        // actor for the new document and emit a root-available event.
   2526        const documentActor = this._getOrCreateNodeActor(window.document);
   2527        this.emit("root-available", documentActor);
   2528      }
   2529    }
   2530  }
   2531 
   2532  // Returns true if domNode is in window or a subframe.
   2533  _childOfWindow(window, domNode) {
   2534    while (domNode) {
   2535      const win = nodeDocument(domNode).defaultView;
   2536      if (win === window) {
   2537        return true;
   2538      }
   2539      domNode = getFrameElement(win);
   2540    }
   2541    return false;
   2542  }
   2543 
   2544  onFrameUnload({ window }) {
   2545    // Any retained orphans that belong to this document
   2546    // or its children need to be released, and a mutation sent
   2547    // to notify of that.
   2548    const releasedOrphans = [];
   2549 
   2550    for (const retained of this._retainedOrphans) {
   2551      if (
   2552        Cu.isDeadWrapper(retained.rawNode) ||
   2553        this._childOfWindow(window, retained.rawNode)
   2554      ) {
   2555        this._retainedOrphans.delete(retained);
   2556        releasedOrphans.push(retained.actorID);
   2557        this.releaseNode(retained, { force: true });
   2558      }
   2559    }
   2560 
   2561    if (releasedOrphans.length) {
   2562      this.queueMutation({
   2563        target: this.rootNode.actorID,
   2564        type: "unretained",
   2565        nodes: releasedOrphans,
   2566      });
   2567    }
   2568 
   2569    const doc = window.document;
   2570    const documentActor = this.getNode(doc);
   2571    if (!documentActor) {
   2572      return;
   2573    }
   2574 
   2575    // Removing a frame also removes any mutation breakpoints set on that
   2576    // document so that clients can clear their set of active breakpoints.
   2577    const mutationBps = this._mutationBreakpointsForDoc(doc);
   2578    const nodes = mutationBps ? Array.from(mutationBps.nodes.keys()) : [];
   2579    for (const node of nodes) {
   2580      this._updateMutationBreakpointState("unload", node, null);
   2581    }
   2582 
   2583    this.emit("root-destroyed", documentActor);
   2584 
   2585    // Cleanup root doc references if we just unloaded the top level root
   2586    // document.
   2587    if (this.rootDoc === doc) {
   2588      this.rootDoc = null;
   2589      this.rootNode = null;
   2590    }
   2591 
   2592    // Release the actor for the unloaded document.
   2593    this.releaseNode(documentActor, { force: true });
   2594  }
   2595 
   2596  /**
   2597   * Check if a node is attached to the DOM tree of the current page.
   2598   *
   2599   * @param {Node} rawNode
   2600   * @return {boolean} false if the node is removed from the tree or within a
   2601   * document fragment
   2602   */
   2603  _isInDOMTree(rawNode) {
   2604    let walker;
   2605    try {
   2606      walker = this.getDocumentWalker(rawNode);
   2607    } catch (e) {
   2608      // The DocumentWalker may throw NS_ERROR_ILLEGAL_VALUE when the node isn't found as a legit children of its parent
   2609      // ex: <iframe> manually added as immediate child of another <iframe>
   2610      if (e.name == "NS_ERROR_ILLEGAL_VALUE") {
   2611        return false;
   2612      }
   2613      throw e;
   2614    }
   2615    let current = walker.currentNode;
   2616 
   2617    // Reaching the top of tree
   2618    while (walker.parentNode()) {
   2619      current = walker.currentNode;
   2620    }
   2621 
   2622    // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't
   2623    // attached
   2624    if (
   2625      current.nodeType === Node.DOCUMENT_FRAGMENT_NODE ||
   2626      current !== this.rootDoc
   2627    ) {
   2628      return false;
   2629    }
   2630 
   2631    // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc
   2632    return true;
   2633  }
   2634 
   2635  /**
   2636   * @see _isInDomTree
   2637   */
   2638  isInDOMTree(node) {
   2639    if (isNodeDead(node)) {
   2640      return false;
   2641    }
   2642    return this._isInDOMTree(node.rawNode);
   2643  }
   2644 
   2645  /**
   2646   * Given a windowID return the NodeActor for the corresponding frameElement,
   2647   * unless it's the root window
   2648   */
   2649  getNodeActorFromWindowID(windowID) {
   2650    let win;
   2651 
   2652    try {
   2653      win = Services.wm.getOuterWindowWithId(windowID);
   2654    } catch (e) {
   2655      // ignore
   2656    }
   2657 
   2658    if (!win) {
   2659      return {
   2660        error: "noWindow",
   2661        message: "The related docshell is destroyed or not found",
   2662      };
   2663    } else if (!win.frameElement) {
   2664      // the frame element of the root document is privileged & thus
   2665      // inaccessible, so return the document body/element instead
   2666      return this.attachElement(
   2667        win.document.body || win.document.documentElement
   2668      );
   2669    }
   2670 
   2671    return this.attachElement(win.frameElement);
   2672  }
   2673 
   2674  /**
   2675   * Given a contentDomReference return the NodeActor for the corresponding frameElement.
   2676   */
   2677  getNodeActorFromContentDomReference(contentDomReference) {
   2678    let rawNode = lazy.ContentDOMReference.resolve(contentDomReference);
   2679    if (!rawNode || !this._isInDOMTree(rawNode)) {
   2680      return null;
   2681    }
   2682 
   2683    // This is a special case for the document object whereby it is considered
   2684    // as document.documentElement (the <html> node)
   2685    if (rawNode.defaultView && rawNode === rawNode.defaultView.document) {
   2686      rawNode = rawNode.documentElement;
   2687    }
   2688 
   2689    return this.attachElement(rawNode);
   2690  }
   2691 
   2692  /**
   2693   * Given a StyleSheet resource ID, commonly used in the style-editor, get its
   2694   * ownerNode and return the corresponding walker's NodeActor.
   2695   * Note that getNodeFromActor was added later and can now be used instead.
   2696   */
   2697  getStyleSheetOwnerNode(resourceId) {
   2698    const manager = this.targetActor.getStyleSheetsManager();
   2699    const ownerNode = manager.getOwnerNode(resourceId);
   2700    return this.attachElement(ownerNode);
   2701  }
   2702 
   2703  /**
   2704   * This method can be used to retrieve NodeActor for DOM nodes from other
   2705   * actors in a way that they can later be highlighted in the page, or
   2706   * selected in the inspector.
   2707   * If an actor has a reference to a DOM node, and the UI needs to know about
   2708   * this DOM node (and possibly select it in the inspector), the UI should
   2709   * first retrieve a reference to the walkerFront:
   2710   *
   2711   * // Make sure the inspector/walker have been initialized first.
   2712   * const inspectorFront = await toolbox.target.getFront("inspector");
   2713   * // Retrieve the walker.
   2714   * const walker = inspectorFront.walker;
   2715   *
   2716   * And then call this method:
   2717   *
   2718   * // Get the nodeFront from my actor, passing the ID and properties path.
   2719   * walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => {
   2720   *   // Use the nodeFront, e.g. select the node in the inspector.
   2721   *   toolbox.getPanel("inspector").selection.setNodeFront(nodeFront);
   2722   * });
   2723   *
   2724   * @param {string} actorID The ID for the actor that has a reference to the
   2725   * DOM node.
   2726   * @param {Array} path Where, on the actor, is the DOM node stored. If in the
   2727   * scope of the actor, the node is available as `this.data.node`, then this
   2728   * should be ["data", "node"].
   2729   * @return {NodeActor} The attached NodeActor, or null if it couldn't be
   2730   * found.
   2731   */
   2732  getNodeFromActor(actorID, path) {
   2733    const actor = this.conn.getActor(actorID);
   2734    if (!actor) {
   2735      return null;
   2736    }
   2737 
   2738    let obj = actor;
   2739    for (const name of path) {
   2740      if (!(name in obj)) {
   2741        return null;
   2742      }
   2743      obj = obj[name];
   2744    }
   2745 
   2746    return this.attachElement(obj);
   2747  }
   2748 
   2749  /**
   2750   * Returns an instance of the LayoutActor that is used to retrieve CSS layout-related
   2751   * information.
   2752   *
   2753   * @return {LayoutActor}
   2754   */
   2755  getLayoutInspector() {
   2756    if (!this.layoutActor) {
   2757      this.layoutActor = new LayoutActor(this.conn, this.targetActor, this);
   2758    }
   2759 
   2760    return this.layoutActor;
   2761  }
   2762 
   2763  /**
   2764   * Returns the parent grid DOMNode of the given node if it exists, otherwise, it
   2765   * returns null.
   2766   */
   2767  getParentGridNode(node) {
   2768    if (isNodeDead(node)) {
   2769      return null;
   2770    }
   2771 
   2772    const parentGridNode = findGridParentContainerForNode(node.rawNode);
   2773    return parentGridNode ? this._getOrCreateNodeActor(parentGridNode) : null;
   2774  }
   2775 
   2776  /**
   2777   * Returns the offset parent DOMNode of the given node if it exists, otherwise, it
   2778   * returns null.
   2779   */
   2780  getOffsetParent(node) {
   2781    if (isNodeDead(node)) {
   2782      return null;
   2783    }
   2784 
   2785    const offsetParent = node.rawNode.offsetParent;
   2786 
   2787    if (!offsetParent) {
   2788      return null;
   2789    }
   2790 
   2791    return this._getOrCreateNodeActor(offsetParent);
   2792  }
   2793 
   2794  getEmbedderElement(browsingContextID) {
   2795    const browsingContext = BrowsingContext.get(browsingContextID);
   2796    let rawNode = browsingContext.embedderElement;
   2797    if (!this._isInDOMTree(rawNode)) {
   2798      return null;
   2799    }
   2800 
   2801    // This is a special case for the document object whereby it is considered
   2802    // as document.documentElement (the <html> node)
   2803    if (rawNode.defaultView && rawNode === rawNode.defaultView.document) {
   2804      rawNode = rawNode.documentElement;
   2805    }
   2806 
   2807    return this.attachElement(rawNode);
   2808  }
   2809 
   2810  pick(doFocus, isLocalTab) {
   2811    this.nodePicker.pick(doFocus, isLocalTab);
   2812  }
   2813 
   2814  cancelPick() {
   2815    this.nodePicker.cancelPick();
   2816  }
   2817 
   2818  clearPicker() {
   2819    this.nodePicker.resetHoveredNodeReference();
   2820  }
   2821 
   2822  /**
   2823   * Given a scrollable node, find its descendants which are causing overflow in it and
   2824   * add their raw nodes to the map as keys with the scrollable element as the values.
   2825   *
   2826   * @param {NodeActor} scrollableNode A scrollable node.
   2827   * @param {Map} map The map to which the overflow causing elements are added.
   2828   */
   2829  updateOverflowCausingElements(scrollableNode, map) {
   2830    if (
   2831      isNodeDead(scrollableNode) ||
   2832      scrollableNode.rawNode.nodeType !== Node.ELEMENT_NODE
   2833    ) {
   2834      return;
   2835    }
   2836 
   2837    const overflowCausingChildren = [
   2838      ...InspectorUtils.getOverflowingChildrenOfElement(scrollableNode.rawNode),
   2839    ];
   2840 
   2841    for (let overflowCausingChild of overflowCausingChildren) {
   2842      // overflowCausingChild is a Node, but not necessarily an Element.
   2843      // So, get the containing Element
   2844      if (overflowCausingChild.nodeType !== Node.ELEMENT_NODE) {
   2845        overflowCausingChild = overflowCausingChild.parentElement;
   2846      }
   2847      map.set(overflowCausingChild, scrollableNode);
   2848    }
   2849  }
   2850 
   2851  /**
   2852   * Returns an array of the overflow causing elements' NodeActor for the given node.
   2853   *
   2854   * @param {NodeActor} node The scrollable node.
   2855   * @return {Array<NodeActor>} An array of the overflow causing elements.
   2856   */
   2857  getOverflowCausingElements(node) {
   2858    if (
   2859      isNodeDead(node) ||
   2860      node.rawNode.nodeType !== Node.ELEMENT_NODE ||
   2861      !node.isScrollable
   2862    ) {
   2863      return [];
   2864    }
   2865 
   2866    const overflowCausingElements = [
   2867      ...InspectorUtils.getOverflowingChildrenOfElement(node.rawNode),
   2868    ].map(overflowCausingChild => {
   2869      if (overflowCausingChild.nodeType !== Node.ELEMENT_NODE) {
   2870        overflowCausingChild = overflowCausingChild.parentElement;
   2871      }
   2872 
   2873      return overflowCausingChild;
   2874    });
   2875 
   2876    return this.attachElements(overflowCausingElements);
   2877  }
   2878 
   2879  /**
   2880   * Return the scrollable ancestor node which has overflow because of the given node.
   2881   *
   2882   * @param {NodeActor} overflowCausingNode
   2883   */
   2884  getScrollableAncestorNode(overflowCausingNode) {
   2885    if (
   2886      isNodeDead(overflowCausingNode) ||
   2887      !this.overflowCausingElementsMap.has(overflowCausingNode.rawNode)
   2888    ) {
   2889      return null;
   2890    }
   2891 
   2892    return this.overflowCausingElementsMap.get(overflowCausingNode.rawNode);
   2893  }
   2894 }
   2895 
   2896 exports.WalkerActor = WalkerActor;