summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/inspector/node.js
blob: 294e3e9564184b020af691a3df00cf1a526fc94f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const { Actor } = require("resource://devtools/shared/protocol.js");
const {
  nodeSpec,
  nodeListSpec,
} = require("resource://devtools/shared/specs/node.js");

const {
  PSEUDO_CLASSES,
} = require("resource://devtools/shared/css/constants.js");

loader.lazyRequireGetter(
  this,
  ["getCssPath", "getXPath", "findCssSelector"],
  "resource://devtools/shared/inspector/css-logic.js",
  true
);

loader.lazyRequireGetter(
  this,
  [
    "getShadowRootMode",
    "isAfterPseudoElement",
    "isAnonymous",
    "isBeforePseudoElement",
    "isDirectShadowHostChild",
    "isFrameBlockedByCSP",
    "isFrameWithChildTarget",
    "isMarkerPseudoElement",
    "isNativeAnonymous",
    "isShadowHost",
    "isShadowRoot",
  ],
  "resource://devtools/shared/layout/utils.js",
  true
);

loader.lazyRequireGetter(
  this,
  [
    "getBackgroundColor",
    "getClosestBackgroundColor",
    "getNodeDisplayName",
    "imageToImageData",
    "isNodeDead",
  ],
  "resource://devtools/server/actors/inspector/utils.js",
  true
);
loader.lazyRequireGetter(
  this,
  "LongStringActor",
  "resource://devtools/server/actors/string.js",
  true
);
loader.lazyRequireGetter(
  this,
  "getFontPreviewData",
  "resource://devtools/server/actors/utils/style-utils.js",
  true
);
loader.lazyRequireGetter(
  this,
  "CssLogic",
  "resource://devtools/server/actors/inspector/css-logic.js",
  true
);
loader.lazyRequireGetter(
  this,
  "EventCollector",
  "resource://devtools/server/actors/inspector/event-collector.js",
  true
);
loader.lazyRequireGetter(
  this,
  "DOMHelpers",
  "resource://devtools/shared/dom-helpers.js",
  true
);

const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;

/**
 * Server side of the node actor.
 */
class NodeActor extends Actor {
  constructor(walker, node) {
    super(walker.conn, nodeSpec);
    this.walker = walker;
    this.rawNode = node;
    this._eventCollector = new EventCollector(this.walker.targetActor);
    // Map<id -> nsIEventListenerInfo> that we maintain to be able to disable/re-enable event listeners
    // The id is generated from getEventListenerInfo
    this._nsIEventListenersInfo = new Map();

    // Store the original display type and scrollable state and whether or not the node is
    // displayed to track changes when reflows occur.
    const wasScrollable = this.isScrollable;

    this.currentDisplayType = this.displayType;
    this.wasDisplayed = this.isDisplayed;
    this.wasScrollable = wasScrollable;
    this.currentContainerType = this.containerType;

    if (wasScrollable) {
      this.walker.updateOverflowCausingElements(
        this,
        this.walker.overflowCausingElementsMap
      );
    }
  }

  toString() {
    return (
      "[NodeActor " + this.actorID + " for " + this.rawNode.toString() + "]"
    );
  }

  isDocumentElement() {
    return (
      this.rawNode.ownerDocument &&
      this.rawNode.ownerDocument.documentElement === this.rawNode
    );
  }

  destroy() {
    super.destroy();

    if (this.mutationObserver) {
      if (!Cu.isDeadWrapper(this.mutationObserver)) {
        this.mutationObserver.disconnect();
      }
      this.mutationObserver = null;
    }

    if (this.slotchangeListener) {
      if (!isNodeDead(this)) {
        this.rawNode.removeEventListener("slotchange", this.slotchangeListener);
      }
      this.slotchangeListener = null;
    }

    if (this._waitForFrameLoadAbortController) {
      this._waitForFrameLoadAbortController.abort();
      this._waitForFrameLoadAbortController = null;
    }
    if (this._waitForFrameLoadIntervalId) {
      clearInterval(this._waitForFrameLoadIntervalId);
      this._waitForFrameLoadIntervalId = null;
    }

    if (this._nsIEventListenersInfo) {
      // Re-enable all event listeners that we might have disabled
      for (const nsIEventListenerInfo of this._nsIEventListenersInfo.values()) {
        // If event listeners/node don't exist anymore, accessing nsIEventListenerInfo.enabled
        // will throw.
        try {
          if (!nsIEventListenerInfo.enabled) {
            nsIEventListenerInfo.enabled = true;
          }
        } catch (e) {
          // ignore
        }
      }
      this._nsIEventListenersInfo = null;
    }

    this._eventCollector.destroy();
    this._eventCollector = null;
    this.rawNode = null;
    this.walker = null;
  }

  // Returns the JSON representation of this object over the wire.
  form() {
    const parentNode = this.walker.parentNode(this);
    const inlineTextChild = this.walker.inlineTextChild(this);
    const shadowRoot = isShadowRoot(this.rawNode);
    const hostActor = shadowRoot
      ? this.walker.getNode(this.rawNode.host)
      : null;

    const form = {
      actor: this.actorID,
      host: hostActor ? hostActor.actorID : undefined,
      baseURI: this.rawNode.baseURI,
      parent: parentNode ? parentNode.actorID : undefined,
      nodeType: this.rawNode.nodeType,
      namespaceURI: this.rawNode.namespaceURI,
      nodeName: this.rawNode.nodeName,
      nodeValue: this.rawNode.nodeValue,
      displayName: getNodeDisplayName(this.rawNode),
      numChildren: this.numChildren,
      inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,
      displayType: this.displayType,
      isScrollable: this.isScrollable,
      isTopLevelDocument: this.isTopLevelDocument,
      causesOverflow: this.walker.overflowCausingElementsMap.has(this.rawNode),
      containerType: this.containerType,

      // doctype attributes
      name: this.rawNode.name,
      publicId: this.rawNode.publicId,
      systemId: this.rawNode.systemId,

      attrs: this.writeAttrs(),
      customElementLocation: this.getCustomElementLocation(),
      isMarkerPseudoElement: isMarkerPseudoElement(this.rawNode),
      isBeforePseudoElement: isBeforePseudoElement(this.rawNode),
      isAfterPseudoElement: isAfterPseudoElement(this.rawNode),
      isAnonymous: isAnonymous(this.rawNode),
      isNativeAnonymous: isNativeAnonymous(this.rawNode),
      isShadowRoot: shadowRoot,
      shadowRootMode: getShadowRootMode(this.rawNode),
      isShadowHost: isShadowHost(this.rawNode),
      isDirectShadowHostChild: isDirectShadowHostChild(this.rawNode),
      pseudoClassLocks: this.writePseudoClassLocks(),
      mutationBreakpoints: this.walker.getMutationBreakpoints(this),

      isDisplayed: this.isDisplayed,
      isInHTMLDocument:
        this.rawNode.ownerDocument &&
        this.rawNode.ownerDocument.contentType === "text/html",
      hasEventListeners: this._hasEventListeners,
      traits: {},
    };

    if (this.isDocumentElement()) {
      form.isDocumentElement = true;
    }

    if (isFrameBlockedByCSP(this.rawNode)) {
      form.numChildren = 0;
    }

    // Flag the node if a different walker is needed to retrieve its children (i.e. if
    // this is a remote frame, or if it's an iframe and we're creating targets for every iframes)
    if (this.useChildTargetToFetchChildren) {
      form.useChildTargetToFetchChildren = true;
      // Declare at least one child (the #document element) so
      // that they can be expanded.
      form.numChildren = 1;
    }
    form.browsingContextID = this.rawNode.browsingContext?.id;

    return form;
  }

  /**
   * Watch the given document node for mutations using the DOM observer
   * API.
   */
  watchDocument(doc, callback) {
    if (!doc.defaultView) {
      return;
    }

    const node = this.rawNode;
    // Create the observer on the node's actor.  The node will make sure
    // the observer is cleaned up when the actor is released.
    const observer = new doc.defaultView.MutationObserver(callback);
    observer.mergeAttributeRecords = true;
    observer.observe(node, {
      attributes: true,
      characterData: true,
      characterDataOldValue: true,
      childList: true,
      subtree: true,
      chromeOnlyNodes: true,
    });
    this.mutationObserver = observer;
  }

  /**
   * Watch for all "slotchange" events on the node.
   */
  watchSlotchange(callback) {
    this.slotchangeListener = callback;
    this.rawNode.addEventListener("slotchange", this.slotchangeListener);
  }

  /**
   * Check if the current node represents an element (e.g. an iframe) which has a dedicated
   * target for its underlying document that we would need to use to fetch the child nodes.
   * This will be the case for iframes if EFT is enabled, or if this is a remote iframe and
   * fission is enabled.
   */
  get useChildTargetToFetchChildren() {
    return isFrameWithChildTarget(this.walker.targetActor, this.rawNode);
  }

  get isTopLevelDocument() {
    return this.rawNode === this.walker.rootDoc;
  }

  // Estimate the number of children that the walker will return without making
  // a call to children() if possible.
  get numChildren() {
    // For pseudo elements, childNodes.length returns 1, but the walker
    // will return 0.
    if (
      isMarkerPseudoElement(this.rawNode) ||
      isBeforePseudoElement(this.rawNode) ||
      isAfterPseudoElement(this.rawNode)
    ) {
      return 0;
    }

    const rawNode = this.rawNode;
    let numChildren = rawNode.childNodes.length;
    const hasContentDocument = rawNode.contentDocument;
    const hasSVGDocument = rawNode.getSVGDocument && rawNode.getSVGDocument();
    if (numChildren === 0 && (hasContentDocument || hasSVGDocument)) {
      // This might be an iframe with virtual children.
      numChildren = 1;
    }

    // Normal counting misses ::before/::after.  Also, some anonymous children
    // may ultimately be skipped, so we have to consult with the walker.
    //
    // FIXME: We should be able to just check <slot> rather than
    // containingShadowRoot.
    if (
      numChildren === 0 ||
      isShadowHost(this.rawNode) ||
      this.rawNode.containingShadowRoot
    ) {
      numChildren = this.walker.countChildren(this);
    }

    return numChildren;
  }

  get computedStyle() {
    if (!this._computedStyle) {
      this._computedStyle = CssLogic.getComputedStyle(this.rawNode);
    }
    return this._computedStyle;
  }

  /**
   * Returns the computed display style property value of the node.
   */
  get displayType() {
    // Consider all non-element nodes as displayed.
    if (isNodeDead(this) || this.rawNode.nodeType !== Node.ELEMENT_NODE) {
      return null;
    }

    const style = this.computedStyle;
    if (!style) {
      return null;
    }

    let display = null;
    try {
      display = style.display;
    } catch (e) {
      // Fails for <scrollbar> elements.
    }

    if (
      (display === "grid" || display === "inline-grid") &&
      (style.gridTemplateRows.startsWith("subgrid") ||
        style.gridTemplateColumns.startsWith("subgrid"))
    ) {
      display = "subgrid";
    }

    return display;
  }

  /**
   * Returns the computed containerType style property value of the node.
   */
  get containerType() {
    // non-element nodes can't be containers
    if (
      isNodeDead(this) ||
      this.rawNode.nodeType !== Node.ELEMENT_NODE ||
      !this.computedStyle
    ) {
      return null;
    }

    return this.computedStyle.containerType;
  }

  /**
   * Check whether the node currently has scrollbars and is scrollable.
   */
  get isScrollable() {
    return (
      this.rawNode.nodeType === Node.ELEMENT_NODE &&
      this.rawNode.hasVisibleScrollbars
    );
  }

  /**
   * Is the node currently displayed?
   */
  get isDisplayed() {
    const type = this.displayType;

    // Consider all non-elements or elements with no display-types to be displayed.
    if (!type) {
      return true;
    }

    // Otherwise consider elements to be displayed only if their display-types is other
    // than "none"".
    return type !== "none";
  }

  /**
   * Are there event listeners that are listening on this node? This method
   * uses all parsers registered via event-parsers.js.registerEventParser() to
   * check if there are any event listeners.
   */
  get _hasEventListeners() {
    // We need to pass a debugger instance from this compartment because
    // otherwise we can't make use of it inside the event-collector module.
    const dbg = this.getParent().targetActor.makeDebugger();
    return this._eventCollector.hasEventListeners(this.rawNode, dbg);
  }

  writeAttrs() {
    // If the node has no attributes or this.rawNode is the document node and a
    // node with `name="attributes"` exists in the DOM we need to bail.
    if (
      !this.rawNode.attributes ||
      !NamedNodeMap.isInstance(this.rawNode.attributes)
    ) {
      return undefined;
    }

    return [...this.rawNode.attributes].map(attr => {
      return { namespace: attr.namespace, name: attr.name, value: attr.value };
    });
  }

  writePseudoClassLocks() {
    if (this.rawNode.nodeType !== Node.ELEMENT_NODE) {
      return undefined;
    }
    let ret = undefined;
    for (const pseudo of PSEUDO_CLASSES) {
      if (InspectorUtils.hasPseudoClassLock(this.rawNode, pseudo)) {
        ret = ret || [];
        ret.push(pseudo);
      }
    }
    return ret;
  }

  /**
   * Retrieve the script location of the custom element definition for this node, when
   * relevant. To be linked to a custom element definition
   */
  getCustomElementLocation() {
    // Get a reference to the custom element definition function.
    const name = this.rawNode.localName;

    if (!this.rawNode.ownerGlobal) {
      return undefined;
    }

    const customElementsRegistry = this.rawNode.ownerGlobal.customElements;
    const customElement =
      customElementsRegistry && customElementsRegistry.get(name);
    if (!customElement) {
      return undefined;
    }
    // Create debugger object for the customElement function.
    const global = Cu.getGlobalForObject(customElement);

    const dbg = this.getParent().targetActor.makeDebugger();

    // If we hit a <browser> element of Firefox, its global will be the chrome window
    // which is system principal and will be in the same compartment as the debuggee.
    // For some reason, this happens when we run the content toolbox. As for the content
    // toolboxes, the modules are loaded in the same compartment as the <browser> element,
    // this throws as the debugger can _not_ be in the same compartment as the debugger.
    // This happens when we toggle fission for content toolbox because we try to reparent
    // the Walker of the tab. This happens because we do not detect in Walker.reparentRemoteFrame
    // that the target of the tab is the top level. That's because the target is a WindowGlobalTargetActor
    // which is retrieved via Node.getEmbedderElement and doesn't return the LocalTabTargetActor.
    // We should probably work on TabDescriptor so that the LocalTabTargetActor has a descriptor,
    // and see if we can possibly move the local tab specific out of the TargetActor and have
    // the TabDescriptor expose a pure WindowGlobalTargetActor?? (See bug 1579042)
    if (Cu.getObjectPrincipal(global) == Cu.getObjectPrincipal(dbg)) {
      return undefined;
    }

    const globalDO = dbg.addDebuggee(global);
    const customElementDO = globalDO.makeDebuggeeValue(customElement);

    // Return undefined if we can't find a script for the custom element definition.
    if (!customElementDO.script) {
      return undefined;
    }

    // NOTE: Debugger.Script.prototype.startColumn is 1-based.
    //       Convert to 0-based, while keeping the wasm's column (1) as is.
    //       (bug 1863878)
    const columnBase = customElementDO.script.format === "wasm" ? 0 : 1;

    return {
      url: customElementDO.script.url,
      line: customElementDO.script.startLine,
      column: customElementDO.script.startColumn - columnBase,
    };
  }

  /**
   * Returns a LongStringActor with the node's value.
   */
  getNodeValue() {
    return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
  }

  /**
   * Set the node's value to a given string.
   */
  setNodeValue(value) {
    this.rawNode.nodeValue = value;
  }

  /**
   * Get a unique selector string for this node.
   */
  getUniqueSelector() {
    if (Cu.isDeadWrapper(this.rawNode)) {
      return "";
    }
    return findCssSelector(this.rawNode);
  }

  /**
   * Get the full CSS path for this node.
   *
   * @return {String} A CSS selector with a part for the node and each of its ancestors.
   */
  getCssPath() {
    if (Cu.isDeadWrapper(this.rawNode)) {
      return "";
    }
    return getCssPath(this.rawNode);
  }

  /**
   * Get the XPath for this node.
   *
   * @return {String} The XPath for finding this node on the page.
   */
  getXPath() {
    if (Cu.isDeadWrapper(this.rawNode)) {
      return "";
    }
    return getXPath(this.rawNode);
  }

  /**
   * Scroll the selected node into view.
   */
  scrollIntoView() {
    this.rawNode.scrollIntoView(true);
  }

  /**
   * Get the node's image data if any (for canvas and img nodes).
   * Returns an imageData object with the actual data being a LongStringActor
   * and a size json object.
   * The image data is transmitted as a base64 encoded png data-uri.
   * The method rejects if the node isn't an image or if the image is missing
   *
   * Accepts a maxDim request parameter to resize images that are larger. This
   * is important as the resizing occurs server-side so that image-data being
   * transfered in the longstring back to the client will be that much smaller
   */
  getImageData(maxDim) {
    return imageToImageData(this.rawNode, maxDim).then(imageData => {
      return {
        data: new LongStringActor(this.conn, imageData.data),
        size: imageData.size,
      };
    });
  }

  /**
   * Get all event listeners that are listening on this node.
   */
  getEventListenerInfo() {
    this._nsIEventListenersInfo.clear();

    const eventListenersData = this._eventCollector.getEventListeners(
      this.rawNode
    );
    let counter = 0;
    for (const eventListenerData of eventListenersData) {
      if (eventListenerData.nsIEventListenerInfo) {
        const id = `event-listener-info-${++counter}`;
        this._nsIEventListenersInfo.set(
          id,
          eventListenerData.nsIEventListenerInfo
        );

        eventListenerData.eventListenerInfoId = id;
        // remove the nsIEventListenerInfo since we don't want to send it to the client.
        delete eventListenerData.nsIEventListenerInfo;
      }
    }
    return eventListenersData;
  }

  /**
   * Disable a specific event listener given its associated id
   *
   * @param {String} eventListenerInfoId
   */
  disableEventListener(eventListenerInfoId) {
    const nsEventListenerInfo =
      this._nsIEventListenersInfo.get(eventListenerInfoId);
    if (!nsEventListenerInfo) {
      throw new Error("Unkown nsEventListenerInfo");
    }
    nsEventListenerInfo.enabled = false;
  }

  /**
   * (Re-)enable a specific event listener given its associated id
   *
   * @param {String} eventListenerInfoId
   */
  enableEventListener(eventListenerInfoId) {
    const nsEventListenerInfo =
      this._nsIEventListenersInfo.get(eventListenerInfoId);
    if (!nsEventListenerInfo) {
      throw new Error("Unkown nsEventListenerInfo");
    }
    nsEventListenerInfo.enabled = true;
  }

  /**
   * Modify a node's attributes.  Passed an array of modifications
   * similar in format to "attributes" mutations.
   * {
   *   attributeName: <string>
   *   attributeNamespace: <optional string>
   *   newValue: <optional string> - If null or undefined, the attribute
   *     will be removed.
   * }
   *
   * Returns when the modifications have been made.  Mutations will
   * be queued for any changes made.
   */
  modifyAttributes(modifications) {
    const rawNode = this.rawNode;
    for (const change of modifications) {
      if (change.newValue == null) {
        if (change.attributeNamespace) {
          rawNode.removeAttributeNS(
            change.attributeNamespace,
            change.attributeName
          );
        } else {
          rawNode.removeAttribute(change.attributeName);
        }
      } else if (change.attributeNamespace) {
        rawNode.setAttributeDevtoolsNS(
          change.attributeNamespace,
          change.attributeName,
          change.newValue
        );
      } else {
        rawNode.setAttributeDevtools(change.attributeName, change.newValue);
      }
    }
  }

  /**
   * Given the font and fill style, get the image data of a canvas with the
   * preview text and font.
   * Returns an imageData object with the actual data being a LongStringActor
   * and the width of the text as a string.
   * The image data is transmitted as a base64 encoded png data-uri.
   */
  getFontFamilyDataURL(font, fillStyle = "black") {
    const doc = this.rawNode.ownerDocument;
    const options = {
      previewText: FONT_FAMILY_PREVIEW_TEXT,
      previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE,
      fillStyle,
    };
    const { dataURL, size } = getFontPreviewData(font, doc, options);

    return { data: new LongStringActor(this.conn, dataURL), size };
  }

  /**
   * Finds the computed background color of the closest parent with a set background
   * color.
   *
   * @return {String}
   *         String with the background color of the form rgba(r, g, b, a). Defaults to
   *         rgba(255, 255, 255, 1) if no background color is found.
   */
  getClosestBackgroundColor() {
    return getClosestBackgroundColor(this.rawNode);
  }

  /**
   * Finds the background color range for the parent of a single text node
   * (i.e. for multi-colored backgrounds with gradients, images) or a single
   * background color for single-colored backgrounds. Defaults to the closest
   * background color if an error is encountered.
   *
   * @return {Object}
   *         Object with one or more of the following properties: value, min, max
   */
  getBackgroundColor() {
    return getBackgroundColor(this);
  }

  /**
   * Returns an object with the width and height of the node's owner window.
   *
   * @return {Object}
   */
  getOwnerGlobalDimensions() {
    const win = this.rawNode.ownerGlobal;
    return {
      innerWidth: win.innerWidth,
      innerHeight: win.innerHeight,
    };
  }

  /**
   * If the current node is an iframe, wait for the content window to be loaded.
   */
  async waitForFrameLoad() {
    if (this.useChildTargetToFetchChildren) {
      // If the document is handled by a dedicated target, we'll wait for a DOCUMENT_EVENT
      // on the created target.
      throw new Error(
        "iframe content document has its own target, use that one instead"
      );
    }

    if (Cu.isDeadWrapper(this.rawNode)) {
      throw new Error("Node is dead");
    }

    const { contentDocument } = this.rawNode;
    if (!contentDocument) {
      throw new Error("Can't access contentDocument");
    }

    if (contentDocument.readyState === "uninitialized") {
      // If the readyState is "uninitialized", the document is probably an about:blank
      // transient document. In such case, we want to wait until the "final" document
      // is inserted.

      const { chromeEventHandler } = this.rawNode.ownerGlobal.docShell;
      const browsingContextID = this.rawNode.browsingContext.id;
      await new Promise((resolve, reject) => {
        this._waitForFrameLoadAbortController = new AbortController();

        chromeEventHandler.addEventListener(
          "DOMDocElementInserted",
          e => {
            const { browsingContext } = e.target.defaultView;
            // Check that the document we're notified about is the iframe one.
            if (browsingContext.id == browsingContextID) {
              resolve();
              this._waitForFrameLoadAbortController.abort();
            }
          },
          { signal: this._waitForFrameLoadAbortController.signal }
        );

        // It might happen that the "final" document will be a remote one, living in a
        // different process, which means we won't get the DOMDocElementInserted event
        // here, and will wait forever. To prevent this Promise to hang forever, we use
        // a setInterval to check if the final document can be reached, so we can reject
        // if it's not.
        // This is definitely not a perfect solution, but I wasn't able to find something
        // better for this feature. I think it's _fine_ as this method will be removed
        // when EFT is  enabled everywhere in release.
        this._waitForFrameLoadIntervalId = setInterval(() => {
          if (Cu.isDeadWrapper(this.rawNode) || !this.rawNode.contentDocument) {
            reject("Can't access the iframe content document");
            clearInterval(this._waitForFrameLoadIntervalId);
            this._waitForFrameLoadIntervalId = null;
            this._waitForFrameLoadAbortController.abort();
          }
        }, 50);
      });
    }

    if (this.rawNode.contentDocument.readyState === "loading") {
      await new Promise(resolve => {
        DOMHelpers.onceDOMReady(this.rawNode.contentWindow, resolve);
      });
    }
  }
}

/**
 * Server side of a node list as returned by querySelectorAll()
 */
class NodeListActor extends Actor {
  constructor(walker, nodeList) {
    super(walker.conn, nodeListSpec);
    this.walker = walker;
    this.nodeList = nodeList || [];
  }

  /**
   * Items returned by this actor should belong to the parent walker.
   */
  marshallPool() {
    return this.walker;
  }

  // Returns the JSON representation of this object over the wire.
  form() {
    return {
      actor: this.actorID,
      length: this.nodeList ? this.nodeList.length : 0,
    };
  }

  /**
   * Get a single node from the node list.
   */
  item(index) {
    return this.walker.attachElement(this.nodeList[index]);
  }

  /**
   * Get a range of the items from the node list.
   */
  items(start = 0, end = this.nodeList.length) {
    const items = Array.prototype.slice
      .call(this.nodeList, start, end)
      .map(item => this.walker._getOrCreateNodeActor(item));
    return this.walker.attachElements(items);
  }

  release() {}
}

exports.NodeActor = NodeActor;
exports.NodeListActor = NodeListActor;