head.js (54672B)
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 /* eslint no-unused-vars: [2, {"vars": "local"}] */ 5 6 "use strict"; 7 8 // Load the shared-head file first. 9 Services.scriptloader.loadSubScript( 10 "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", 11 this 12 ); 13 14 // Services.prefs.setBoolPref("devtools.debugger.log", true); 15 16 // Import helpers for the inspector that are also shared with others 17 Services.scriptloader.loadSubScript( 18 "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js", 19 this 20 ); 21 22 const INSPECTOR_L10N = new LocalizationHelper( 23 "devtools/client/locales/inspector.properties" 24 ); 25 26 registerCleanupFunction(function () { 27 // Move the mouse outside inspector. If the test happened fake a mouse event 28 // somewhere over inspector the pointer is considered to be there when the 29 // next test begins. This might cause unexpected events to be emitted when 30 // another test moves the mouse. 31 // Move the mouse at the top-right corner of the browser, to prevent 32 // the mouse from triggering the tab tooltip to be shown while the tab is 33 // being closed because the test is exiting (See Bug 1378524 for rationale). 34 EventUtils.synthesizeMouseAtPoint( 35 window.innerWidth, 36 1, 37 { type: "mousemove" }, 38 window 39 ); 40 }); 41 42 /** 43 * Start the element picker and focus the content window. 44 * 45 * @param {Toolbox} toolbox 46 * @param {boolean} skipFocus - Allow tests to bypass the focus event. 47 */ 48 var startPicker = async function (toolbox, skipFocus) { 49 info("Start the element picker"); 50 toolbox.win.focus(); 51 await toolbox.nodePicker.start(); 52 if (!skipFocus) { 53 // By default make sure the content window is focused since the picker may not focus 54 // the content window by default. 55 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { 56 content.focus(); 57 }); 58 } 59 }; 60 61 /** 62 * Stop the element picker using the Escape keyboard shortcut 63 * 64 * @param {Toolbox} toolbox 65 */ 66 var stopPickerWithEscapeKey = async function (toolbox) { 67 const onPickerStopped = toolbox.nodePicker.once("picker-node-canceled"); 68 EventUtils.synthesizeKey("VK_ESCAPE", {}, toolbox.win); 69 await onPickerStopped; 70 }; 71 72 /** 73 * Start the eye dropper tool. 74 * 75 * @param {Toolbox} toolbox 76 */ 77 var startEyeDropper = async function (toolbox) { 78 info("Start the eye dropper tool"); 79 toolbox.win.focus(); 80 await toolbox.getPanel("inspector").showEyeDropper(); 81 }; 82 83 /** 84 * Pick an element from the content page using the element picker. 85 * 86 * @param {Inspector} inspector 87 * Inspector instance 88 * @param {string} selector 89 * CSS selector to identify the click target 90 * @param {number} x 91 * X-offset from the top-left corner of the element matching the provided selector 92 * @param {number} y 93 * Y-offset from the top-left corner of the element matching the provided selector 94 * @return {Promise} promise that resolves when the selection is updated with the picked 95 * node. 96 */ 97 function pickElement(inspector, selector, x, y) { 98 info("Waiting for element " + selector + " to be picked"); 99 // Use an empty options argument in order trigger the default synthesizeMouse behavior 100 // which will trigger mousedown, then mouseup. 101 const onNewNodeFront = inspector.selection.once("new-node-front"); 102 BrowserTestUtils.synthesizeMouse( 103 selector, 104 x, 105 y, 106 {}, 107 gBrowser.selectedTab.linkedBrowser 108 ); 109 return onNewNodeFront; 110 } 111 112 /** 113 * Hover an element from the content page using the element picker. 114 * 115 * @param {Inspector} inspector 116 * Inspector instance 117 * @param {string | Array} selector 118 * CSS selector to identify the hover target. 119 * Example: ".target" 120 * If the element is at the bottom of a nested iframe stack, the selector should 121 * be an array with each item identifying the iframe within its host document. 122 * The last item of the array should be the element selector within the deepest 123 * nested iframe. 124 Example: ["iframe#top", "iframe#nested", ".target"] 125 * @param {number} x 126 * X-offset from the top-left corner of the element matching the provided selector 127 * @param {number} y 128 * Y-offset from the top-left corner of the element matching the provided selector 129 * @param {object} eventOptions 130 * Options that will be passed to synthesizeMouse 131 * @return {Promise} promise that resolves when both the "picker-node-hovered" and 132 * "highlighter-shown" events are emitted. 133 */ 134 async function hoverElement(inspector, selector, x, y, eventOptions = {}) { 135 const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); 136 info(`Waiting for element "${selector}" to be hovered`); 137 const onHovered = inspector.toolbox.nodePicker.once("picker-node-hovered"); 138 const onHighlighterShown = waitForHighlighterTypeShown( 139 inspector.highlighters.TYPES.BOXMODEL 140 ); 141 142 // Default to the top-level target browsing context 143 let browsingContext = gBrowser.selectedTab.linkedBrowser; 144 145 if (Array.isArray(selector)) { 146 // Get the browsing context for the deepest nested frame; exclude the last array item. 147 // Cloning the array so it can be safely mutated. 148 browsingContext = await getBrowsingContextForNestedFrame( 149 selector.slice(0, selector.length - 1) 150 ); 151 // Assume the last item in the selector array is the actual element selector. 152 // DO NOT mutate the selector array with .pop(), it might still be used by a test. 153 selector = selector[selector.length - 1]; 154 } 155 156 if (isNaN(x) || isNaN(y)) { 157 BrowserTestUtils.synthesizeMouseAtCenter( 158 selector, 159 { ...eventOptions, type: "mousemove" }, 160 browsingContext 161 ); 162 } else { 163 BrowserTestUtils.synthesizeMouse( 164 selector, 165 x, 166 y, 167 { ...eventOptions, type: "mousemove" }, 168 browsingContext 169 ); 170 } 171 172 info("Wait for picker-node-hovered"); 173 await onHovered; 174 175 info("Wait for highlighter shown"); 176 await onHighlighterShown; 177 178 return Promise.all([onHighlighterShown, onHovered]); 179 } 180 181 /** 182 * Get the browsing context for the deepest nested iframe 183 * as identified by an array of selectors. 184 * 185 * @param {Array} selectorArray 186 * Each item in the array is a selector that identifies the iframe 187 * within its host document. 188 * Example: ["iframe#top", "iframe#nested"] 189 * @return {BrowsingContext} 190 * BrowsingContext for the deepest nested iframe. 191 */ 192 async function getBrowsingContextForNestedFrame(selectorArray = []) { 193 // Default to the top-level target browsing context 194 let browsingContext = gBrowser.selectedTab.linkedBrowser; 195 196 // Return the top-level target browsing context if the selector is not an array. 197 if (!Array.isArray(selectorArray)) { 198 return browsingContext; 199 } 200 201 // Recursively get the browsing context for each nested iframe. 202 while (selectorArray.length) { 203 browsingContext = await SpecialPowers.spawn( 204 browsingContext, 205 [selectorArray.shift()], 206 function (selector) { 207 const iframe = content.document.querySelector(selector); 208 return iframe.browsingContext; 209 } 210 ); 211 } 212 213 return browsingContext; 214 } 215 216 /** 217 * Highlight a node and set the inspector's current selection to the node or 218 * the first match of the given css selector. 219 * 220 * @param {string | NodeFront} selector 221 * @param {InspectorPanel} inspector 222 * The instance of InspectorPanel currently loaded in the toolbox 223 * @return a promise that resolves when the inspector is updated with the new 224 * node 225 */ 226 async function selectAndHighlightNode(selector, inspector) { 227 const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); 228 info("Highlighting and selecting the node " + selector); 229 const onHighlighterShown = waitForHighlighterTypeShown( 230 inspector.highlighters.TYPES.BOXMODEL 231 ); 232 233 await selectNode(selector, inspector, "test-highlight"); 234 await onHighlighterShown; 235 } 236 237 /** 238 * Select node for a given selector, make it focusable and set focus in its 239 * container element. 240 * 241 * @param {string | NodeFront} selector 242 * @param {InspectorPanel} inspector The current inspector-panel instance. 243 * @return {MarkupContainer} 244 */ 245 async function focusNode(selector, inspector) { 246 getContainerForNodeFront(inspector.walker.rootNode, inspector).elt.focus(); 247 const nodeFront = await getNodeFront(selector, inspector); 248 const container = getContainerForNodeFront(nodeFront, inspector); 249 await selectNode(nodeFront, inspector); 250 EventUtils.sendKey("return", inspector.panelWin); 251 return container; 252 } 253 254 /** 255 * Set the inspector's current selection to null so that no node is selected 256 * 257 * @param {InspectorPanel} inspector 258 * The instance of InspectorPanel currently loaded in the toolbox 259 * @return a promise that resolves when the inspector is updated 260 */ 261 function clearCurrentNodeSelection(inspector) { 262 info("Clearing the current selection"); 263 const updated = inspector.once("inspector-updated"); 264 inspector.selection.setNodeFront(null); 265 return updated; 266 } 267 268 /** 269 * Right click on a node in the test page and click on the inspect menu item. 270 * 271 * @param {string} selector The selector for the node to click on in the page. 272 * @return {Promise} Resolves to the inspector when it has opened and is updated 273 */ 274 var clickOnInspectMenuItem = async function (selector) { 275 info("Showing the contextual menu on node " + selector); 276 const contentAreaContextMenu = document.querySelector( 277 "#contentAreaContextMenu" 278 ); 279 const contextOpened = once(contentAreaContextMenu, "popupshown"); 280 281 await safeSynthesizeMouseEventAtCenterInContentPage(selector, { 282 type: "contextmenu", 283 button: 2, 284 }); 285 286 await contextOpened; 287 288 info("Triggering the inspect action"); 289 await gContextMenu.inspectNode(); 290 291 info("Hiding the menu"); 292 const contextClosed = once(contentAreaContextMenu, "popuphidden"); 293 contentAreaContextMenu.hidePopup(); 294 await contextClosed; 295 296 return getActiveInspector(); 297 }; 298 299 /** 300 * Get the NodeFront for the document node inside a given iframe. 301 * 302 * @param {string | NodeFront} frameSelector 303 * A selector that matches the iframe the document node is in 304 * @param {InspectorPanel} inspector 305 * The instance of InspectorPanel currently loaded in the toolbox 306 * @return {Promise} Resolves the node front when the inspector is updated with the new 307 * node. 308 */ 309 var getFrameDocument = async function (frameSelector, inspector) { 310 const iframe = await getNodeFront(frameSelector, inspector); 311 const { nodes } = await inspector.walker.children(iframe); 312 313 // Find the document node in the children of the iframe element. 314 return nodes.filter(node => node.displayName === "#document")[0]; 315 }; 316 317 /** 318 * Get the NodeFront for the shadowRoot of a shadow host. 319 * 320 * @param {string | NodeFront} hostSelector 321 * Selector or front of the element to which the shadow root is attached. 322 * @param {InspectorPanel} inspector 323 * The instance of InspectorPanel currently loaded in the toolbox 324 * @return {Promise} Resolves the node front when the inspector is updated with the new 325 * node. 326 */ 327 var getShadowRoot = async function (hostSelector, inspector) { 328 const hostFront = await getNodeFront(hostSelector, inspector); 329 const { nodes } = await inspector.walker.children(hostFront); 330 331 // Find the shadow root in the children of the host element. 332 return nodes.filter(node => node.isShadowRoot)[0]; 333 }; 334 335 /** 336 * Get the NodeFront for a node that matches a given css selector inside a shadow root. 337 * 338 * @param {string} selector 339 * CSS selector of the node inside the shadow root. 340 * @param {string | NodeFront} hostSelector 341 * Selector or front of the element to which the shadow root is attached. 342 * @param {InspectorPanel} inspector 343 * The instance of InspectorPanel currently loaded in the toolbox 344 * @return {Promise} Resolves the node front when the inspector is updated with the new 345 * node. 346 */ 347 var getNodeFrontInShadowDom = async function ( 348 selector, 349 hostSelector, 350 inspector 351 ) { 352 const shadowRoot = await getShadowRoot(hostSelector, inspector); 353 if (!shadowRoot) { 354 throw new Error( 355 "Could not find a shadow root under selector: " + hostSelector 356 ); 357 } 358 359 return inspector.walker.querySelector(shadowRoot, selector); 360 }; 361 362 var focusSearchBoxUsingShortcut = async function (panelWin, callback) { 363 info("Focusing search box"); 364 const searchBox = panelWin.document.getElementById("inspector-searchbox"); 365 const focused = once(searchBox, "focus"); 366 367 panelWin.focus(); 368 369 synthesizeKeyShortcut(INSPECTOR_L10N.getStr("inspector.searchHTML.key")); 370 371 await focused; 372 373 if (callback) { 374 callback(); 375 } 376 }; 377 378 /** 379 * Get the MarkupContainer object instance that corresponds to the given 380 * NodeFront 381 * 382 * @param {NodeFront} nodeFront 383 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 384 * loaded in the toolbox 385 * @return {MarkupContainer} 386 */ 387 function getContainerForNodeFront(nodeFront, { markup }) { 388 return markup.getContainer(nodeFront); 389 } 390 391 /** 392 * Get the MarkupContainer object instance that corresponds to the given 393 * selector 394 * 395 * @param {string | NodeFront} selector 396 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 397 * loaded in the toolbox 398 * @param {boolean} Set to true in the event that the node shouldn't be found. 399 * @return {MarkupContainer} 400 */ 401 var getContainerForSelector = async function ( 402 selector, 403 inspector, 404 expectFailure = false 405 ) { 406 info("Getting the markup-container for node " + selector); 407 const nodeFront = await getNodeFront(selector, inspector); 408 const container = getContainerForNodeFront(nodeFront, inspector); 409 410 if (expectFailure) { 411 ok(!container, "Shouldn't find markup-container for selector: " + selector); 412 } else { 413 ok(container, "Found markup-container for selector: " + selector); 414 } 415 416 return container; 417 }; 418 419 /** 420 * Simulate a mouse-over on the markup-container (a line in the markup-view) 421 * that corresponds to the selector passed. 422 * 423 * @param {string | NodeFront} selector 424 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 425 * loaded in the toolbox 426 * @return {Promise} Resolves when the container is hovered and the higlighter 427 * is shown on the corresponding node 428 */ 429 var hoverContainer = async function (selector, inspector) { 430 const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); 431 info("Hovering over the markup-container for node " + selector); 432 433 const nodeFront = await getNodeFront(selector, inspector); 434 const container = getContainerForNodeFront(nodeFront, inspector); 435 436 const onHighlighterShown = waitForHighlighterTypeShown( 437 inspector.highlighters.TYPES.BOXMODEL 438 ); 439 EventUtils.synthesizeMouseAtCenter( 440 container.tagLine, 441 { type: "mousemove" }, 442 inspector.markup.doc.defaultView 443 ); 444 await onHighlighterShown; 445 }; 446 447 /** 448 * Simulate a click on the markup-container (a line in the markup-view) 449 * that corresponds to the selector passed. 450 * 451 * @param {string | NodeFront} selector 452 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 453 * loaded in the toolbox 454 * @return {Promise} Resolves when the node has been selected. 455 */ 456 var clickContainer = async function (selector, inspector) { 457 info("Clicking on the markup-container for node " + selector); 458 459 const nodeFront = await getNodeFront(selector, inspector); 460 const container = getContainerForNodeFront(nodeFront, inspector); 461 462 const updated = inspector.once("inspector-updated"); 463 EventUtils.synthesizeMouseAtCenter( 464 container.tagLine, 465 { type: "mousedown" }, 466 inspector.markup.doc.defaultView 467 ); 468 EventUtils.synthesizeMouseAtCenter( 469 container.tagLine, 470 { type: "mouseup" }, 471 inspector.markup.doc.defaultView 472 ); 473 return updated; 474 }; 475 476 /** 477 * Simulate the mouse leaving the markup-view area 478 * 479 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 480 * loaded in the toolbox 481 * @return a promise when done 482 */ 483 function mouseLeaveMarkupView(inspector) { 484 info("Leaving the markup-view area"); 485 486 // Find another element to mouseover over in order to leave the markup-view 487 const btn = inspector.toolbox.doc.querySelector("#toolbox-controls"); 488 489 EventUtils.synthesizeMouseAtCenter( 490 btn, 491 { type: "mousemove" }, 492 inspector.toolbox.win 493 ); 494 495 return new Promise(resolve => { 496 executeSoon(resolve); 497 }); 498 } 499 500 /** 501 * Dispatch the copy event on the given element 502 */ 503 function fireCopyEvent(element) { 504 const evt = element.ownerDocument.createEvent("Event"); 505 evt.initEvent("copy", true, true); 506 element.dispatchEvent(evt); 507 } 508 509 /** 510 * Undo the last markup-view action and wait for the corresponding mutation to 511 * occur 512 * 513 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 514 * loaded in the toolbox 515 * @return a promise that resolves when the markup-mutation has been treated or 516 * rejects if no undo action is possible 517 */ 518 function undoChange(inspector) { 519 const canUndo = inspector.markup.undo.canUndo(); 520 ok(canUndo, "The last change in the markup-view can be undone"); 521 if (!canUndo) { 522 return Promise.reject(); 523 } 524 525 const mutated = inspector.once("markupmutation"); 526 inspector.markup.undo.undo(); 527 return mutated; 528 } 529 530 /** 531 * Redo the last markup-view action and wait for the corresponding mutation to 532 * occur 533 * 534 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 535 * loaded in the toolbox 536 * @return a promise that resolves when the markup-mutation has been treated or 537 * rejects if no redo action is possible 538 */ 539 function redoChange(inspector) { 540 const canRedo = inspector.markup.undo.canRedo(); 541 ok(canRedo, "The last change in the markup-view can be redone"); 542 if (!canRedo) { 543 return Promise.reject(); 544 } 545 546 const mutated = inspector.once("markupmutation"); 547 inspector.markup.undo.redo(); 548 return mutated; 549 } 550 551 /** 552 * A helper that fetches a front for a node that matches the given selector or 553 * doctype node if the selector is falsy. 554 */ 555 async function getNodeFrontForSelector(selector, inspector) { 556 if (selector) { 557 info("Retrieving front for selector " + selector); 558 return getNodeFront(selector, inspector); 559 } 560 561 info("Retrieving front for doctype node"); 562 const { nodes } = await inspector.walker.children(inspector.walker.rootNode); 563 return nodes[0]; 564 } 565 566 /** 567 * A simple polling helper that executes a given function until it returns true. 568 * 569 * @param {Function} check A generator function that is expected to return true at some 570 * stage. 571 * @param {string} desc A text description to be displayed when the polling starts. 572 * @param {number} attemptes Optional number of times we poll. Defaults to 10. 573 * @param {number} timeBetweenAttempts Optional time to wait between each attempt. 574 * Defaults to 200ms. 575 */ 576 async function poll(check, desc, attempts = 10, timeBetweenAttempts = 200) { 577 info(desc); 578 579 for (let i = 0; i < attempts; i++) { 580 if (await check()) { 581 return; 582 } 583 await new Promise(resolve => setTimeout(resolve, timeBetweenAttempts)); 584 } 585 586 throw new Error(`Timeout while: ${desc}`); 587 } 588 589 /** 590 * Encapsulate some common operations for highlighter's tests, to have 591 * the tests cleaner, without exposing directly `inspector`, `highlighter`, and 592 * `highlighterTestFront` if not needed. 593 * 594 * @param {string} 595 * The highlighter's type 596 * @return 597 * A generator function that takes an object with `inspector` and `highlighterTestFront` 598 * properties. (see `openInspector`) 599 */ 600 const getHighlighterHelperFor = type => 601 async function ({ inspector, highlighterTestFront }) { 602 const front = inspector.inspectorFront; 603 const highlighter = await front.getHighlighterByType(type); 604 605 let prefix = ""; 606 607 // Internals for mouse events 608 let prevX, prevY; 609 610 // Highlighted node 611 let highlightedNode = null; 612 613 return { 614 set prefix(value) { 615 prefix = value; 616 }, 617 618 get highlightedNode() { 619 if (!highlightedNode) { 620 return null; 621 } 622 623 return { 624 async getComputedStyle(options = {}) { 625 const pageStyle = highlightedNode.inspectorFront.pageStyle; 626 return pageStyle.getComputed(highlightedNode, options); 627 }, 628 }; 629 }, 630 631 get actorID() { 632 if (!highlighter) { 633 return null; 634 } 635 636 return highlighter.actorID; 637 }, 638 639 async show(selector = ":root", options, frameSelector = null) { 640 if (frameSelector) { 641 highlightedNode = await getNodeFrontInFrames( 642 [frameSelector, selector], 643 inspector 644 ); 645 } else { 646 highlightedNode = await getNodeFront(selector, inspector); 647 } 648 return highlighter.show(highlightedNode, options); 649 }, 650 651 async hide() { 652 await highlighter.hide(); 653 }, 654 655 async isElementHidden(id) { 656 return ( 657 (await highlighterTestFront.getHighlighterNodeAttribute( 658 prefix + id, 659 "hidden", 660 highlighter 661 )) === "true" 662 ); 663 }, 664 665 async getElementTextContent(id) { 666 return highlighterTestFront.getHighlighterNodeTextContent( 667 prefix + id, 668 highlighter 669 ); 670 }, 671 672 async getElementAttribute(id, name) { 673 return highlighterTestFront.getHighlighterNodeAttribute( 674 prefix + id, 675 name, 676 highlighter 677 ); 678 }, 679 680 async waitForElementAttributeSet(id, name) { 681 await poll(async function () { 682 const value = await highlighterTestFront.getHighlighterNodeAttribute( 683 prefix + id, 684 name, 685 highlighter 686 ); 687 return !!value; 688 }, `Waiting for element ${id} to have attribute ${name} set`); 689 }, 690 691 async waitForElementAttributeRemoved(id, name) { 692 await poll(async function () { 693 const value = await highlighterTestFront.getHighlighterNodeAttribute( 694 prefix + id, 695 name, 696 highlighter 697 ); 698 return !value; 699 }, `Waiting for element ${id} to have attribute ${name} removed`); 700 }, 701 702 async synthesizeMouse({ 703 selector = ":root", 704 center, 705 x, 706 y, 707 options, 708 } = {}) { 709 if (center === true) { 710 await safeSynthesizeMouseEventAtCenterInContentPage( 711 selector, 712 options 713 ); 714 } else { 715 await safeSynthesizeMouseEventInContentPage(selector, x, y, options); 716 } 717 }, 718 719 // This object will synthesize any "mouse" prefixed event to the 720 // `highlighterTestFront`, using the name of method called as suffix for the 721 // event's name. 722 // If no x, y coords are given, the previous ones are used. 723 // 724 // For example: 725 // mouse.down(10, 20); // synthesize "mousedown" at 10,20 726 // mouse.move(20, 30); // synthesize "mousemove" at 20,30 727 // mouse.up(); // synthesize "mouseup" at 20,30 728 mouse: new Proxy( 729 {}, 730 { 731 get: (target, name) => 732 async function (x = prevX, y = prevY, selector = ":root") { 733 prevX = x; 734 prevY = y; 735 await safeSynthesizeMouseEventInContentPage(selector, x, y, { 736 type: "mouse" + name, 737 }); 738 }, 739 } 740 ), 741 742 async finalize() { 743 highlightedNode = null; 744 await highlighter.finalize(); 745 }, 746 }; 747 }; 748 749 /** 750 * Inspector-scoped wrapper for highlighter helpers to be used in tests. 751 * 752 * @param {Inspector} inspector 753 * Inspector client object instance. 754 * @return {object} Object with helper methods 755 */ 756 function getHighlighterTestHelpers(inspector) { 757 /** 758 * Return a promise which resolves when a highlighter triggers the given event. 759 * 760 * @param {string} type 761 * Highlighter type. 762 * @param {string} eventName 763 * Name of the event to listen to. 764 * @return {Promise} 765 * Promise which resolves when the highlighter event occurs. 766 * Resolves with the data payload attached to the event. 767 */ 768 function _waitForHighlighterTypeEvent(type, eventName) { 769 return new Promise(resolve => { 770 function _handler(data) { 771 if (type === data.type) { 772 inspector.highlighters.off(eventName, _handler); 773 resolve(data); 774 } 775 } 776 777 inspector.highlighters.on(eventName, _handler); 778 }); 779 } 780 781 return { 782 getActiveHighlighter(type) { 783 return inspector.highlighters.getActiveHighlighter(type); 784 }, 785 getNodeForActiveHighlighter(type) { 786 return inspector.highlighters.getNodeForActiveHighlighter(type); 787 }, 788 waitForHighlighterTypeShown(type) { 789 return _waitForHighlighterTypeEvent(type, "highlighter-shown"); 790 }, 791 waitForHighlighterTypeHidden(type) { 792 return _waitForHighlighterTypeEvent(type, "highlighter-hidden"); 793 }, 794 waitForHighlighterTypeRestored(type) { 795 return _waitForHighlighterTypeEvent(type, "highlighter-restored"); 796 }, 797 waitForHighlighterTypeDiscarded(type) { 798 return _waitForHighlighterTypeEvent(type, "highlighter-discarded"); 799 }, 800 }; 801 } 802 803 /** 804 * Wait for the toolbox to emit the styleeditor-selected event and when done 805 * wait for the stylesheet identified by href to be loaded in the stylesheet 806 * editor 807 * 808 * @param {Toolbox} toolbox 809 * @param {string} href 810 * Optional, if not provided, wait for the first editor to be ready 811 * @return a promise that resolves to the editor when the stylesheet editor is 812 * ready 813 */ 814 function waitForStyleEditor(toolbox, href) { 815 info("Waiting for the toolbox to switch to the styleeditor"); 816 817 return new Promise(resolve => { 818 toolbox.once("styleeditor-selected").then(() => { 819 const panel = toolbox.getCurrentPanel(); 820 ok(panel && panel.UI, "Styleeditor panel switched to front"); 821 822 // A helper that resolves the promise once it receives an editor that 823 // matches the expected href. Returns false if the editor was not correct. 824 const gotEditor = editor => { 825 if (!editor) { 826 info("Editor went away after selected?"); 827 return false; 828 } 829 830 const currentHref = editor.styleSheet.href; 831 if (!href || (href && currentHref.endsWith(href))) { 832 info("Stylesheet editor selected"); 833 panel.UI.off("editor-selected", gotEditor); 834 835 editor.getSourceEditor().then(sourceEditor => { 836 info("Stylesheet editor fully loaded"); 837 resolve(sourceEditor); 838 }); 839 840 return true; 841 } 842 843 info("The editor was incorrect. Waiting for editor-selected event."); 844 return false; 845 }; 846 847 // The expected editor may already be selected. Check the if the currently 848 // selected editor is the expected one and if not wait for an 849 // editor-selected event. 850 if (!gotEditor(panel.UI.selectedEditor)) { 851 // The expected editor is not selected (yet). Wait for it. 852 panel.UI.on("editor-selected", gotEditor); 853 } 854 }); 855 }); 856 } 857 858 /** 859 * Checks if document's active element is within the given element. 860 * 861 * @param {HTMLDocument} doc document with active element in question 862 * @param {DOMNode} container element tested on focus containment 863 * @return {boolean} 864 */ 865 function containsFocus(doc, container) { 866 let elm = doc.activeElement; 867 while (elm) { 868 if (elm === container) { 869 return true; 870 } 871 elm = elm.parentNode; 872 } 873 return false; 874 } 875 876 /** 877 * Listen for a new tab to open and return a promise that resolves when one 878 * does and completes the load event. 879 * 880 * @return a promise that resolves to the tab object 881 */ 882 var waitForTab = async function () { 883 info("Waiting for a tab to open"); 884 await once(gBrowser.tabContainer, "TabOpen"); 885 const tab = gBrowser.selectedTab; 886 await BrowserTestUtils.browserLoaded(tab.linkedBrowser); 887 info("The tab load completed"); 888 return tab; 889 }; 890 891 /** 892 * Simulate the key input for the given input in the window. 893 * 894 * @param {string} input 895 * The string value to input 896 * @param {Window} win 897 * The window containing the panel 898 */ 899 function synthesizeKeys(input, win) { 900 for (const key of input.split("")) { 901 EventUtils.synthesizeKey(key, {}, win); 902 } 903 } 904 905 /** 906 * Make sure window is properly focused before sending a key event. 907 * 908 * @param {Window} win 909 * The window containing the panel 910 * @param {string} key 911 * The string value to input 912 */ 913 function focusAndSendKey(win, key) { 914 win.document.documentElement.focus(); 915 EventUtils.sendKey(key, win); 916 } 917 918 /** 919 * Given a Tooltip instance, fake a mouse event on the `target` DOM Element 920 * and assert that the `tooltip` is correctly displayed. 921 * 922 * @param {Tooltip} tooltip 923 * The tooltip instance 924 * @param {DOMElement} target 925 * The DOM Element on which a tooltip should appear 926 * 927 * @return a promise that resolves with the tooltip object 928 */ 929 async function assertTooltipShownOnHover(tooltip, target) { 930 const mouseEvent = new target.ownerDocument.defaultView.MouseEvent( 931 "mousemove", 932 { 933 bubbles: true, 934 } 935 ); 936 target.dispatchEvent(mouseEvent); 937 938 if (!tooltip.isVisible()) { 939 info("Waiting for tooltip to be shown"); 940 await tooltip.once("shown"); 941 } 942 943 ok(tooltip.isVisible(), `The tooltip is visible`); 944 945 return tooltip; 946 } 947 948 /** 949 * Given an inspector `view` object, fake a mouse event on the `target` DOM 950 * Element and assert that the preview tooltip is correctly displayed. 951 * 952 * @param {CssRuleView|ComputedView|...} view 953 * The instance of an inspector panel 954 * @param {DOMElement} target 955 * The DOM Element on which a tooltip should appear 956 * 957 * @return a promise that resolves with the tooltip object 958 */ 959 async function assertShowPreviewTooltip(view, target) { 960 const name = "previewTooltip"; 961 962 // Get the tooltip. If it does not exist one will be created. 963 const tooltip = view.tooltips.getTooltip(name); 964 ok(tooltip, `Tooltip '${name}' has been instantiated`); 965 966 const shown = tooltip.once("shown"); 967 const mouseEvent = new target.ownerDocument.defaultView.MouseEvent( 968 "mousemove", 969 { 970 bubbles: true, 971 } 972 ); 973 target.dispatchEvent(mouseEvent); 974 975 info("Waiting for tooltip to be shown"); 976 await shown; 977 978 ok(tooltip.isVisible(), `The tooltip '${name}' is visible`); 979 980 return tooltip; 981 } 982 983 /** 984 * Given a `tooltip` instance, fake a mouse event on `target` DOM element 985 * and check that the tooltip correctly disappear. 986 * 987 * @param {Tooltip} tooltip 988 * The tooltip instance 989 * @param {DOMElement} target 990 * The DOM Element on which a tooltip should appear 991 */ 992 async function assertTooltipHiddenOnMouseOut(tooltip, target) { 993 // The tooltip actually relies on mousemove events to check if it should be hidden. 994 const mouseEvent = new target.ownerDocument.defaultView.MouseEvent( 995 "mousemove", 996 { 997 bubbles: true, 998 relatedTarget: target, 999 } 1000 ); 1001 target.parentNode.dispatchEvent(mouseEvent); 1002 1003 await tooltip.once("hidden"); 1004 1005 ok(!tooltip.isVisible(), "The tooltip is hidden on mouseout"); 1006 } 1007 1008 /** 1009 * Check the content of a `var()` tooltip on a given rule and property name. 1010 * 1011 * @param {CssRuleView} view 1012 * @param {string} ruleSelector 1013 * @param {string} propertyName 1014 * @param {object} tooltipExpected 1015 * @param {string} tooltipExpected.header: The HTML for the top section 1016 * (might be the only section when the variable is not a registered property and 1017 * there is no starting-style, nor computed value). 1018 * @param {Array<string>} tooltipExpected.headerClasses: Classes applied on the header element 1019 * (no need to include `variable-value` which is always added). 1020 * @param {string} tooltipExpected.computed: The HTML for the computed value section. 1021 * @param {Array<string>} tooltipExpected.computedClasses: Classes applied on the computed value element. 1022 * @param {Integer} tooltipExpected.index: The index in the property value for the variable 1023 * element we want to check. Defaults to 0 so we can quickly check values when only 1024 * one variable is used. 1025 * @param {boolean} tooltipExpected.isMatched: Is the element matched or unmatched, defaults 1026 * to true. 1027 * @param {string} tooltipExpected.startingStyle: The HTML for the starting-style section. 1028 * Pass undefined if the tooltip isn't supposed to have a `@starting-style` section. 1029 * @param {Array<string>} tooltipExpected.startingStyleClasses: Classes applied on the 1030 * starting-style value element. 1031 * @param {object} tooltipExpected.registeredProperty: Object whose properties should match 1032 * the displayed registered property fields, e.g: 1033 * {syntax:`"<color>"`, inherits:"true", "initial-value": "10px"} 1034 * The properties values are the HTML of the dd elements. 1035 * Pass undefined if the tooltip isn't supposed to have a @property section. 1036 */ 1037 async function assertVariableTooltipForProperty( 1038 view, 1039 ruleSelector, 1040 propertyName, 1041 { 1042 computed, 1043 computedClasses = ["theme-fg-color1"], 1044 header, 1045 headerClasses = ["theme-fg-color1"], 1046 index = 0, 1047 isMatched = true, 1048 registeredProperty, 1049 startingStyle, 1050 startingStyleClasses = ["theme-fg-color1"], 1051 } 1052 ) { 1053 // retrieve tooltip target 1054 const variableEl = await waitFor( 1055 () => 1056 getRuleViewProperty( 1057 view, 1058 ruleSelector, 1059 propertyName 1060 ).valueSpan.querySelectorAll(".inspector-variable,.inspector-unmatched")[ 1061 index 1062 ] 1063 ); 1064 1065 if (isMatched) { 1066 ok( 1067 !variableEl.classList.contains("inspector-unmatched"), 1068 `CSS variable #${index} for ${propertyName} in ${ruleSelector} is matched` 1069 ); 1070 } else { 1071 ok( 1072 variableEl.classList.contains("inspector-unmatched"), 1073 `CSS variable #${index} for ${propertyName} in ${ruleSelector} is unmatched` 1074 ); 1075 } 1076 1077 const previewTooltip = await assertShowPreviewTooltip(view, variableEl); 1078 const valueEl = previewTooltip.panel.querySelector(".variable-value"); 1079 const computedValueEl = previewTooltip.panel.querySelector(".computed div"); 1080 const startingStyleEl = previewTooltip.panel.querySelector( 1081 ".starting-style div" 1082 ); 1083 const registeredPropertyEl = previewTooltip.panel.querySelector( 1084 ".registered-property dl" 1085 ); 1086 is( 1087 valueEl?.innerHTML, 1088 header, 1089 `CSS variable #${index} preview tooltip has expected header text for ${propertyName} in ${ruleSelector}` 1090 ); 1091 Assert.deepEqual( 1092 [...valueEl.classList], 1093 ["variable-value", ...headerClasses], 1094 `CSS variable #${index} preview tooltip has expected classes for ${propertyName} in ${ruleSelector}` 1095 ); 1096 1097 if (typeof computed !== "string") { 1098 is( 1099 computedValueEl, 1100 null, 1101 `CSS variable #${index} preview tooltip doesn't have computed value section for ${propertyName} in ${ruleSelector}` 1102 ); 1103 } else { 1104 is( 1105 computedValueEl?.innerHTML, 1106 computed, 1107 `CSS variable #${index} preview tooltip has expected computed value section for ${propertyName} in ${ruleSelector}` 1108 ); 1109 Assert.deepEqual( 1110 [...computedValueEl.classList], 1111 computedClasses, 1112 `CSS variable #${index} preview tooltip has expected classes on computed value for ${propertyName} in ${ruleSelector}` 1113 ); 1114 } 1115 1116 if (!registeredProperty) { 1117 is( 1118 registeredPropertyEl, 1119 null, 1120 `CSS variable #${index} preview tooltip doesn't have registered property section for ${propertyName} in ${ruleSelector}` 1121 ); 1122 } else { 1123 const dts = registeredPropertyEl.querySelectorAll("dt"); 1124 const registeredPropertyEntries = Object.entries(registeredProperty); 1125 is( 1126 dts.length, 1127 registeredPropertyEntries.length, 1128 `CSS variable #${index} preview tooltip has the expected number of element in the registered property section for ${propertyName} in ${ruleSelector}` 1129 ); 1130 for (let i = 0; i < registeredPropertyEntries.length; i++) { 1131 const [label, value] = registeredPropertyEntries[i]; 1132 const dt = dts[i]; 1133 const dd = dt.nextElementSibling; 1134 is( 1135 dt.innerText, 1136 `${label}:`, 1137 `CSS variable #${index} preview tooltip has expected ${label} registered property element for ${propertyName} in ${ruleSelector}` 1138 ); 1139 is( 1140 dd.innerHTML, 1141 value, 1142 `CSS variable #${index} preview tooltip has expected HTML for ${label} registered property element for ${propertyName} in ${ruleSelector}` 1143 ); 1144 } 1145 } 1146 1147 if (!startingStyle) { 1148 is( 1149 startingStyleEl, 1150 null, 1151 `CSS variable #${index} preview tooltip doesn't have a starting-style section for ${propertyName} in ${ruleSelector}` 1152 ); 1153 } else { 1154 is( 1155 startingStyleEl?.innerHTML, 1156 startingStyle, 1157 `CSS variable #${index} preview tooltip has expected starting-style section for ${propertyName} in ${ruleSelector}` 1158 ); 1159 Assert.deepEqual( 1160 [...startingStyleEl.classList], 1161 startingStyleClasses, 1162 `CSS variable #${index} preview tooltip has expected classes on starting-style value for ${propertyName} in ${ruleSelector}` 1163 ); 1164 } 1165 1166 await assertTooltipHiddenOnMouseOut(previewTooltip, variableEl); 1167 } 1168 1169 /** 1170 * Get the text displayed for a given DOM Element's textContent within the 1171 * markup view. 1172 * 1173 * @param {string} selector 1174 * @param {InspectorPanel} inspector 1175 * @return {string} The text displayed in the markup view 1176 */ 1177 async function getDisplayedNodeTextContent(selector, inspector) { 1178 // We have to ensure that the textContent is displayed, for that the DOM 1179 // Element has to be selected in the markup view and to be expanded. 1180 await selectNode(selector, inspector); 1181 1182 const container = await getContainerForSelector(selector, inspector); 1183 await inspector.markup.expandNode(container.node); 1184 await waitForMultipleChildrenUpdates(inspector); 1185 if (container) { 1186 const textContainer = container.elt.querySelector("pre"); 1187 return textContainer?.textContent; 1188 } 1189 return null; 1190 } 1191 1192 /** 1193 * Toggle the shapes highlighter by simulating a click on the toggle 1194 * in the rules view with the given selector and property 1195 * 1196 * @param {CssRuleView} view 1197 * The instance of the rule-view panel 1198 * @param {string} selector 1199 * The selector in the rule-view to look for the property in 1200 * @param {string} property 1201 * The name of the property 1202 * @param {boolean} show 1203 * If true, the shapes highlighter is being shown. If false, it is being hidden 1204 * @param {Options} options 1205 * Config option for the shapes highlighter. Contains: 1206 * - {Boolean} transformMode: whether to show the highlighter in transforms mode 1207 */ 1208 async function toggleShapesHighlighter( 1209 view, 1210 selector, 1211 property, 1212 show, 1213 options = {} 1214 ) { 1215 info( 1216 `Toggle shapes highlighter ${ 1217 show ? "on" : "off" 1218 } for ${property} on ${selector}` 1219 ); 1220 const highlighters = view.highlighters; 1221 const container = getRuleViewProperty(view, selector, property).valueSpan; 1222 const shapesToggle = container.querySelector(".inspector-shapeswatch"); 1223 1224 const metaKey = options.transformMode; 1225 const ctrlKey = options.transformMode; 1226 1227 if (show) { 1228 const onHighlighterShown = highlighters.once("shapes-highlighter-shown"); 1229 EventUtils.sendMouseEvent( 1230 { type: "click", metaKey, ctrlKey }, 1231 shapesToggle, 1232 view.styleWindow 1233 ); 1234 await onHighlighterShown; 1235 } else { 1236 const onHighlighterHidden = highlighters.once("shapes-highlighter-hidden"); 1237 EventUtils.sendMouseEvent( 1238 { type: "click", metaKey, ctrlKey }, 1239 shapesToggle, 1240 view.styleWindow 1241 ); 1242 await onHighlighterHidden; 1243 } 1244 } 1245 1246 /** 1247 * Toggle the provided markup container by clicking on the expand arrow and waiting for 1248 * children to update. Similar to expandContainer helper, but this method 1249 * uses a click rather than programatically calling expandNode(). 1250 * 1251 * @param {InspectorPanel} inspector 1252 * The current inspector instance. 1253 * @param {MarkupContainer} container 1254 * The markup container to click on. 1255 * @param {object} modifiers 1256 * options.altKey {Boolean} Use the altKey modifier, to recursively apply 1257 * the action to all the children of the container. 1258 */ 1259 async function toggleContainerByClick( 1260 inspector, 1261 container, 1262 { altKey = false } = {} 1263 ) { 1264 EventUtils.synthesizeMouseAtCenter( 1265 container.expander, 1266 { 1267 altKey, 1268 }, 1269 inspector.markup.doc.defaultView 1270 ); 1271 1272 // Wait for any pending children updates 1273 await waitForMultipleChildrenUpdates(inspector); 1274 } 1275 1276 /** 1277 * Simulate a color change in a given color picker tooltip. 1278 * 1279 * @param {Spectrum} colorPicker 1280 * The color picker widget. 1281 * @param {Array} newRgba 1282 * Array of the new rgba values to be set in the color widget. 1283 */ 1284 async function simulateColorPickerChange(colorPicker, newRgba) { 1285 info("Getting the spectrum colorpicker object"); 1286 const spectrum = await colorPicker.spectrum; 1287 info("Setting the new color"); 1288 spectrum.rgb = newRgba; 1289 info("Applying the change"); 1290 spectrum.updateUI(); 1291 spectrum.onChange(); 1292 } 1293 1294 /** 1295 * Assert method to compare the current content of the markupview to a text based tree. 1296 * 1297 * @param {string} tree 1298 * Multiline string representing the markup view tree, for instance: 1299 * `root 1300 * child1 1301 * subchild1 1302 * subchild2 1303 * child2 1304 * subchild3!slotted` 1305 * child3!ignore-children 1306 * Each sub level should be indented by 2 spaces. 1307 * Each line contains text expected to match with the text of the corresponding 1308 * node in the markup view. Some suffixes are supported: 1309 * - !slotted -> indicates that the line corresponds to the slotted version 1310 * - !ignore-children -> the node might have children but do not assert them 1311 * @param {string} selector 1312 * A CSS selector that will uniquely match the "root" element from the tree 1313 * @param {Inspector} inspector 1314 * The inspector instance. 1315 */ 1316 async function assertMarkupViewAsTree(tree, selector, inspector) { 1317 const { markup } = inspector; 1318 1319 info(`Find and expand the shadow DOM host matching selector ${selector}.`); 1320 const rootFront = await getNodeFront(selector, inspector); 1321 const rootContainer = markup.getContainer(rootFront); 1322 1323 const parsedTree = _parseMarkupViewTree(tree); 1324 const treeRoot = parsedTree.children[0]; 1325 await _checkMarkupViewNode(treeRoot, rootContainer, inspector); 1326 } 1327 1328 async function _checkMarkupViewNode(treeNode, container, inspector) { 1329 const { node, children, path } = treeNode; 1330 info(`Checking [${path}]`); 1331 1332 const ignoreChildren = node.includes("!ignore-children"); 1333 const slotted = node.includes("!slotted"); 1334 1335 // Remove optional suffixes. 1336 const nodeText = node.replace("!slotted", "").replace("!ignore-children", ""); 1337 1338 assertContainerHasText(container, nodeText); 1339 1340 if (slotted) { 1341 assertContainerSlotted(container); 1342 } 1343 1344 if (ignoreChildren) { 1345 return; 1346 } 1347 1348 if (!children.length) { 1349 ok(!container.canExpand, "Container for [" + path + "] has no children"); 1350 return; 1351 } 1352 1353 // Expand the container if not already done. 1354 if (!container.expanded) { 1355 await expandContainer(inspector, container); 1356 } 1357 1358 const containers = container.getChildContainers(); 1359 is( 1360 containers.length, 1361 children.length, 1362 "Node [" + path + "] has the expected number of children" 1363 ); 1364 for (let i = 0; i < children.length; i++) { 1365 await _checkMarkupViewNode(children[i], containers[i], inspector); 1366 } 1367 } 1368 1369 /** 1370 * Helper designed to parse a tree represented as: 1371 * root 1372 * child1 1373 * subchild1 1374 * subchild2 1375 * child2 1376 * subchild3!slotted 1377 * 1378 * Lines represent a simplified view of the markup, where the trimmed line is supposed to 1379 * be included in the text content of the actual markupview container. 1380 * This method returns an object that can be passed to _checkMarkupViewNode() to verify 1381 * the current markup view displays the expected structure. 1382 */ 1383 function _parseMarkupViewTree(inputString) { 1384 const tree = { 1385 level: 0, 1386 children: [], 1387 }; 1388 let lines = inputString.split("\n"); 1389 lines = lines.filter(l => l.trim()); 1390 1391 let currentNode = tree; 1392 for (const line of lines) { 1393 const nodeString = line.trim(); 1394 const level = line.split(" ").length; 1395 1396 let parent; 1397 if (level > currentNode.level) { 1398 parent = currentNode; 1399 } else { 1400 parent = currentNode.parent; 1401 for (let i = 0; i < currentNode.level - level; i++) { 1402 parent = parent.parent; 1403 } 1404 } 1405 1406 const node = { 1407 node: nodeString, 1408 children: [], 1409 parent, 1410 level, 1411 path: (parent.path ? parent.path + " > " : "") + nodeString, 1412 }; 1413 1414 parent.children.push(node); 1415 currentNode = node; 1416 } 1417 1418 return tree; 1419 } 1420 1421 /** 1422 * Assert whether the provided container is slotted. 1423 */ 1424 function assertContainerSlotted(container) { 1425 ok(container.isSlotted(), "Container is a slotted container"); 1426 ok( 1427 container.elt.querySelector(".reveal-link"), 1428 "Slotted container has a reveal link element" 1429 ); 1430 } 1431 1432 /** 1433 * Check if the provided text can be matched anywhere in the text content for the provided 1434 * container. 1435 */ 1436 function assertContainerHasText(container, expectedText) { 1437 const textContent = container.elt.textContent; 1438 ok( 1439 textContent.includes(expectedText), 1440 `Container has expected text "${expectedText}"${!textContent.includes(expectedText) ? ` - got "${textContent}"` : ""}` 1441 ); 1442 } 1443 1444 function waitForMutation(inspector, type) { 1445 return waitForNMutations(inspector, type, 1); 1446 } 1447 1448 function waitForNMutations(inspector, type, count) { 1449 info(`Expecting ${count} markupmutation of type ${type}`); 1450 let receivedMutations = 0; 1451 return new Promise(resolve => { 1452 inspector.on("markupmutation", function onMutation(mutations) { 1453 const validMutations = mutations.filter(m => m.type === type).length; 1454 receivedMutations = receivedMutations + validMutations; 1455 if (receivedMutations == count) { 1456 inspector.off("markupmutation", onMutation); 1457 resolve(); 1458 } 1459 }); 1460 }); 1461 } 1462 1463 /** 1464 * Move the mouse on the content page at the x,y position and check the color displayed 1465 * in the eyedropper label. 1466 * 1467 * @param {HighlighterTestFront} highlighterTestFront 1468 * @param {number} x 1469 * @param {number} y 1470 * @param {string} expectedColor: Hexa string of the expected color 1471 * @param {string} assertionDescription 1472 */ 1473 async function checkEyeDropperColorAt( 1474 highlighterTestFront, 1475 x, 1476 y, 1477 expectedColor, 1478 assertionDescription 1479 ) { 1480 info(`Move mouse to ${x},${y}`); 1481 await safeSynthesizeMouseEventInContentPage(":root", x, y, { 1482 type: "mousemove", 1483 }); 1484 1485 const colorValue = await highlighterTestFront.getEyeDropperColorValue(); 1486 is(colorValue, expectedColor, assertionDescription); 1487 } 1488 1489 /** 1490 * Delete the provided node front using the context menu in the markup view. 1491 * Will resolve after the inspector UI was fully updated. 1492 * 1493 * @param {NodeFront} node 1494 * The node front to delete. 1495 * @param {Inspector} inspector 1496 * The current inspector panel instance. 1497 */ 1498 async function deleteNodeWithContextMenu(node, inspector) { 1499 const container = inspector.markup.getContainer(node); 1500 1501 const allMenuItems = openContextMenuAndGetAllItems(inspector, { 1502 target: container.tagLine, 1503 }); 1504 const menuItem = allMenuItems.find(item => item.id === "node-menu-delete"); 1505 const onInspectorUpdated = inspector.once("inspector-updated"); 1506 1507 info("Clicking 'Delete Node' in the context menu."); 1508 is(menuItem.disabled, false, "delete menu item is enabled"); 1509 menuItem.click(); 1510 1511 // close the open context menu 1512 EventUtils.synthesizeKey("KEY_Escape"); 1513 1514 info("Waiting for inspector to update."); 1515 await onInspectorUpdated; 1516 1517 // Since the mutations are sent asynchronously from the server, the 1518 // inspector-updated event triggered by the deletion might happen before 1519 // the mutation is received and the element is removed from the 1520 // breadcrumbs. See bug 1284125. 1521 if (inspector.breadcrumbs.indexOf(node) > -1) { 1522 info("Crumbs haven't seen deletion. Waiting for breadcrumbs-updated."); 1523 await inspector.once("breadcrumbs-updated"); 1524 } 1525 } 1526 1527 /** 1528 * Forces the content page to reflow and waits for the next repaint. 1529 */ 1530 function reflowContentPage() { 1531 return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { 1532 return new Promise(resolve => { 1533 content.document.documentElement.offsetWidth; 1534 content.requestAnimationFrame(resolve); 1535 }); 1536 }); 1537 } 1538 1539 /** 1540 * Get all box-model regions' adjusted boxquads for the given element 1541 * 1542 * @param {string | Array} selector The node selector to target a given element 1543 * @return {Promise<object>} A promise that resolves with an object with each property of 1544 * a box-model region, each of them being an object with the p1/p2/p3/p4 properties. 1545 */ 1546 async function getAllAdjustedQuadsForContentPageElement( 1547 selector, 1548 useTopWindowAsBoundary = true 1549 ) { 1550 const selectors = Array.isArray(selector) ? selector : [selector]; 1551 1552 const browsingContext = 1553 selectors.length == 1 1554 ? gBrowser.selectedBrowser.browsingContext 1555 : await getBrowsingContextInFrames( 1556 gBrowser.selectedBrowser.browsingContext, 1557 selectors.slice(0, -1) 1558 ); 1559 1560 const inBrowsingContextSelector = selectors.at(-1); 1561 return SpecialPowers.spawn( 1562 browsingContext, 1563 [inBrowsingContextSelector, useTopWindowAsBoundary], 1564 (_selector, _useTopWindowAsBoundary) => { 1565 const { require } = ChromeUtils.importESModule( 1566 "resource://devtools/shared/loader/Loader.sys.mjs" 1567 ); 1568 const { 1569 getAdjustedQuads, 1570 } = require("resource://devtools/shared/layout/utils.js"); 1571 1572 const node = content.document.querySelector(_selector); 1573 1574 const boundaryWindow = _useTopWindowAsBoundary ? content.top : content; 1575 const regions = {}; 1576 for (const boxType of ["content", "padding", "border", "margin"]) { 1577 regions[boxType] = getAdjustedQuads(boundaryWindow, node, boxType); 1578 } 1579 1580 return regions; 1581 } 1582 ); 1583 } 1584 1585 /** 1586 * Assert that the box-model highlighter's current position corresponds to the 1587 * given node boxquads. 1588 * 1589 * @param {HighlighterTestFront} highlighterTestFront 1590 * @param {string} selector The node selector to get the boxQuads from 1591 */ 1592 async function isNodeCorrectlyHighlighted(highlighterTestFront, selector) { 1593 const boxModel = await highlighterTestFront.getBoxModelStatus(); 1594 1595 const useTopWindowAsBoundary = !!highlighterTestFront.parentFront.isTopLevel; 1596 const regions = await getAllAdjustedQuadsForContentPageElement( 1597 selector, 1598 useTopWindowAsBoundary 1599 ); 1600 1601 for (const boxType of ["content", "padding", "border", "margin"]) { 1602 const [quad] = regions[boxType]; 1603 for (const point in boxModel[boxType].points) { 1604 is( 1605 boxModel[boxType].points[point].x, 1606 quad[point].x, 1607 `${selector} ${boxType} point ${point} x coordinate is correct` 1608 ); 1609 is( 1610 boxModel[boxType].points[point].y, 1611 quad[point].y, 1612 `${selector} ${boxType} point ${point} y coordinate is correct` 1613 ); 1614 } 1615 } 1616 } 1617 1618 /** 1619 * Get the position and size of the measuring tool. 1620 * 1621 * @param {object} Object returned by getHighlighterHelperFor() 1622 * @return {Promise<object>} A promise that resolves with an object containing 1623 * the x, y, width, and height properties of the measuring tool which has 1624 * been drawn on-screen 1625 */ 1626 async function getAreaRect({ getElementAttribute }) { 1627 // The 'box-path' element holds the width and height of the 1628 // measuring area as well as the position relative to its 1629 // parent <g> element. 1630 const d = await getElementAttribute("box-path", "d"); 1631 // The tool element itself is a <g> element grouping all paths. 1632 // Though <g> elements do not have coordinates by themselves, 1633 // therefore it is positioned using the 'transform' CSS property. 1634 // So, in order to get the position of the measuring area, the 1635 // coordinates need to be read from the translate() function. 1636 const transform = await getElementAttribute("tool", "transform"); 1637 const reDir = /(\d+) (\d+)/g; 1638 const reTransform = /(\d+),(\d+)/; 1639 const coords = { 1640 x: 0, 1641 y: 0, 1642 width: 0, 1643 height: 0, 1644 }; 1645 let match; 1646 while ((match = reDir.exec(d))) { 1647 let [, x, y] = match; 1648 x = Number(x); 1649 y = Number(y); 1650 if (x < coords.x) { 1651 coords.x = x; 1652 } 1653 if (y < coords.y) { 1654 coords.y = y; 1655 } 1656 if (x > coords.width) { 1657 coords.width = x; 1658 } 1659 if (y > coords.height) { 1660 coords.height = y; 1661 } 1662 } 1663 1664 match = reTransform.exec(transform); 1665 coords.x += Number(match[1]); 1666 coords.y += Number(match[2]); 1667 1668 return coords; 1669 } 1670 1671 /** 1672 * Follow a sequence of keys to be pressed in the markup view search input and check 1673 * that the input value and the suggestions are the expected ones. 1674 * 1675 * @param {Inspector} inspector 1676 * @param {Array} expected: This is the array describing the sequence. 1677 * Each item hasthe following shape: 1678 * - key {String}: The keyboard key that is pressed 1679 * - value {String}: The expected input value after the key was pressed 1680 * - suggestions {Array<String>}: An array of the labels in the autocomplete popup. 1681 * Pass an empty array if the popup should be hidden. 1682 */ 1683 async function checkMarkupSearchSuggestions(inspector, expected) { 1684 const searchBox = inspector.searchBox; 1685 const popup = inspector.searchSuggestions.searchPopup; 1686 1687 await focusSearchBoxUsingShortcut(inspector.panelWin); 1688 1689 for (const { key, suggestions, value } of expected) { 1690 info("Pressing " + key + " to get " + JSON.stringify(suggestions)); 1691 1692 const command = once(searchBox, "input"); 1693 const onSearchProcessingDone = 1694 inspector.searchSuggestions.once("processing-done"); 1695 EventUtils.synthesizeKey(key, {}, inspector.panelWin); 1696 await command; 1697 1698 is(searchBox.value, value, "search input has expected value"); 1699 1700 info("Waiting for search query to complete"); 1701 await onSearchProcessingDone; 1702 1703 info( 1704 "Query completed. Performing checks for input '" + 1705 searchBox.value + 1706 "' - key pressed: " + 1707 key 1708 ); 1709 1710 if (suggestions.length === 0) { 1711 ok(!popup.isOpen, `There is no suggestion for "${searchBox.value}"`); 1712 } else { 1713 Assert.deepEqual( 1714 popup.getItems().map(item => item.label), 1715 suggestions, 1716 `Suggestions are correct for "${searchBox.value}"` 1717 ); 1718 } 1719 } 1720 }