shared-head.js (38390B)
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 /* eslint no-unused-vars: [2, {"vars": "local"}] */ 8 /* globals getHighlighterTestFront, openToolboxForTab, gBrowser */ 9 /* import-globals-from ../../shared/test/shared-head.js */ 10 11 var { 12 getInplaceEditorForSpan: inplaceEditor, 13 } = require("resource://devtools/client/shared/inplace-editor.js"); 14 15 // This file contains functions related to the inspector that are also of interest to 16 // other test directores as well. 17 18 /** 19 * Open the toolbox, with the inspector tool visible. 20 * 21 * @param {string} hostType Optional hostType, as defined in Toolbox.HostType 22 * @return {Promise} A promise that resolves when the inspector is ready.The promise 23 * resolves with an object containing the following properties: 24 * - toolbox 25 * - inspector 26 * - highlighterTestFront 27 */ 28 var openInspector = async function (hostType) { 29 info("Opening the inspector"); 30 31 const toolbox = await openToolboxForTab( 32 gBrowser.selectedTab, 33 "inspector", 34 hostType 35 ); 36 const inspector = toolbox.getPanel("inspector"); 37 38 const highlighterTestFront = await getHighlighterTestFront(toolbox); 39 40 return { toolbox, inspector, highlighterTestFront }; 41 }; 42 43 /** 44 * Open the toolbox, with the inspector tool visible, and the one of the sidebar 45 * tabs selected. 46 * 47 * @param {string} id 48 * The ID of the sidebar tab to be opened 49 * @return {Promise<object>} A promise that resolves when the inspector is ready and the tab is 50 * visible and ready. The promise resolves with an object containing the 51 * following properties: 52 * - toolbox 53 * - inspector 54 * - highlighterTestFront 55 */ 56 var openInspectorSidebarTab = async function (id) { 57 const { toolbox, inspector, highlighterTestFront } = await openInspector(); 58 59 info("Selecting the " + id + " sidebar"); 60 61 const onSidebarSelect = inspector.sidebar.once("select"); 62 if (id === "layoutview") { 63 // The layout view should wait until the box-model and grid-panel are ready. 64 const onBoxModelViewReady = inspector.once("boxmodel-view-updated"); 65 const onGridPanelReady = inspector.once("grid-panel-updated"); 66 inspector.sidebar.select(id); 67 await onBoxModelViewReady; 68 await onGridPanelReady; 69 } else { 70 inspector.sidebar.select(id); 71 } 72 await onSidebarSelect; 73 74 return { 75 toolbox, 76 inspector, 77 highlighterTestFront, 78 }; 79 }; 80 81 /** 82 * Open the toolbox, with the inspector tool visible, and the rule-view 83 * sidebar tab selected. 84 * 85 * @param {object} options 86 * @param {boolean} options.overrideDebounce: Whether to replace the rule view debounce 87 * method with manual debounce (requires explicit calls to trigger the debounced calls). 88 * Defaults to true. 89 * @return a promise that resolves when the inspector is ready and the rule view 90 * is visible and ready 91 */ 92 async function openRuleView({ overrideDebounce = true } = {}) { 93 const { inspector, toolbox, highlighterTestFront } = await openInspector(); 94 95 const ruleViewPanel = inspector.getPanel("ruleview"); 96 await ruleViewPanel.readyPromise; 97 const view = ruleViewPanel.view; 98 99 if (overrideDebounce) { 100 // Replace the view to use a custom debounce function that can be triggered manually 101 // through an additional ".flush()" property. 102 view.debounce = manualDebounce(); 103 } 104 105 return { 106 toolbox, 107 inspector, 108 highlighterTestFront, 109 view, 110 }; 111 } 112 113 /** 114 * Open the toolbox, with the inspector tool visible, and the computed-view 115 * sidebar tab selected. 116 * 117 * @return a promise that resolves when the inspector is ready and the computed 118 * view is visible and ready 119 */ 120 function openComputedView() { 121 return openInspectorSidebarTab("computedview").then(data => { 122 const view = data.inspector.getPanel("computedview").computedView; 123 124 return { 125 toolbox: data.toolbox, 126 inspector: data.inspector, 127 highlighterTestFront: data.highlighterTestFront, 128 view, 129 }; 130 }); 131 } 132 133 /** 134 * Open the toolbox, with the inspector tool visible, and the changes view 135 * sidebar tab selected. 136 * 137 * @return a promise that resolves when the inspector is ready and the changes 138 * view is visible and ready 139 */ 140 function openChangesView() { 141 return openInspectorSidebarTab("changesview").then(data => { 142 return { 143 toolbox: data.toolbox, 144 inspector: data.inspector, 145 highlighterTestFront: data.highlighterTestFront, 146 view: data.inspector.getPanel("changesview"), 147 }; 148 }); 149 } 150 151 /** 152 * Open the toolbox, with the inspector tool visible, and the layout view 153 * sidebar tab selected to display the box model view with properties. 154 * 155 * @return {Promise} a promise that resolves when the inspector is ready and the layout 156 * view is visible and ready. 157 */ 158 function openLayoutView() { 159 return openInspectorSidebarTab("layoutview").then(data => { 160 return { 161 toolbox: data.toolbox, 162 inspector: data.inspector, 163 boxmodel: data.inspector.getPanel("boxmodel"), 164 gridInspector: data.inspector.getPanel("layoutview").gridInspector, 165 flexboxInspector: data.inspector.getPanel("layoutview").flexboxInspector, 166 layoutView: data.inspector.getPanel("layoutview"), 167 highlighterTestFront: data.highlighterTestFront, 168 }; 169 }); 170 } 171 172 /** 173 * Select the rule view sidebar tab on an already opened inspector panel. 174 * 175 * @param {InspectorPanel} inspector 176 * The opened inspector panel 177 * @return {CssRuleView} the rule view 178 */ 179 function selectRuleView(inspector) { 180 return inspector.getPanel("ruleview").view; 181 } 182 183 /** 184 * Select the computed view sidebar tab on an already opened inspector panel. 185 * 186 * @param {InspectorPanel} inspector 187 * The opened inspector panel 188 * @return {CssComputedView} the computed view 189 */ 190 function selectComputedView(inspector) { 191 inspector.sidebar.select("computedview"); 192 return inspector.getPanel("computedview").computedView; 193 } 194 195 /** 196 * Select the changes view sidebar tab on an already opened inspector panel. 197 * 198 * @param {InspectorPanel} inspector 199 * The opened inspector panel 200 * @return {ChangesView} the changes view 201 */ 202 function selectChangesView(inspector) { 203 inspector.sidebar.select("changesview"); 204 return inspector.getPanel("changesview"); 205 } 206 207 /** 208 * Select the layout view sidebar tab on an already opened inspector panel. 209 * 210 * @param {InspectorPanel} inspector 211 * @return {BoxModel} the box model 212 */ 213 function selectLayoutView(inspector) { 214 inspector.sidebar.select("layoutview"); 215 return inspector.getPanel("boxmodel"); 216 } 217 218 /** 219 * Get the NodeFront for a node that matches a given css selector, via the 220 * protocol. 221 * 222 * @param {string | NodeFront} selector 223 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 224 * loaded in the toolbox 225 * @return {Promise} Resolves to the NodeFront instance 226 */ 227 function getNodeFront(selector, { walker }) { 228 if (selector._form) { 229 return selector; 230 } 231 return walker.querySelector(walker.rootNode, selector); 232 } 233 234 /** 235 * Set the inspector's current selection to the first match of the given css 236 * selector 237 * 238 * @param {string | NodeFront} selector 239 * @param {InspectorPanel} inspector 240 * The instance of InspectorPanel currently loaded in the toolbox. 241 * @param {string} reason 242 * Defaults to "test" which instructs the inspector not to highlight the 243 * node upon selection. 244 * @param {boolean} isSlotted 245 * Is the selection representing the slotted version the node. 246 * @return {Promise} Resolves when the inspector is updated with the new node 247 */ 248 var selectNode = async function ( 249 selector, 250 inspector, 251 reason = "test", 252 isSlotted 253 ) { 254 info("Selecting the node for '" + selector + "'"); 255 const nodeFront = await getNodeFront(selector, inspector); 256 const updated = inspector.once("inspector-updated"); 257 258 const { 259 ELEMENT_NODE, 260 } = require("resource://devtools/shared/dom-node-constants.js"); 261 const onSelectionCssSelectorsUpdated = 262 nodeFront?.nodeType == ELEMENT_NODE 263 ? inspector.once("selection-css-selectors-updated") 264 : null; 265 266 inspector.selection.setNodeFront(nodeFront, { reason, isSlotted }); 267 await updated; 268 await onSelectionCssSelectorsUpdated; 269 }; 270 271 /** 272 * Using the markupview's _waitForChildren function, wait for all queued 273 * children updates to be handled. 274 * 275 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 276 * loaded in the toolbox 277 * @return a promise that resolves when all queued children updates have been 278 * handled 279 */ 280 function waitForChildrenUpdated({ markup }) { 281 info("Waiting for queued children updates to be handled"); 282 return new Promise(resolve => { 283 markup._waitForChildren().then(() => { 284 executeSoon(resolve); 285 }); 286 }); 287 } 288 289 // The expand all operation of the markup-view calls itself recursively and 290 // there's not one event we can wait for to know when it's done, so use this 291 // helper function to wait until all recursive children updates are done. 292 async function waitForMultipleChildrenUpdates(inspector) { 293 // As long as child updates are queued up while we wait for an update already 294 // wait again 295 if ( 296 inspector.markup._queuedChildUpdates && 297 inspector.markup._queuedChildUpdates.size 298 ) { 299 await waitForChildrenUpdated(inspector); 300 return waitForMultipleChildrenUpdates(inspector); 301 } 302 return null; 303 } 304 305 /** 306 * Expand the provided markup container programmatically and wait for all 307 * children to update. 308 */ 309 async function expandContainer(inspector, container) { 310 await inspector.markup.expandNode(container.node); 311 await waitForMultipleChildrenUpdates(inspector); 312 } 313 314 /** 315 * Get the NodeFront for a node that matches a given css selector inside a 316 * given iframe. 317 * 318 * @param {Array} selectors 319 * Arrays of CSS selectors from the root document to the node. 320 * The last CSS selector of the array is for the node in its frame doc. 321 * The before-last CSS selector is for the frame in its parent frame, etc... 322 * Ex: ["frame.first-frame", ..., "frame.last-frame", ".target-node"] 323 * @param {InspectorPanel} inspector 324 * See `selectNode` 325 * @return {NodeFront} Resolves the corresponding node front. 326 */ 327 async function getNodeFrontInFrames(selectors, inspector) { 328 let walker = inspector.walker; 329 let rootNode = walker.rootNode; 330 331 // clone the array since `selectors` could be used from callsite after. 332 selectors = [...selectors]; 333 // Extract the last selector from the provided array of selectors. 334 const nodeSelector = selectors.pop(); 335 336 // Remaining selectors should all be frame selectors. Renaming for clarity. 337 const frameSelectors = selectors; 338 339 info("Loop through all frame selectors"); 340 for (const frameSelector of frameSelectors) { 341 const url = walker.targetFront.url; 342 info(`Find the frame element for selector ${frameSelector} in ${url}`); 343 344 const frameNodeFront = await walker.querySelector(rootNode, frameSelector); 345 346 // If needed, connect to the corresponding frame target. 347 // Otherwise, reuse the current targetFront. 348 let frameTarget = frameNodeFront.targetFront; 349 if (frameNodeFront.useChildTargetToFetchChildren) { 350 info("Connect to frame and retrieve the targetFront"); 351 frameTarget = await frameNodeFront.connectToFrame(); 352 } 353 354 walker = (await frameTarget.getFront("inspector")).walker; 355 356 if (frameNodeFront.useChildTargetToFetchChildren) { 357 // For frames or browser elements, use the walker's rootNode. 358 rootNode = walker.rootNode; 359 } else { 360 // For same-process frames, select the document front as the root node. 361 // It is a different node from the walker's rootNode. 362 info("Retrieve the children of the frame to find the document node"); 363 const { nodes } = await walker.children(frameNodeFront); 364 rootNode = nodes.find(n => n.nodeType === Node.DOCUMENT_NODE); 365 } 366 } 367 368 return walker.querySelector(rootNode, nodeSelector); 369 } 370 371 /** 372 * Helper to select a node in the markup-view, in a nested tree of 373 * frames/browser elements. The iframes can either be remote or same-process. 374 * 375 * Note: "frame" will refer to either "frame" or "browser" in the documentation 376 * and method. 377 * 378 * @param {Array} selectors 379 * Arrays of CSS selectors from the root document to the node. 380 * The last CSS selector of the array is for the node in its frame doc. 381 * The before-last CSS selector is for the frame in its parent frame, etc... 382 * Ex: ["frame.first-frame", ..., "frame.last-frame", ".target-node"] 383 * @param {InspectorPanel} inspector 384 * See `selectNode` 385 * @param {string} reason 386 * See `selectNode` 387 * @param {boolean} isSlotted 388 * See `selectNode` 389 * @return {NodeFront} The selected node front. 390 */ 391 async function selectNodeInFrames( 392 selectors, 393 inspector, 394 reason = "test", 395 isSlotted 396 ) { 397 const nodeFront = await getNodeFrontInFrames(selectors, inspector); 398 await selectNode(nodeFront, inspector, reason, isSlotted); 399 return nodeFront; 400 } 401 402 /** 403 * Create a throttling function that can be manually "flushed". This is to replace the 404 * use of the `debounce` function from `devtools/client/inspector/shared/utils.js`, which 405 * has a setTimeout that can cause intermittents. 406 * 407 * @return {Function} This function has the same function signature as debounce, but 408 * the property `.flush()` has been added for flushing out any 409 * debounced calls. 410 */ 411 function manualDebounce() { 412 let calls = []; 413 414 function debounce(func, wait, scope) { 415 return function () { 416 const existingCall = calls.find(call => call.func === func); 417 if (existingCall) { 418 existingCall.args = arguments; 419 } else { 420 calls.push({ func, wait, scope, args: arguments }); 421 } 422 }; 423 } 424 425 debounce.flush = function () { 426 calls.forEach(({ func, scope, args }) => func.apply(scope, args)); 427 calls = []; 428 }; 429 430 return debounce; 431 } 432 433 /** 434 * Get the requested rule style property from the current browser. 435 * 436 * @param {number} styleSheetIndex 437 * @param {number} ruleIndex 438 * @param {string} name 439 * @return {string} The value, if found, null otherwise 440 */ 441 442 async function getRulePropertyValue(styleSheetIndex, ruleIndex, name) { 443 return SpecialPowers.spawn( 444 gBrowser.selectedBrowser, 445 [styleSheetIndex, ruleIndex, name], 446 (styleSheetIndexChild, ruleIndexChild, nameChild) => { 447 let value = null; 448 449 info( 450 "Getting the value for property name " + 451 nameChild + 452 " in sheet " + 453 styleSheetIndexChild + 454 " and rule " + 455 ruleIndexChild 456 ); 457 458 const sheet = content.document.styleSheets[styleSheetIndexChild]; 459 if (sheet) { 460 const rule = sheet.cssRules[ruleIndexChild]; 461 if (rule) { 462 value = rule.style.getPropertyValue(nameChild); 463 } 464 } 465 466 return value; 467 } 468 ); 469 } 470 471 /** 472 * Get the requested computed style property from the current browser. 473 * 474 * @param {string} selector 475 * The selector used to obtain the element. 476 * @param {string} pseudo 477 * pseudo id to query, or null. 478 * @param {string} propName 479 * name of the property. 480 */ 481 async function getComputedStyleProperty(selector, pseudo, propName) { 482 return SpecialPowers.spawn( 483 gBrowser.selectedBrowser, 484 [selector, pseudo, propName], 485 (selectorChild, pseudoChild, propNameChild) => { 486 const element = content.document.querySelector(selectorChild); 487 return content 488 .getComputedStyle(element, pseudoChild) 489 .getPropertyValue(propNameChild); 490 } 491 ); 492 } 493 494 /** 495 * Wait until the requested computed style property has the 496 * expected value in the the current browser. 497 * 498 * @param {string} selector 499 * The selector used to obtain the element. 500 * @param {string} pseudo 501 * pseudo id to query, or null. 502 * @param {string} propName 503 * name of the property. 504 * @param {string} expected 505 * expected value of property 506 */ 507 async function waitForComputedStyleProperty( 508 selector, 509 pseudo, 510 propName, 511 expected 512 ) { 513 return SpecialPowers.spawn( 514 gBrowser.selectedBrowser, 515 [selector, pseudo, propName, expected], 516 (selectorChild, pseudoChild, propNameChild, expectedChild) => { 517 const element = content.document.querySelector(selectorChild); 518 return ContentTaskUtils.waitForCondition(() => { 519 const value = content 520 .getComputedStyle(element, pseudoChild) 521 .getPropertyValue(propNameChild); 522 return value === expectedChild; 523 }); 524 } 525 ); 526 } 527 528 /** 529 * Given an inplace editable element, click to switch it to edit mode, wait for 530 * focus 531 * 532 * @return a promise that resolves to the inplace-editor element when ready 533 */ 534 var focusEditableField = async function ( 535 ruleView, 536 editable, 537 xOffset = 1, 538 yOffset = 1, 539 options = {} 540 ) { 541 editable.scrollIntoView(); 542 const onFocus = once(editable.parentNode, "focus", true); 543 info("Clicking on editable field to turn to edit mode"); 544 if (options.type === undefined) { 545 // "mousedown" and "mouseup" flushes any pending layout. Therefore, 546 // if the caller wants to click an element, e.g., closebrace to add new 547 // property, we need to guarantee that the element is clicked here even 548 // if it's moved by flushing the layout because whether the UI is useful 549 // or not when there is pending reflow is not scope of the tests. 550 options.type = "mousedown"; 551 EventUtils.synthesizeMouse( 552 editable, 553 xOffset, 554 yOffset, 555 options, 556 editable.ownerGlobal 557 ); 558 options.type = "mouseup"; 559 EventUtils.synthesizeMouse( 560 editable, 561 xOffset, 562 yOffset, 563 options, 564 editable.ownerGlobal 565 ); 566 } else { 567 EventUtils.synthesizeMouse( 568 editable, 569 xOffset, 570 yOffset, 571 options, 572 editable.ownerGlobal 573 ); 574 } 575 await onFocus; 576 577 info("Editable field gained focus, returning the input field now"); 578 return inplaceEditor(editable.ownerDocument.activeElement); 579 }; 580 581 /** 582 * Get the DOMNode for a css rule in the rule-view that corresponds to the given 583 * selector. 584 * 585 * @param {CssRuleView} view 586 * The instance of the rule-view panel 587 * @param {string} selectorText 588 * The selector in the rule-view for which the rule 589 * object is wanted 590 * @param {number} index 591 * If there are more than 1 rule with the same selector, you may pass a 592 * index to determine which of the rules you want. 593 * @return {DOMNode} 594 */ 595 function getRuleViewRule(view, selectorText, index = 0) { 596 let rule; 597 let pos = 0; 598 for (const r of view.styleDocument.querySelectorAll(".ruleview-rule")) { 599 const selector = r.querySelector( 600 ".ruleview-selectors-container, .ruleview-selector.matched" 601 ); 602 if (selector && selector.textContent === selectorText) { 603 if (index == pos) { 604 rule = r; 605 break; 606 } 607 pos++; 608 } 609 } 610 611 return rule; 612 } 613 614 /** 615 * Get references to the name and value span nodes corresponding to a given 616 * selector and property name in the rule-view. 617 * 618 * @param {CssRuleView} view 619 * The instance of the rule-view panel 620 * @param {string} selectorText 621 * The selector in the rule-view to look for the property in 622 * @param {string} propertyName 623 * The name of the property 624 * @param {object=} options 625 * @param {boolean=} options.wait 626 * When true, returns a promise which waits until a valid rule view 627 * property can be retrieved for the provided selectorText & propertyName. 628 * Defaults to false. 629 * @return {object} An object like {nameSpan: DOMNode, valueSpan: DOMNode} 630 */ 631 function getRuleViewProperty(view, selectorText, propertyName, options = {}) { 632 if (options.wait) { 633 return waitFor(() => 634 _syncGetRuleViewProperty(view, selectorText, propertyName) 635 ); 636 } 637 return _syncGetRuleViewProperty(view, selectorText, propertyName); 638 } 639 640 function _syncGetRuleViewProperty(view, selectorText, propertyName) { 641 const rule = getRuleViewRule(view, selectorText); 642 if (!rule) { 643 return null; 644 } 645 646 // Look for the propertyName in that rule element 647 for (const p of rule.querySelectorAll(".ruleview-property")) { 648 const nameSpan = p.querySelector(".ruleview-propertyname"); 649 const valueSpan = p.querySelector(".ruleview-propertyvalue"); 650 651 if (nameSpan.textContent === propertyName) { 652 return { nameSpan, valueSpan }; 653 } 654 } 655 return null; 656 } 657 658 /** 659 * Get the text value of the property corresponding to a given selector and name 660 * in the rule-view 661 * 662 * @param {CssRuleView} view 663 * The instance of the rule-view panel 664 * @param {string} selectorText 665 * The selector in the rule-view to look for the property in 666 * @param {string} propertyName 667 * The name of the property 668 * @return {string} The property value 669 */ 670 function getRuleViewPropertyValue(view, selectorText, propertyName) { 671 return getRuleViewProperty(view, selectorText, propertyName).valueSpan 672 .textContent; 673 } 674 675 /** 676 * Get a reference to the selector DOM element corresponding to a given selector 677 * in the rule-view 678 * 679 * @param {CssRuleView} view 680 * The instance of the rule-view panel 681 * @param {string} selectorText 682 * The selector in the rule-view to look for 683 * @return {DOMNode} The selector DOM element 684 */ 685 function getRuleViewSelector(view, selectorText) { 686 const rule = getRuleViewRule(view, selectorText); 687 return rule.querySelector( 688 ".ruleview-selectors-container, .ruleview-selector.matched" 689 ); 690 } 691 692 /** 693 * Get a rule-link from the rule-view given the rule index 694 * 695 * @param {CssRuleView} view 696 * The instance of the rule-view panel 697 * @param {number} index 698 * The index of the link to get 699 * @return {DOMNode|null} The link if any at this rule index, or null if it doesn't exist 700 */ 701 function getRuleViewLinkByIndex(view, index) { 702 const ruleEl = view.styleDocument.querySelectorAll(".ruleview-rule")[index]; 703 return ruleEl?.querySelector(".ruleview-rule-source") || null; 704 } 705 706 /** 707 * Get rule-link text from the rule-view given its index 708 * 709 * @param {CssRuleView} view 710 * The instance of the rule-view panel 711 * @param {number} index 712 * The index of the link to get 713 * @return {string} The string at this index 714 */ 715 function getRuleViewLinkTextByIndex(view, index) { 716 const link = getRuleViewLinkByIndex(view, index); 717 return link.querySelector(".ruleview-rule-source-label").textContent; 718 } 719 720 /** 721 * Click on a rule-view's close brace to focus a new property name editor 722 * 723 * @param {RuleEditor} ruleEditor 724 * An instance of RuleEditor that will receive the new property 725 * @return a promise that resolves to the newly created editor when ready and 726 * focused 727 */ 728 var focusNewRuleViewProperty = async function (ruleEditor) { 729 info("Clicking on a close ruleEditor brace to start editing a new property"); 730 731 // Use bottom alignment to avoid scrolling out of the parent element area. 732 ruleEditor.closeBrace.scrollIntoView(false); 733 const editor = await focusEditableField( 734 ruleEditor.ruleView, 735 ruleEditor.closeBrace 736 ); 737 738 is( 739 inplaceEditor(ruleEditor.newPropSpan), 740 editor, 741 "Focused editor is the new property editor." 742 ); 743 744 return editor; 745 }; 746 747 /** 748 * Create a new property name in the rule-view, focusing a new property editor 749 * by clicking on the close brace, and then entering the given text. 750 * Keep in mind that the rule-view knows how to handle strings with multiple 751 * properties, so the input text may be like: "p1:v1;p2:v2;p3:v3". 752 * 753 * @param {RuleEditor} ruleEditor 754 * The instance of RuleEditor that will receive the new property(ies) 755 * @param {string} inputValue 756 * The text to be entered in the new property name field 757 * @return a promise that resolves when the new property name has been entered 758 * and once the value field is focused 759 */ 760 var createNewRuleViewProperty = async function (ruleEditor, inputValue) { 761 info("Creating a new property editor"); 762 const editor = await focusNewRuleViewProperty(ruleEditor); 763 764 info("Entering the value " + inputValue); 765 editor.input.value = inputValue; 766 767 info("Submitting the new value and waiting for value field focus"); 768 const onFocus = once(ruleEditor.element, "focus", true); 769 EventUtils.synthesizeKey( 770 "VK_RETURN", 771 {}, 772 ruleEditor.element.ownerDocument.defaultView 773 ); 774 await onFocus; 775 }; 776 777 /** 778 * Set the search value for the rule-view filter styles search box. 779 * 780 * @param {CssRuleView} view 781 * The instance of the rule-view panel 782 * @param {string} searchValue 783 * The filter search value 784 * @return a promise that resolves when the rule-view is filtered for the 785 * search term 786 */ 787 var setSearchFilter = async function (view, searchValue) { 788 info('Setting filter text to "' + searchValue + '"'); 789 790 const searchField = view.searchField; 791 searchField.focus(); 792 793 const onRuleviewFiltered = view.inspector.once("ruleview-filtered"); 794 for (const key of searchValue.split("")) { 795 EventUtils.synthesizeKey(key, {}, view.styleWindow); 796 } 797 await onRuleviewFiltered; 798 }; 799 800 /** 801 * Flatten all context menu items into a single array to make searching through 802 * it easier. 803 */ 804 function buildContextMenuItems(menu) { 805 const allItems = [].concat.apply( 806 [], 807 menu.items.map(function addItem(item) { 808 if (item.submenu) { 809 return addItem(item.submenu.items); 810 } 811 return item; 812 }) 813 ); 814 815 return allItems; 816 } 817 818 /** 819 * Open the style editor context menu and return all of it's items in a flat array 820 * 821 * @param {CssRuleView} view 822 * The instance of the rule-view panel 823 * @return An array of MenuItems 824 */ 825 function openStyleContextMenuAndGetAllItems(view, target) { 826 const menu = view.contextMenu._openMenu({ target }); 827 return buildContextMenuItems(menu); 828 } 829 830 /** 831 * Open the inspector menu and return all of it's items in a flat array 832 * 833 * @param {InspectorPanel} inspector 834 * @param {object} options to pass into openMenu 835 * @return An array of MenuItems 836 */ 837 function openContextMenuAndGetAllItems(inspector, options) { 838 const menu = inspector.markup.contextMenu._openMenu(options); 839 return buildContextMenuItems(menu); 840 } 841 842 /** 843 * Wait until the elements the given selectors indicate come to have the visited state. 844 * 845 * @param {Tab} tab 846 * The tab where the elements on. 847 * @param {Array} selectors 848 * The selectors for the elements. 849 */ 850 async function waitUntilVisitedState(tab, selectors) { 851 await asyncWaitUntil(async () => { 852 const hasVisitedState = await ContentTask.spawn( 853 tab.linkedBrowser, 854 selectors, 855 args => { 856 // ElementState::VISITED 857 const ELEMENT_STATE_VISITED = 1 << 18; 858 859 for (const selector of args) { 860 const target = 861 content.wrappedJSObject.document.querySelector(selector); 862 if ( 863 !( 864 target && 865 InspectorUtils.getContentState(target) & ELEMENT_STATE_VISITED 866 ) 867 ) { 868 return false; 869 } 870 } 871 return true; 872 } 873 ); 874 return hasVisitedState; 875 }); 876 } 877 878 /** 879 * Return wether or not the passed selector matches an element in the content page. 880 * 881 * @param {string} selector 882 * @returns Promise<Boolean> 883 */ 884 function hasMatchingElementInContentPage(selector) { 885 return SpecialPowers.spawn( 886 gBrowser.selectedBrowser, 887 [selector], 888 function (innerSelector) { 889 return content.document.querySelector(innerSelector) !== null; 890 } 891 ); 892 } 893 894 /** 895 * Return the number of elements matching the passed selector. 896 * 897 * @param {string} selector 898 * @returns Promise<Number> the number of matching elements 899 */ 900 function getNumberOfMatchingElementsInContentPage(selector) { 901 return SpecialPowers.spawn( 902 gBrowser.selectedBrowser, 903 [selector], 904 function (innerSelector) { 905 return content.document.querySelectorAll(innerSelector).length; 906 } 907 ); 908 } 909 910 /** 911 * Get the property of an element in the content page 912 * 913 * @param {string} selector: The selector to get the element we want the property of 914 * @param {string} propertyName: The name of the property we want the value of 915 * @returns {Promise} A promise that returns with the value of the property for the element 916 */ 917 function getContentPageElementProperty(selector, propertyName) { 918 return SpecialPowers.spawn( 919 gBrowser.selectedBrowser, 920 [selector, propertyName], 921 function (innerSelector, innerPropertyName) { 922 return content.document.querySelector(innerSelector)[innerPropertyName]; 923 } 924 ); 925 } 926 927 /** 928 * Set the property of an element in the content page 929 * 930 * @param {string} selector: The selector to get the element we want to set the property on 931 * @param {string} propertyName: The name of the property we want to set 932 * @param {string} propertyValue: The value that is going to be assigned to the property 933 * @returns {Promise} 934 */ 935 function setContentPageElementProperty(selector, propertyName, propertyValue) { 936 return SpecialPowers.spawn( 937 gBrowser.selectedBrowser, 938 [selector, propertyName, propertyValue], 939 function (innerSelector, innerPropertyName, innerPropertyValue) { 940 content.document.querySelector(innerSelector)[innerPropertyName] = 941 innerPropertyValue; 942 } 943 ); 944 } 945 946 /** 947 * Get all the attributes for a DOM Node living in the content page. 948 * 949 * @param {string} selector The node selector 950 * @returns {Array<object>} An array of {name, value} objects. 951 */ 952 async function getContentPageElementAttributes(selector) { 953 return SpecialPowers.spawn( 954 gBrowser.selectedBrowser, 955 [selector], 956 _selector => { 957 const node = content.document.querySelector(_selector); 958 return Array.from(node.attributes).map(({ name, value }) => ({ 959 name, 960 value, 961 })); 962 } 963 ); 964 } 965 966 /** 967 * Get an attribute on a DOM Node living in the content page. 968 * 969 * @param {string} selector The node selector 970 * @param {string} attribute The attribute name 971 * @return {string} value The attribute value 972 */ 973 async function getContentPageElementAttribute(selector, attribute) { 974 return SpecialPowers.spawn( 975 gBrowser.selectedBrowser, 976 [selector, attribute], 977 (_selector, _attribute) => { 978 return content.document.querySelector(_selector).getAttribute(_attribute); 979 } 980 ); 981 } 982 983 /** 984 * Set an attribute on a DOM Node living in the content page. 985 * 986 * @param {string} selector The node selector 987 * @param {string} attribute The attribute name 988 * @param {string} value The attribute value 989 */ 990 async function setContentPageElementAttribute(selector, attribute, value) { 991 return SpecialPowers.spawn( 992 gBrowser.selectedBrowser, 993 [selector, attribute, value], 994 (_selector, _attribute, _value) => { 995 content.document 996 .querySelector(_selector) 997 .setAttribute(_attribute, _value); 998 } 999 ); 1000 } 1001 1002 /** 1003 * Remove an attribute from a DOM Node living in the content page. 1004 * 1005 * @param {string} selector The node selector 1006 * @param {string} attribute The attribute name 1007 */ 1008 async function removeContentPageElementAttribute(selector, attribute) { 1009 return SpecialPowers.spawn( 1010 gBrowser.selectedBrowser, 1011 [selector, attribute], 1012 (_selector, _attribute) => { 1013 content.document.querySelector(_selector).removeAttribute(_attribute); 1014 } 1015 ); 1016 } 1017 1018 /** 1019 * Get the rule editor from the rule-view given its index 1020 * 1021 * @param {CssRuleView} ruleView 1022 * The instance of the rule-view panel 1023 * @param {number} childrenIndex 1024 * The children index of the element to get 1025 * @param {number} nodeIndex 1026 * The child node index of the element to get 1027 * @return {DOMNode} The rule editor if any at this index 1028 */ 1029 function getRuleViewRuleEditor(ruleView, childrenIndex, nodeIndex) { 1030 const child = ruleView.element.children[childrenIndex]; 1031 if (!child) { 1032 return null; 1033 } 1034 1035 return nodeIndex !== undefined 1036 ? child.childNodes[nodeIndex]?._ruleEditor 1037 : child._ruleEditor; 1038 } 1039 1040 /** 1041 * Get the TextProperty instance corresponding to a CSS declaration 1042 * from a CSS rule in the Rules view. 1043 * 1044 * @param {RuleView} ruleView 1045 * Instance of RuleView. 1046 * @param {number} ruleIndex 1047 * The index of the CSS rule where to find the declaration. 1048 * @param {object} declaration 1049 * An object representing the target declaration e.g. { color: red }. 1050 * The first TextProperty instance which matches will be returned. 1051 * @return {TextProperty} 1052 */ 1053 function getTextProperty(ruleView, ruleIndex, declaration) { 1054 const ruleEditor = getRuleViewRuleEditor(ruleView, ruleIndex); 1055 const [[name, value]] = Object.entries(declaration); 1056 const textProp = ruleEditor.rule.textProps.find(prop => { 1057 return prop.name === name && prop.value === value; 1058 }); 1059 1060 if (!textProp) { 1061 throw Error( 1062 `Declaration ${name}:${value} not found on rule at index ${ruleIndex}` 1063 ); 1064 } 1065 1066 return textProp; 1067 } 1068 1069 /** 1070 * Simulate changing the value of a property in a rule in the rule-view. 1071 * 1072 * @param {CssRuleView} ruleView 1073 * The instance of the rule-view panel 1074 * @param {TextProperty} textProp 1075 * The instance of the TextProperty to be changed 1076 * @param {string} value 1077 * The new value to be used. If null is passed, then the value will be 1078 * deleted 1079 * @param {object} options 1080 * @param {boolean} options.blurNewProperty 1081 * After the value has been changed, a new property would have been 1082 * focused. This parameter is true by default, and that causes the new 1083 * property to be blurred. Set to false if you don't want this. 1084 * @param {number} options.flushCount 1085 * The ruleview uses a manual flush for tests only, and some properties are 1086 * only updated after several flush. Allow tests to trigger several flushes 1087 * if necessary. Defaults to 1. 1088 */ 1089 async function setProperty( 1090 ruleView, 1091 textProp, 1092 value, 1093 { blurNewProperty = true, flushCount = 1 } = {} 1094 ) { 1095 info("Set property to: " + value); 1096 const editor = await focusEditableField(ruleView, textProp.editor.valueSpan); 1097 1098 // Because of the manual flush approach used for tests, we might have an 1099 // unknown number of debounced "preview" requests . Each preview should 1100 // synchronously emit "start-preview-property-value". 1101 // Listen to both this event and "ruleview-changed" which is emitted at the 1102 // end of a preview and make sure each preview completes successfully. 1103 let previewStartedCounter = 0; 1104 const onStartPreview = () => previewStartedCounter++; 1105 ruleView.on("start-preview-property-value", onStartPreview); 1106 1107 let previewCounter = 0; 1108 const onPreviewApplied = () => previewCounter++; 1109 ruleView.on("ruleview-changed", onPreviewApplied); 1110 1111 if (value === null) { 1112 const onPopupOpened = once(ruleView.popup, "popup-opened"); 1113 EventUtils.synthesizeKey("VK_DELETE", {}, ruleView.styleWindow); 1114 await onPopupOpened; 1115 } else { 1116 await wait(500); 1117 // Since some time have passed since we made the input visible and focused it, 1118 // we might have some previous async work that causes the input to be blurred 1119 // (see intermittent Bug 1845152). 1120 // Make sure the input is focused before triggering the keyboard event. 1121 editor.input.focus(); 1122 EventUtils.sendString(value, ruleView.styleWindow); 1123 } 1124 1125 info(`Flush debounced ruleview methods (remaining: ${flushCount})`); 1126 ruleView.debounce.flush(); 1127 await waitFor(() => previewCounter >= previewStartedCounter); 1128 1129 flushCount--; 1130 1131 while (flushCount > 0) { 1132 // Wait for some time before triggering a new flush to let new debounced 1133 // functions queue in-between. 1134 await wait(100); 1135 1136 info(`Flush debounced ruleview methods (remaining: ${flushCount})`); 1137 ruleView.debounce.flush(); 1138 await waitFor(() => previewCounter >= previewStartedCounter); 1139 1140 flushCount--; 1141 } 1142 1143 ruleView.off("start-preview-property-value", onStartPreview); 1144 ruleView.off("ruleview-changed", onPreviewApplied); 1145 1146 const onValueDone = ruleView.once("ruleview-changed"); 1147 // In case the popup was opened, wait until it closes 1148 let onPopupClosed; 1149 if (ruleView.popup?.isOpen) { 1150 // it might happen that the popup is still in the process of being opened, 1151 // so wait until it's properly opened 1152 await ruleView.popup._pendingShowPromise; 1153 onPopupClosed = once(ruleView.popup, "popup-closed"); 1154 } 1155 1156 // Since some time have passed since we made the input visible and focused it, 1157 // we might have some previous async work that causes the input to be blurred 1158 // (see intermittent Bug 1845152). 1159 // Make sure the input is focused before triggering the keyboard event. 1160 editor.input.focus(); 1161 EventUtils.synthesizeKey( 1162 blurNewProperty ? "VK_RETURN" : "VK_TAB", 1163 {}, 1164 ruleView.styleWindow 1165 ); 1166 1167 info("Waiting for another ruleview-changed after setting property"); 1168 await onValueDone; 1169 1170 const focusNextOnEnter = Services.prefs.getBoolPref( 1171 "devtools.inspector.rule-view.focusNextOnEnter" 1172 ); 1173 if (blurNewProperty && !focusNextOnEnter) { 1174 info("Force blur on the active element"); 1175 ruleView.styleDocument.activeElement.blur(); 1176 } 1177 await onPopupClosed; 1178 } 1179 1180 /** 1181 * Return the markup view search input 1182 * 1183 * @param {Inspector} inspector 1184 * @returns {Element} 1185 */ 1186 function getMarkupViewSearchInput(inspector) { 1187 return inspector.panelWin.document.getElementById("inspector-searchbox"); 1188 } 1189 1190 /** 1191 * Using the inspector panel's selector search box, search for a given selector. 1192 * The selector input string will be entered in the input field and the <ENTER> 1193 * keypress will be simulated. 1194 */ 1195 async function searchInMarkupView(inspector, search) { 1196 info(`Entering "${search}" into the markup view search field`); 1197 const inspectorSearchboxEl = getMarkupViewSearchInput(inspector); 1198 inspectorSearchboxEl.focus(); 1199 inspectorSearchboxEl.value = search; 1200 1201 const onNewNodeFront = inspector.selection.once("new-node-front"); 1202 const onSearchResult = inspector.search.once("search-result"); 1203 const onSearchResultHighlightingUpdated = inspector.markup.once( 1204 "search-results-highlighting-updated" 1205 ); 1206 EventUtils.sendKey("return", inspector.panelWin); 1207 1208 info("Wait for search-result"); 1209 await onSearchResult; 1210 1211 info("Wait for new node being selected"); 1212 await onNewNodeFront; 1213 1214 info("Wait for the search results highlighted to be updated"); 1215 await onSearchResultHighlightingUpdated; 1216 }