node.js (27095B)
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 { 9 nodeSpec, 10 nodeListSpec, 11 } = require("resource://devtools/shared/specs/node.js"); 12 13 const { 14 PSEUDO_CLASSES, 15 } = require("resource://devtools/shared/css/constants.js"); 16 17 loader.lazyRequireGetter( 18 this, 19 ["getCssPath", "getXPath", "findCssSelector"], 20 "resource://devtools/shared/inspector/css-logic.js", 21 true 22 ); 23 24 loader.lazyRequireGetter( 25 this, 26 [ 27 "getShadowRootMode", 28 "isDirectShadowHostChild", 29 "isFrameBlockedByCSP", 30 "isFrameWithChildTarget", 31 "isShadowHost", 32 "isShadowRoot", 33 ], 34 "resource://devtools/shared/layout/utils.js", 35 true 36 ); 37 38 loader.lazyRequireGetter( 39 this, 40 [ 41 "getBackgroundColor", 42 "getClosestBackgroundColor", 43 "getNodeDisplayName", 44 "imageToImageData", 45 "isNodeDead", 46 ], 47 "resource://devtools/server/actors/inspector/utils.js", 48 true 49 ); 50 loader.lazyRequireGetter( 51 this, 52 "LongStringActor", 53 "resource://devtools/server/actors/string.js", 54 true 55 ); 56 loader.lazyRequireGetter( 57 this, 58 "getFontPreviewData", 59 "resource://devtools/server/actors/utils/style-utils.js", 60 true 61 ); 62 loader.lazyRequireGetter( 63 this, 64 "CssLogic", 65 "resource://devtools/server/actors/inspector/css-logic.js", 66 true 67 ); 68 loader.lazyRequireGetter( 69 this, 70 "EventCollector", 71 "resource://devtools/server/actors/inspector/event-collector.js", 72 true 73 ); 74 loader.lazyRequireGetter( 75 this, 76 "DOMHelpers", 77 "resource://devtools/shared/dom-helpers.js", 78 true 79 ); 80 81 const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog"; 82 const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20; 83 84 /** 85 * Server side of the node actor. 86 */ 87 class NodeActor extends Actor { 88 constructor(walker, node) { 89 super(walker.conn, nodeSpec); 90 this.walker = walker; 91 this.rawNode = node; 92 this._eventCollector = new EventCollector(this.walker.targetActor); 93 // Map<id -> nsIEventListenerInfo> that we maintain to be able to disable/re-enable event listeners 94 // The id is generated from getEventListenerInfo 95 this._nsIEventListenersInfo = new Map(); 96 97 // Store the original display type and scrollable state and whether or not the node is 98 // displayed to track changes when reflows occur. 99 const wasScrollable = this.isScrollable; 100 101 this.currentDisplayType = this.displayType; 102 this.wasDisplayed = this.isDisplayed; 103 this.wasScrollable = wasScrollable; 104 this.currentContainerType = this.containerType; 105 this.currentAnchorName = this.anchorName; 106 107 if (wasScrollable) { 108 this.walker.updateOverflowCausingElements( 109 this, 110 this.walker.overflowCausingElementsMap 111 ); 112 } 113 } 114 115 toString() { 116 return ( 117 "[NodeActor " + this.actorID + " for " + this.rawNode.toString() + "]" 118 ); 119 } 120 121 isDocumentElement() { 122 return ( 123 this.rawNode.ownerDocument && 124 this.rawNode.ownerDocument.documentElement === this.rawNode 125 ); 126 } 127 128 destroy() { 129 super.destroy(); 130 131 if (this.mutationObserver) { 132 if (!Cu.isDeadWrapper(this.mutationObserver)) { 133 this.mutationObserver.disconnect(); 134 } 135 this.mutationObserver = null; 136 } 137 138 if (this.slotchangeListener) { 139 if (!isNodeDead(this)) { 140 this.rawNode.removeEventListener("slotchange", this.slotchangeListener); 141 } 142 this.slotchangeListener = null; 143 } 144 145 if (this._waitForFrameLoadAbortController) { 146 this._waitForFrameLoadAbortController.abort(); 147 this._waitForFrameLoadAbortController = null; 148 } 149 if (this._waitForFrameLoadIntervalId) { 150 clearInterval(this._waitForFrameLoadIntervalId); 151 this._waitForFrameLoadIntervalId = null; 152 } 153 154 if (this._nsIEventListenersInfo) { 155 // Re-enable all event listeners that we might have disabled 156 for (const nsIEventListenerInfo of this._nsIEventListenersInfo.values()) { 157 // If event listeners/node don't exist anymore, accessing nsIEventListenerInfo.enabled 158 // will throw. 159 try { 160 if (!nsIEventListenerInfo.enabled) { 161 nsIEventListenerInfo.enabled = true; 162 } 163 } catch (e) { 164 // ignore 165 } 166 } 167 this._nsIEventListenersInfo = null; 168 } 169 170 this._eventCollector.destroy(); 171 this._eventCollector = null; 172 this.rawNode = null; 173 this.walker = null; 174 } 175 176 // Returns the JSON representation of this object over the wire. 177 form() { 178 const parentNode = this.walker.parentNode(this); 179 const inlineTextChild = this.walker.inlineTextChild(this.rawNode); 180 const shadowRoot = isShadowRoot(this.rawNode); 181 const hostActor = shadowRoot 182 ? this.walker.getNode(this.rawNode.host) 183 : null; 184 const nodeType = this.rawNode.nodeType; 185 186 const form = { 187 actor: this.actorID, 188 host: hostActor ? hostActor.actorID : undefined, 189 baseURI: this.rawNode.baseURI, 190 parent: parentNode ? parentNode.actorID : undefined, 191 nodeType, 192 namespaceURI: this.rawNode.namespaceURI, 193 nodeName: this.rawNode.nodeName, 194 nodeValue: this.rawNode.nodeValue, 195 displayName: getNodeDisplayName(this.rawNode), 196 numChildren: this.numChildren, 197 inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined, 198 displayType: this.displayType, 199 isScrollable: this.isScrollable, 200 isTopLevelDocument: this.isTopLevelDocument, 201 causesOverflow: this.walker.overflowCausingElementsMap.has(this.rawNode), 202 containerType: this.containerType, 203 anchorName: this.anchorName, 204 205 // doctype attributes 206 name: this.rawNode.name, 207 publicId: this.rawNode.publicId, 208 systemId: this.rawNode.systemId, 209 210 attrs: this.writeAttrs(), 211 customElementLocation: this.getCustomElementLocation(), 212 isPseudoElement: !!this.rawNode.implementedPseudoElement, 213 isNativeAnonymous: this.rawNode.isNativeAnonymous, 214 isShadowRoot: shadowRoot, 215 shadowRootMode: getShadowRootMode(this.rawNode), 216 isShadowHost: isShadowHost(this.rawNode), 217 isDirectShadowHostChild: isDirectShadowHostChild(this.rawNode), 218 pseudoClassLocks: this.writePseudoClassLocks(), 219 mutationBreakpoints: this.walker.getMutationBreakpoints(this), 220 221 isDisplayed: this.isDisplayed, 222 isInHTMLDocument: 223 this.rawNode.ownerDocument && 224 this.rawNode.ownerDocument.contentType === "text/html", 225 traits: { 226 // @backward-compat { version 147 } Can be removed once 147 reaches release 227 hasPseudoElementNameInDisplayName: true, 228 }, 229 }; 230 231 // The event collector can be expensive, so only check for events on nodes that 232 // can display the `event` badge. 233 if ( 234 nodeType !== Node.COMMENT_NODE && 235 nodeType !== Node.TEXT_NODE && 236 nodeType !== Node.CDATA_SECTION_NODE && 237 nodeType !== Node.DOCUMENT_NODE && 238 nodeType !== Node.DOCUMENT_TYPE_NODE && 239 !form.isPseudoElement 240 ) { 241 form.hasEventListeners = this.hasEventListeners(); 242 } 243 244 if (this.isDocumentElement()) { 245 form.isDocumentElement = true; 246 } 247 248 if (isFrameBlockedByCSP(this.rawNode)) { 249 form.numChildren = 0; 250 } 251 252 // Flag the node if a different walker is needed to retrieve its children (i.e. if 253 // this is a remote frame, or if it's an iframe and we're creating targets for every iframes) 254 if (this.useChildTargetToFetchChildren) { 255 form.useChildTargetToFetchChildren = true; 256 // Declare at least one child (the #document element) so 257 // that they can be expanded. 258 form.numChildren = 1; 259 } 260 form.browsingContextID = this.rawNode.browsingContext?.id; 261 262 return form; 263 } 264 265 /** 266 * Watch the given document node for mutations using the DOM observer 267 * API. 268 */ 269 watchDocument(doc, callback) { 270 if (!doc.defaultView) { 271 return; 272 } 273 274 const node = this.rawNode; 275 // Create the observer on the node's actor. The node will make sure 276 // the observer is cleaned up when the actor is released. 277 const observer = new doc.defaultView.MutationObserver(callback); 278 observer.mergeAttributeRecords = true; 279 observer.observe(node, { 280 attributes: true, 281 characterData: true, 282 characterDataOldValue: true, 283 childList: true, 284 subtree: true, 285 // Track addition/removal of pseudo-elements too 286 chromeOnlyNodes: true, 287 }); 288 this.mutationObserver = observer; 289 } 290 291 /** 292 * Watch for all "slotchange" events on the node. 293 */ 294 watchSlotchange(callback) { 295 this.slotchangeListener = callback; 296 this.rawNode.addEventListener("slotchange", this.slotchangeListener); 297 } 298 299 /** 300 * Check if the current node represents an element (e.g. an iframe) which has a dedicated 301 * target for its underlying document that we would need to use to fetch the child nodes. 302 * This will be the case for iframes if EFT is enabled, or if this is a remote iframe and 303 * fission is enabled. 304 */ 305 get useChildTargetToFetchChildren() { 306 return isFrameWithChildTarget(this.walker.targetActor, this.rawNode); 307 } 308 309 get isTopLevelDocument() { 310 return this.rawNode === this.walker.rootDoc; 311 } 312 313 // Estimate the number of children that the walker will return without making 314 // a call to children() if possible. 315 get numChildren() { 316 const rawNode = this.rawNode; 317 let numChildren = rawNode.childNodes.length; 318 const hasContentDocument = rawNode.contentDocument; 319 const hasSVGDocument = rawNode.getSVGDocument && rawNode.getSVGDocument(); 320 if (numChildren === 0 && (hasContentDocument || hasSVGDocument)) { 321 // This might be an iframe with virtual children. 322 numChildren = 1; 323 } 324 325 // Normal counting misses ::before/::after. Also, some anonymous children 326 // may ultimately be skipped, so we have to consult with the walker. 327 if ( 328 numChildren === 0 || 329 isShadowHost(this.rawNode) || 330 // FIXME: We should be able to just check <slot> rather than 331 // containingShadowRoot. 332 this.rawNode.containingShadowRoot || 333 !!this.rawNode.implementedPseudoElement 334 ) { 335 numChildren = this.walker.countChildren(this); 336 } 337 338 return numChildren; 339 } 340 341 get computedStyle() { 342 if (!this._computedStyle) { 343 this._computedStyle = CssLogic.getComputedStyle(this.rawNode); 344 } 345 return this._computedStyle; 346 } 347 348 /** 349 * Returns the computed display style property value of the node. 350 */ 351 get displayType() { 352 // Consider all non-element nodes as displayed. 353 if (isNodeDead(this) || this.rawNode.nodeType !== Node.ELEMENT_NODE) { 354 return null; 355 } 356 357 const style = this.computedStyle; 358 if (!style) { 359 return null; 360 } 361 362 let display = null; 363 try { 364 display = style.display; 365 } catch (e) { 366 // Fails for <scrollbar> elements. 367 } 368 369 const gridContainerType = InspectorUtils.getGridContainerType(this.rawNode); 370 if ( 371 gridContainerType & 372 (InspectorUtils.GRID_SUBGRID_COL | InspectorUtils.GRID_SUBGRID_ROW) 373 ) { 374 display = "subgrid"; 375 } 376 377 return display; 378 } 379 380 /** 381 * Returns the computed containerType style property value of the node. 382 */ 383 get containerType() { 384 // non-element nodes can't be containers 385 if ( 386 isNodeDead(this) || 387 this.rawNode.nodeType !== Node.ELEMENT_NODE || 388 !this.computedStyle 389 ) { 390 return null; 391 } 392 393 return this.computedStyle.containerType; 394 } 395 396 /** 397 * Returns the computed anchorName style property value of the node. 398 */ 399 get anchorName() { 400 // non-element nodes can't be anchors 401 if ( 402 isNodeDead(this) || 403 this.rawNode.nodeType !== Node.ELEMENT_NODE || 404 !this.computedStyle 405 ) { 406 return null; 407 } 408 409 return this.computedStyle.anchorName; 410 } 411 412 /** 413 * Check whether the node currently has scrollbars and is scrollable. 414 */ 415 get isScrollable() { 416 return ( 417 this.rawNode.nodeType === Node.ELEMENT_NODE && 418 this.rawNode.hasVisibleScrollbars 419 ); 420 } 421 422 /** 423 * Is the node currently displayed? 424 */ 425 get isDisplayed() { 426 const type = this.displayType; 427 428 // Consider all non-elements or elements with no display-types to be displayed. 429 if (!type) { 430 return true; 431 } 432 433 // Otherwise consider elements to be displayed only if their display-types is other 434 // than "none"". 435 return type !== "none"; 436 } 437 438 /** 439 * Are there event listeners that are listening on this node? This method 440 * uses all parsers registered via event-parsers.js.registerEventParser() to 441 * check if there are any event listeners. 442 * 443 * @returns {boolean} 444 */ 445 hasEventListeners(refreshCache = false) { 446 if (this._hasEventListenersCached === undefined || refreshCache) { 447 const result = this._eventCollector.hasEventListeners(this.rawNode); 448 this._hasEventListenersCached = result; 449 } 450 return this._hasEventListenersCached; 451 } 452 453 writeAttrs() { 454 // If the node has no attributes or this.rawNode is the document node and a 455 // node with `name="attributes"` exists in the DOM we need to bail. 456 if ( 457 !this.rawNode.attributes || 458 !NamedNodeMap.isInstance(this.rawNode.attributes) 459 ) { 460 return undefined; 461 } 462 463 return [...this.rawNode.attributes].map(attr => { 464 return { namespace: attr.namespace, name: attr.name, value: attr.value }; 465 }); 466 } 467 468 writePseudoClassLocks() { 469 if (this.rawNode.nodeType !== Node.ELEMENT_NODE) { 470 return undefined; 471 } 472 let ret = undefined; 473 for (const pseudo of PSEUDO_CLASSES) { 474 if (InspectorUtils.hasPseudoClassLock(this.rawNode, pseudo)) { 475 ret = ret || []; 476 ret.push(pseudo); 477 } 478 } 479 return ret; 480 } 481 482 /** 483 * Retrieve the script location of the custom element definition for this node, when 484 * relevant. To be linked to a custom element definition 485 */ 486 getCustomElementLocation() { 487 // Get a reference to the custom element definition function. 488 const name = this.rawNode.localName; 489 490 if (!this.rawNode.ownerGlobal) { 491 return undefined; 492 } 493 494 const customElementsRegistry = this.rawNode.ownerGlobal.customElements; 495 const customElement = 496 customElementsRegistry && customElementsRegistry.get(name); 497 if (!customElement) { 498 return undefined; 499 } 500 // Create debugger object for the customElement function. 501 const global = Cu.getGlobalForObject(customElement); 502 503 const dbg = this.getParent().targetActor.makeDebugger(); 504 505 // If we hit a <browser> element of Firefox, its global will be the chrome window 506 // which is system principal and will be in the same compartment as the debuggee. 507 // For some reason, this happens when we run the content toolbox. As for the content 508 // toolboxes, the modules are loaded in the same compartment as the <browser> element, 509 // this throws as the debugger can _not_ be in the same compartment as the debugger. 510 // This happens when we toggle fission for content toolbox because we try to reparent 511 // the Walker of the tab. This happens because we do not detect in Walker.reparentRemoteFrame 512 // that the target of the tab is the top level. That's because the target is a WindowGlobalTargetActor 513 // which is retrieved via Node.getEmbedderElement and doesn't return the LocalTabTargetActor. 514 // We should probably work on TabDescriptor so that the LocalTabTargetActor has a descriptor, 515 // and see if we can possibly move the local tab specific out of the TargetActor and have 516 // the TabDescriptor expose a pure WindowGlobalTargetActor?? (See bug 1579042) 517 if (Cu.getObjectPrincipal(global) == Cu.getObjectPrincipal(dbg)) { 518 return undefined; 519 } 520 521 const globalDO = dbg.addDebuggee(global); 522 const customElementDO = globalDO.makeDebuggeeValue(customElement); 523 524 // Return undefined if we can't find a script for the custom element definition. 525 if (!customElementDO.script) { 526 return undefined; 527 } 528 529 // NOTE: Debugger.Script.prototype.startColumn is 1-based. 530 // Convert to 0-based, while keeping the wasm's column (1) as is. 531 // (bug 1863878) 532 const columnBase = customElementDO.script.format === "wasm" ? 0 : 1; 533 534 return { 535 url: customElementDO.script.url, 536 line: customElementDO.script.startLine, 537 column: customElementDO.script.startColumn - columnBase, 538 }; 539 } 540 541 /** 542 * Returns a LongStringActor with the node's value. 543 */ 544 getNodeValue() { 545 return new LongStringActor(this.conn, this.rawNode.nodeValue || ""); 546 } 547 548 /** 549 * Set the node's value to a given string. 550 */ 551 setNodeValue(value) { 552 this.rawNode.nodeValue = value; 553 } 554 555 /** 556 * Get a unique selector string for this node. 557 */ 558 getUniqueSelector() { 559 if (Cu.isDeadWrapper(this.rawNode)) { 560 return ""; 561 } 562 return findCssSelector(this.rawNode); 563 } 564 565 /** 566 * Get the full CSS path for this node. 567 * 568 * @return {string} A CSS selector with a part for the node and each of its ancestors. 569 */ 570 getCssPath() { 571 if (Cu.isDeadWrapper(this.rawNode)) { 572 return ""; 573 } 574 return getCssPath(this.rawNode); 575 } 576 577 /** 578 * Get the XPath for this node. 579 * 580 * @return {string} The XPath for finding this node on the page. 581 */ 582 getXPath() { 583 if (Cu.isDeadWrapper(this.rawNode)) { 584 return ""; 585 } 586 return getXPath(this.rawNode); 587 } 588 589 /** 590 * Scroll the selected node into view. 591 */ 592 scrollIntoView() { 593 // this.rawNode can be an element without `scrollIntoView` (e.g. a `Text` or a `Comment`) 594 // In such case, bail out. 595 if (typeof this.rawNode.scrollIntoView !== "function") { 596 return; 597 } 598 this.rawNode.scrollIntoView(true); 599 } 600 601 /** 602 * Get the node's image data if any (for canvas and img nodes). 603 * Returns an imageData object with the actual data being a LongStringActor 604 * and a size json object. 605 * The image data is transmitted as a base64 encoded png data-uri. 606 * The method rejects if the node isn't an image or if the image is missing 607 * 608 * Accepts a maxDim request parameter to resize images that are larger. This 609 * is important as the resizing occurs server-side so that image-data being 610 * transfered in the longstring back to the client will be that much smaller 611 */ 612 getImageData(maxDim) { 613 return imageToImageData(this.rawNode, maxDim).then(imageData => { 614 return { 615 data: new LongStringActor(this.conn, imageData.data), 616 size: imageData.size, 617 }; 618 }); 619 } 620 621 /** 622 * Get all event listeners that are listening on this node. 623 */ 624 getEventListenerInfo() { 625 this._nsIEventListenersInfo.clear(); 626 627 const eventListenersData = this._eventCollector.getEventListeners( 628 this.rawNode 629 ); 630 let counter = 0; 631 for (const eventListenerData of eventListenersData) { 632 if (eventListenerData.nsIEventListenerInfo) { 633 const id = `event-listener-info-${++counter}`; 634 this._nsIEventListenersInfo.set( 635 id, 636 eventListenerData.nsIEventListenerInfo 637 ); 638 639 eventListenerData.eventListenerInfoId = id; 640 // remove the nsIEventListenerInfo since we don't want to send it to the client. 641 delete eventListenerData.nsIEventListenerInfo; 642 } 643 } 644 return eventListenersData; 645 } 646 647 /** 648 * Disable a specific event listener given its associated id 649 * 650 * @param {string} eventListenerInfoId 651 */ 652 disableEventListener(eventListenerInfoId) { 653 const nsEventListenerInfo = 654 this._nsIEventListenersInfo.get(eventListenerInfoId); 655 if (!nsEventListenerInfo) { 656 throw new Error("Unkown nsEventListenerInfo"); 657 } 658 nsEventListenerInfo.enabled = false; 659 } 660 661 /** 662 * (Re-)enable a specific event listener given its associated id 663 * 664 * @param {string} eventListenerInfoId 665 */ 666 enableEventListener(eventListenerInfoId) { 667 const nsEventListenerInfo = 668 this._nsIEventListenersInfo.get(eventListenerInfoId); 669 if (!nsEventListenerInfo) { 670 throw new Error("Unkown nsEventListenerInfo"); 671 } 672 nsEventListenerInfo.enabled = true; 673 } 674 675 /** 676 * Modify a node's attributes. Passed an array of modifications 677 * similar in format to "attributes" mutations. 678 * { 679 * attributeName: <string> 680 * attributeNamespace: <optional string> 681 * newValue: <optional string> - If null or undefined, the attribute 682 * will be removed. 683 * } 684 * 685 * Returns when the modifications have been made. Mutations will 686 * be queued for any changes made. 687 */ 688 modifyAttributes(modifications) { 689 const rawNode = this.rawNode; 690 for (const change of modifications) { 691 if (change.newValue == null) { 692 if (change.attributeNamespace) { 693 rawNode.removeAttributeNS( 694 change.attributeNamespace, 695 change.attributeName 696 ); 697 } else { 698 rawNode.removeAttribute(change.attributeName); 699 } 700 } else if (change.attributeNamespace) { 701 rawNode.setAttributeDevtoolsNS( 702 change.attributeNamespace, 703 change.attributeName, 704 change.newValue 705 ); 706 } else { 707 rawNode.setAttributeDevtools(change.attributeName, change.newValue); 708 } 709 } 710 } 711 712 /** 713 * Given the font and fill style, get the image data of a canvas with the 714 * preview text and font. 715 * Returns an imageData object with the actual data being a LongStringActor 716 * and the width of the text as a string. 717 * The image data is transmitted as a base64 encoded png data-uri. 718 */ 719 getFontFamilyDataURL(font, fillStyle = "black") { 720 const doc = this.rawNode.ownerDocument; 721 const options = { 722 previewText: FONT_FAMILY_PREVIEW_TEXT, 723 previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE, 724 fillStyle, 725 }; 726 const { dataURL, size } = getFontPreviewData(font, doc, options); 727 728 return { data: new LongStringActor(this.conn, dataURL), size }; 729 } 730 731 /** 732 * Finds the computed background color of the closest parent with a set background 733 * color. 734 * 735 * @return {string} 736 * String with the background color of the form rgba(r, g, b, a). Defaults to 737 * rgba(255, 255, 255, 1) if no background color is found. 738 */ 739 getClosestBackgroundColor() { 740 return getClosestBackgroundColor(this.rawNode); 741 } 742 743 /** 744 * Finds the background color range for the parent of a single text node 745 * (i.e. for multi-colored backgrounds with gradients, images) or a single 746 * background color for single-colored backgrounds. Defaults to the closest 747 * background color if an error is encountered. 748 * 749 * @return {object} 750 * Object with one or more of the following properties: value, min, max 751 */ 752 getBackgroundColor() { 753 return getBackgroundColor(this); 754 } 755 756 /** 757 * Returns an object with the width and height of the node's owner window. 758 * 759 * @return {object} 760 */ 761 getOwnerGlobalDimensions() { 762 const win = this.rawNode.ownerGlobal; 763 return { 764 innerWidth: win.innerWidth, 765 innerHeight: win.innerHeight, 766 }; 767 } 768 769 /** 770 * If the current node is an iframe, wait for the content window to be loaded. 771 */ 772 async waitForFrameLoad() { 773 if (this.useChildTargetToFetchChildren) { 774 // If the document is handled by a dedicated target, we'll wait for a DOCUMENT_EVENT 775 // on the created target. 776 throw new Error( 777 "iframe content document has its own target, use that one instead" 778 ); 779 } 780 781 if (Cu.isDeadWrapper(this.rawNode)) { 782 throw new Error("Node is dead"); 783 } 784 785 const { contentDocument } = this.rawNode; 786 if (!contentDocument) { 787 throw new Error("Can't access contentDocument"); 788 } 789 790 if (contentDocument.readyState === "uninitialized") { 791 // If the readyState is "uninitialized", the document is probably an about:blank 792 // transient document. In such case, we want to wait until the "final" document 793 // is inserted. 794 795 const { chromeEventHandler } = this.rawNode.ownerGlobal.docShell; 796 const browsingContextID = this.rawNode.browsingContext.id; 797 await new Promise((resolve, reject) => { 798 this._waitForFrameLoadAbortController = new AbortController(); 799 800 chromeEventHandler.addEventListener( 801 "DOMDocElementInserted", 802 e => { 803 const { browsingContext } = e.target.defaultView; 804 // Check that the document we're notified about is the iframe one. 805 if (browsingContext.id == browsingContextID) { 806 resolve(); 807 this._waitForFrameLoadAbortController.abort(); 808 } 809 }, 810 { signal: this._waitForFrameLoadAbortController.signal } 811 ); 812 813 // It might happen that the "final" document will be a remote one, living in a 814 // different process, which means we won't get the DOMDocElementInserted event 815 // here, and will wait forever. To prevent this Promise to hang forever, we use 816 // a setInterval to check if the final document can be reached, so we can reject 817 // if it's not. 818 // This is definitely not a perfect solution, but I wasn't able to find something 819 // better for this feature. I think it's _fine_ as this method will be removed 820 // when EFT is enabled everywhere in release. 821 this._waitForFrameLoadIntervalId = setInterval(() => { 822 if (Cu.isDeadWrapper(this.rawNode) || !this.rawNode.contentDocument) { 823 reject("Can't access the iframe content document"); 824 clearInterval(this._waitForFrameLoadIntervalId); 825 this._waitForFrameLoadIntervalId = null; 826 this._waitForFrameLoadAbortController.abort(); 827 } 828 }, 50); 829 }); 830 } 831 832 if (this.rawNode.contentDocument.readyState === "loading") { 833 await new Promise(resolve => { 834 DOMHelpers.onceDOMReady(this.rawNode.contentWindow, resolve); 835 }); 836 } 837 } 838 } 839 840 /** 841 * Server side of a node list as returned by querySelectorAll() 842 */ 843 class NodeListActor extends Actor { 844 constructor(walker, nodeList) { 845 super(walker.conn, nodeListSpec); 846 this.walker = walker; 847 this.nodeList = nodeList || []; 848 } 849 850 /** 851 * Items returned by this actor should belong to the parent walker. 852 */ 853 marshallPool() { 854 return this.walker; 855 } 856 857 // Returns the JSON representation of this object over the wire. 858 form() { 859 return { 860 actor: this.actorID, 861 length: this.nodeList ? this.nodeList.length : 0, 862 }; 863 } 864 865 /** 866 * Get a single node from the node list. 867 */ 868 item(index) { 869 return this.walker.attachElement(this.nodeList[index]); 870 } 871 872 /** 873 * Get a range of the items from the node list. 874 */ 875 items(start = 0, end = this.nodeList.length) { 876 const items = Array.prototype.slice 877 .call(this.nodeList, start, end) 878 .map(item => this.walker._getOrCreateNodeActor(item)); 879 return this.walker.attachElements(items); 880 } 881 882 release() {} 883 } 884 885 exports.NodeActor = NodeActor; 886 exports.NodeListActor = NodeListActor;