head.js (53204B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 /* eslint no-unused-vars: [2, {"vars": "local"}] */ 4 5 "use strict"; 6 7 // Import the inspector's head.js first (which itself imports shared-head.js). 8 Services.scriptloader.loadSubScript( 9 "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", 10 this 11 ); 12 13 var { 14 getInplaceEditorForSpan: inplaceEditor, 15 } = require("resource://devtools/client/shared/inplace-editor.js"); 16 17 const { 18 COMPATIBILITY_TOOLTIP_MESSAGE, 19 } = require("resource://devtools/client/inspector/rules/constants.js"); 20 21 const ROOT_TEST_DIR = getRootDirectory(gTestPath); 22 23 const STYLE_INSPECTOR_L10N = new LocalizationHelper( 24 "devtools/shared/locales/styleinspector.properties" 25 ); 26 27 /** 28 * When a tooltip is closed, this ends up "commiting" the value changed within 29 * the tooltip (e.g. the color in case of a colorpicker) which, in turn, ends up 30 * setting the value of the corresponding css property in the rule-view. 31 * Use this function to close the tooltip and make sure the test waits for the 32 * ruleview-changed event. 33 * 34 * @param {SwatchBasedEditorTooltip} editorTooltip 35 * @param {CSSRuleView} view 36 */ 37 async function hideTooltipAndWaitForRuleViewChanged(editorTooltip, view) { 38 const onModified = view.once("ruleview-changed"); 39 const onHidden = editorTooltip.tooltip.once("hidden"); 40 editorTooltip.hide(); 41 await onModified; 42 await onHidden; 43 } 44 45 /** 46 * Polls a given generator function waiting for it to return true. 47 * 48 * @param {Function} validatorFn 49 * A validator generator function that returns a boolean. 50 * This is called every few milliseconds to check if the result is true. 51 * When it is true, the promise resolves. 52 * @param {string} name 53 * Optional name of the test. This is used to generate 54 * the success and failure messages. 55 * @return a promise that resolves when the function returned true or rejects 56 * if the timeout is reached 57 */ 58 var waitForSuccess = async function (validatorFn, desc = "untitled") { 59 let i = 0; 60 while (true) { 61 info("Checking: " + desc); 62 if (await validatorFn()) { 63 ok(true, "Success: " + desc); 64 break; 65 } 66 i++; 67 if (i > 10) { 68 ok(false, "Failure: " + desc); 69 break; 70 } 71 await new Promise(r => setTimeout(r, 200)); 72 } 73 }; 74 75 /** 76 * Simulate a color change in a given color picker tooltip, and optionally wait 77 * for a given element in the page to have its style changed as a result. 78 * Note that this function assumes that the colorpicker popup is already open 79 * and it won't close it after having selected the new color. 80 * 81 * @param {RuleView} ruleView 82 * The related rule view instance 83 * @param {SwatchColorPickerTooltip} colorPicker 84 * @param {Array} newRgba 85 * The new color to be set [r, g, b, a] 86 * @param {object} expectedChange 87 * Optional object that needs the following props: 88 * - {String} selector The selector to the element in the page that 89 * will have its style changed. 90 * - {String} name The style name that will be changed 91 * - {String} value The expected style value 92 * The style will be checked like so: getComputedStyle(element)[name] === value 93 */ 94 var simulateColorPickerChange = async function ( 95 ruleView, 96 colorPicker, 97 newRgba, 98 expectedChange 99 ) { 100 let onComputedStyleChanged; 101 if (expectedChange) { 102 const { selector, name, value } = expectedChange; 103 onComputedStyleChanged = waitForComputedStyleProperty( 104 selector, 105 null, 106 name, 107 value 108 ); 109 } 110 const onRuleViewChanged = ruleView.once("ruleview-changed"); 111 info("Getting the spectrum colorpicker object"); 112 const spectrum = colorPicker.spectrum; 113 info("Setting the new color"); 114 spectrum.rgb = newRgba; 115 info("Applying the change"); 116 spectrum.updateUI(); 117 spectrum.onChange(); 118 info("Waiting for rule-view to update"); 119 await onRuleViewChanged; 120 121 if (expectedChange) { 122 info("Waiting for the style to be applied on the page"); 123 await onComputedStyleChanged; 124 } 125 }; 126 127 /** 128 * Open the color picker popup for a given property in a given rule and 129 * simulate a color change. Optionally wait for a given element in the page to 130 * have its style changed as a result. 131 * 132 * @param {RuleView} view 133 * The related rule view instance 134 * @param {number} ruleIndex 135 * Which rule to target in the rule view 136 * @param {number} propIndex 137 * Which property to target in the rule 138 * @param {Array} newRgba 139 * The new color to be set [r, g, b, a] 140 * @param {object} expectedChange 141 * Optional object that needs the following props: 142 * - {String} selector The selector to the element in the page that 143 * will have its style changed. 144 * - {String} name The style name that will be changed 145 * - {String} value The expected style value 146 * The style will be checked like so: getComputedStyle(element)[name] === value 147 */ 148 var openColorPickerAndSelectColor = async function ( 149 view, 150 ruleIndex, 151 propIndex, 152 newRgba, 153 expectedChange 154 ) { 155 const ruleEditor = getRuleViewRuleEditor(view, ruleIndex); 156 const propEditor = ruleEditor.rule.textProps[propIndex].editor; 157 const swatch = propEditor.valueSpan.querySelector(".inspector-colorswatch"); 158 const cPicker = view.tooltips.getTooltip("colorPicker"); 159 160 info("Opening the colorpicker by clicking the color swatch"); 161 const onColorPickerReady = cPicker.once("ready"); 162 swatch.click(); 163 await onColorPickerReady; 164 165 await simulateColorPickerChange(view, cPicker, newRgba, expectedChange); 166 167 return { propEditor, swatch, cPicker }; 168 }; 169 170 /** 171 * Open the cubicbezier popup for a given property in a given rule and 172 * simulate a curve change. Optionally wait for a given element in the page to 173 * have its style changed as a result. 174 * 175 * @param {RuleView} view 176 * The related rule view instance 177 * @param {number} ruleIndex 178 * Which rule to target in the rule view 179 * @param {number} propIndex 180 * Which property to target in the rule 181 * @param {Array} coords 182 * The new coordinates to be used, e.g. [0.1, 2, 0.9, -1] 183 * @param {object} expectedChange 184 * Optional object that needs the following props: 185 * - {String} selector The selector to the element in the page that 186 * will have its style changed. 187 * - {String} name The style name that will be changed 188 * - {String} value The expected style value 189 * The style will be checked like so: getComputedStyle(element)[name] === value 190 */ 191 var openCubicBezierAndChangeCoords = async function ( 192 view, 193 ruleIndex, 194 propIndex, 195 coords, 196 expectedChange 197 ) { 198 const ruleEditor = getRuleViewRuleEditor(view, ruleIndex); 199 const propEditor = ruleEditor.rule.textProps[propIndex].editor; 200 const swatch = propEditor.valueSpan.querySelector(".inspector-bezierswatch"); 201 const bezierTooltip = view.tooltips.getTooltip("cubicBezier"); 202 203 info("Opening the cubicBezier by clicking the swatch"); 204 const onBezierWidgetReady = bezierTooltip.once("ready"); 205 swatch.click(); 206 await onBezierWidgetReady; 207 208 const widget = await bezierTooltip.widget; 209 210 info("Simulating a change of curve in the widget"); 211 const onRuleViewChanged = view.once("ruleview-changed"); 212 widget.coordinates = coords; 213 await onRuleViewChanged; 214 215 if (expectedChange) { 216 info("Waiting for the style to be applied on the page"); 217 const { selector, name, value } = expectedChange; 218 await waitForComputedStyleProperty(selector, null, name, value); 219 } 220 221 return { propEditor, swatch, bezierTooltip }; 222 }; 223 224 /** 225 * Simulate adding a new property in an existing rule in the rule-view. 226 * 227 * @param {CssRuleView} view 228 * The instance of the rule-view panel 229 * @param {number} ruleIndex 230 * The index of the rule to use. 231 * @param {string} name 232 * The name for the new property 233 * @param {string} value 234 * The value for the new property 235 * @param {object=} options 236 * @param {string=} options.commitValueWith 237 * Which key should be used to commit the new value. VK_TAB is used by 238 * default, but tests might want to use another key to test cancelling 239 * for exemple. 240 * If set to null, no keys will be hit, so the input will still be focused 241 * at the end of this function 242 * @param {boolean=} options.blurNewProperty 243 * After the new value has been added, a new property would have been 244 * focused. This parameter is true by default, and that causes the new 245 * property to be blurred. Set to false if you don't want this. 246 * @return {TextProperty} The instance of the TextProperty that was added 247 */ 248 var addProperty = async function ( 249 view, 250 ruleIndex, 251 name, 252 value, 253 { commitValueWith = "VK_TAB", blurNewProperty = true } = {} 254 ) { 255 info("Adding new property " + name + ":" + value + " to rule " + ruleIndex); 256 257 const ruleEditor = getRuleViewRuleEditor(view, ruleIndex); 258 let editor = await focusNewRuleViewProperty(ruleEditor); 259 const numOfProps = ruleEditor.rule.textProps.length; 260 261 const onMutations = new Promise(r => { 262 // If the rule index is 0, then we are updating the rule for the "element" 263 // selector in the rule view. 264 // This rule is actually updating the style attribute of the element, and 265 // therefore we can expect mutations. 266 // For any other rule index, no mutation should be created, we can resolve 267 // immediately. 268 if (ruleIndex !== 0) { 269 r(); 270 } 271 272 // Use CSS.escape for the name in order to match the logic at 273 // devtools/client/fronts/inspector/rule-rewriter.js 274 // This leads to odd values in the style attribute and might change in the 275 // future. See https://bugzilla.mozilla.org/show_bug.cgi?id=1765943 276 const expectedAttributeValue = `${CSS.escape(name)}: ${value}`; 277 view.inspector.walker.on( 278 "mutations", 279 function onWalkerMutations(mutations) { 280 // Wait until we receive a mutation which updates the style attribute 281 // with the expected value. 282 const receivedLastMutation = mutations.some( 283 mut => 284 mut.attributeName === "style" && 285 mut.newValue.includes(expectedAttributeValue) 286 ); 287 if (receivedLastMutation) { 288 view.inspector.walker.off("mutations", onWalkerMutations); 289 r(); 290 } 291 } 292 ); 293 }); 294 295 info("Adding name " + name); 296 editor.input.value = name; 297 is( 298 editor.input.getAttribute("aria-label"), 299 "New property name", 300 "New property name input has expected aria-label" 301 ); 302 303 const onNameAdded = view.once("ruleview-changed"); 304 EventUtils.synthesizeKey("VK_TAB", {}, view.styleWindow); 305 await onNameAdded; 306 307 // Focus has moved to the value inplace-editor automatically. 308 editor = inplaceEditor(view.styleDocument.activeElement); 309 const textProps = ruleEditor.rule.textProps; 310 const textProp = textProps[textProps.length - 1]; 311 312 is( 313 ruleEditor.rule.textProps.length, 314 numOfProps + 1, 315 "A new test property was added" 316 ); 317 is( 318 editor, 319 inplaceEditor(textProp.editor.valueSpan), 320 "The inplace editor appeared for the value" 321 ); 322 323 info("Adding value " + value); 324 // Setting the input value schedules a preview to be shown in 10ms which 325 // triggers a ruleview-changed event (see bug 1209295). 326 const onPreview = view.once("ruleview-changed"); 327 editor.input.value = value; 328 329 ok( 330 !!editor.input.getAttribute("aria-labelledby"), 331 "The value input has an aria-labelledby attribute…" 332 ); 333 is( 334 editor.input.getAttribute("aria-labelledby"), 335 textProp.editor.nameSpan.id, 336 "…which references the property name input" 337 ); 338 339 view.debounce.flush(); 340 await onPreview; 341 342 if (commitValueWith === null) { 343 return textProp; 344 } 345 346 const onRuleViewChanged = view.once("ruleview-changed"); 347 EventUtils.synthesizeKey(commitValueWith, {}, view.styleWindow); 348 await onRuleViewChanged; 349 350 info( 351 "Waiting for DOM mutations in case the property was added to the element style" 352 ); 353 await onMutations; 354 355 if (blurNewProperty) { 356 view.styleDocument.activeElement.blur(); 357 } 358 359 return textProp; 360 }; 361 362 /** 363 * Change the name of a property in a rule in the rule-view. 364 * 365 * @param {CssRuleView} view 366 * The instance of the rule-view panel. 367 * @param {TextProperty} textProp 368 * The instance of the TextProperty to be changed. 369 * @param {string} name 370 * The new property name. 371 */ 372 var renameProperty = async function (view, textProp, name) { 373 await focusEditableField(view, textProp.editor.nameSpan); 374 375 const onNameDone = view.once("ruleview-changed"); 376 info(`Rename the property to ${name}`); 377 EventUtils.sendString(name, view.styleWindow); 378 EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); 379 info("Wait for property name."); 380 await onNameDone; 381 382 if ( 383 !Services.prefs.getBoolPref("devtools.inspector.rule-view.focusNextOnEnter") 384 ) { 385 return; 386 } 387 388 // Renaming the property auto-advances the focus to the value input. Exiting without 389 // committing will still fire a change event. @see TextPropertyEditor._onValueDone(). 390 // Wait for that event too before proceeding. 391 const onValueDone = view.once("ruleview-changed"); 392 EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); 393 info("Wait for property value."); 394 await onValueDone; 395 }; 396 397 /** 398 * Simulate removing a property from an existing rule in the rule-view. 399 * 400 * @param {CssRuleView} view 401 * The instance of the rule-view panel 402 * @param {TextProperty} textProp 403 * The instance of the TextProperty to be removed 404 * @param {boolean} blurNewProperty 405 * After the property has been removed, a new property would have been 406 * focused. This parameter is true by default, and that causes the new 407 * property to be blurred. Set to false if you don't want this. 408 */ 409 var removeProperty = async function (view, textProp, blurNewProperty = true) { 410 await focusEditableField(view, textProp.editor.nameSpan); 411 412 const onModifications = view.once("ruleview-changed"); 413 info("Deleting the property name now"); 414 EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow); 415 EventUtils.synthesizeKey("VK_TAB", {}, view.styleWindow); 416 await onModifications; 417 418 if (blurNewProperty) { 419 view.styleDocument.activeElement.blur(); 420 } 421 }; 422 423 /** 424 * Simulate clicking the enable/disable checkbox next to a property in a rule. 425 * 426 * @param {CssRuleView} view 427 * The instance of the rule-view panel 428 * @param {TextProperty} textProp 429 * The instance of the TextProperty to be enabled/disabled 430 */ 431 var togglePropStatus = async function (view, textProp) { 432 const onRuleViewRefreshed = view.once("ruleview-changed"); 433 textProp.editor.enable.click(); 434 await onRuleViewRefreshed; 435 }; 436 437 /** 438 * Create a new rule by clicking on the "add rule" button. 439 * This will leave the selector inplace-editor active. 440 * 441 * @param {InspectorPanel} inspector 442 * The instance of InspectorPanel currently loaded in the toolbox 443 * @param {CssRuleView} view 444 * The instance of the rule-view panel 445 * @returns {Rule} a promise that resolves the new model Rule after the rule has 446 * been added 447 */ 448 async function addNewRule(inspector, view) { 449 const onNewRuleAdded = view.once("new-rule-added"); 450 info("Adding the new rule using the button"); 451 view.addRuleButton.click(); 452 453 info("Waiting for new-rule-added event…"); 454 const rule = await onNewRuleAdded; 455 info("…received new-rule-added"); 456 457 return rule; 458 } 459 460 /** 461 * Create a new rule by clicking on the "add rule" button, dismiss the editor field and 462 * verify that the selector is correct. 463 * 464 * @param {InspectorPanel} inspector 465 * The instance of InspectorPanel currently loaded in the toolbox 466 * @param {CssRuleView} view 467 * The instance of the rule-view panel 468 * @param {string} expectedSelector 469 * The value we expect the selector to have 470 * @param {number} expectedIndex 471 * The index we expect the rule to have in the rule-view 472 * @returns {Rule} a promise that resolves the new model Rule after the rule has 473 * been added 474 */ 475 async function addNewRuleAndDismissEditor( 476 inspector, 477 view, 478 expectedSelector, 479 expectedIndex 480 ) { 481 const rule = await addNewRule(inspector, view); 482 483 info("Getting the new rule at index " + expectedIndex); 484 const ruleEditor = getRuleViewRuleEditor(view, expectedIndex); 485 const editor = ruleEditor.selectorText.ownerDocument.activeElement; 486 is( 487 editor.value, 488 expectedSelector, 489 "The editor for the new selector has the correct value: " + expectedSelector 490 ); 491 492 info("Pressing escape to leave the editor"); 493 EventUtils.synthesizeKey("KEY_Escape"); 494 495 is( 496 ruleEditor.selectorText.textContent, 497 expectedSelector, 498 "The new selector has the correct text: " + expectedSelector 499 ); 500 501 return rule; 502 } 503 504 /** 505 * Simulate a sequence of non-character keys (return, escape, tab) and wait for 506 * a given element to receive the focus. 507 * 508 * @param {CssRuleView} view 509 * The instance of the rule-view panel 510 * @param {DOMNode} element 511 * The element that should be focused 512 * @param {Array} keys 513 * Array of non-character keys, the part that comes after "DOM_VK_" eg. 514 * "RETURN", "ESCAPE" 515 * @return a promise that resolves after the element received the focus 516 */ 517 async function sendKeysAndWaitForFocus(view, element, keys) { 518 const onFocus = once(element, "focus", true); 519 for (const key of keys) { 520 EventUtils.sendKey(key, view.styleWindow); 521 } 522 await onFocus; 523 } 524 525 /** 526 * Wait for a markupmutation event on the inspector that is for a style modification. 527 * 528 * @param {InspectorPanel} inspector 529 * @return {Promise} 530 */ 531 function waitForStyleModification(inspector) { 532 return new Promise(function (resolve) { 533 function checkForStyleModification(mutations) { 534 for (const mutation of mutations) { 535 if ( 536 mutation.type === "attributes" && 537 mutation.attributeName === "style" 538 ) { 539 inspector.off("markupmutation", checkForStyleModification); 540 resolve(); 541 return; 542 } 543 } 544 } 545 inspector.on("markupmutation", checkForStyleModification); 546 }); 547 } 548 549 /** 550 * Click on the icon next to the selector of a CSS rule in the Rules view 551 * to toggle the selector highlighter. If a selector highlighter is not already visible 552 * for the given selector, wait for it to be shown. Otherwise, wait for it to be hidden. 553 * 554 * @param {CssRuleView} view 555 * The instance of the Rules view 556 * @param {string} selectorText 557 * The selector of the CSS rule to look for 558 * @param {number} index 559 * If there are more CSS rules with the same selector, use this index 560 * to determine which one should be retrieved. Defaults to 0 (first) 561 */ 562 async function clickSelectorIcon(view, selectorText, index = 0) { 563 const { inspector } = view; 564 const rule = getRuleViewRule(view, selectorText, index); 565 566 info(`Waiting for icon to be available for selector: ${selectorText}`); 567 const icon = await waitFor(() => { 568 return rule.querySelector(".js-toggle-selector-highlighter"); 569 }); 570 571 // Grab the actual selector associated with the matched icon. 572 // For inline styles, the CSS rule with the "element" selector actually points to 573 // a generated unique selector, for example: "div:nth-child(1)". 574 // The selector highlighter is invoked with this unique selector. 575 // Continuing to use selectorText ("element") would fail some of the checks below. 576 const selector = icon.dataset.computedSelector; 577 578 const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } = 579 getHighlighterTestHelpers(inspector); 580 581 // If there is an active selector highlighter, get its configuration options. 582 // Will be undefined if there isn't an active selector highlighter. 583 const options = inspector.highlighters.getOptionsForActiveHighlighter( 584 inspector.highlighters.TYPES.SELECTOR 585 ); 586 587 // If there is already a highlighter visible for this selector, 588 // wait for hidden event. Otherwise, wait for shown event. 589 const waitForEvent = 590 options?.selector === selector 591 ? waitForHighlighterTypeHidden(inspector.highlighters.TYPES.SELECTOR) 592 : waitForHighlighterTypeShown(inspector.highlighters.TYPES.SELECTOR); 593 594 // Boolean flag whether we waited for a highlighter shown event 595 const waitedForShown = options?.selector !== selector; 596 597 info(`Click the icon for selector: ${selectorText}`); 598 icon.scrollIntoView(); 599 EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow); 600 601 // Promise resolves with event data from either highlighter shown or hidden event. 602 const data = await waitForEvent; 603 return { ...data, isShown: waitedForShown }; 604 } 605 /** 606 * Toggle one of the checkboxes inside the class-panel. Resolved after the DOM mutation 607 * has been recorded. 608 * 609 * @param {CssRuleView} view The rule-view instance. 610 * @param {string} name The class name to find the checkbox. 611 */ 612 async function toggleClassPanelCheckBox(view, name) { 613 info(`Clicking on checkbox for class ${name}`); 614 const checkBox = [ 615 ...view.classPanel.querySelectorAll("[type=checkbox]"), 616 ].find(box => { 617 return box.dataset.name === name; 618 }); 619 620 const onMutation = view.inspector.once("markupmutation"); 621 checkBox.click(); 622 info("Waiting for a markupmutation as a result of toggling this class"); 623 await onMutation; 624 } 625 626 /** 627 * Verify the content of the class-panel. 628 * 629 * @param {CssRuleView} view The rule-view instance 630 * @param {Array} classes The list of expected classes. Each item in this array is an 631 * object with the following properties: {name: {String}, state: {Boolean}} 632 */ 633 function checkClassPanelContent(view, classes) { 634 const checkBoxNodeList = view.classPanel.querySelectorAll("[type=checkbox]"); 635 is( 636 checkBoxNodeList.length, 637 classes.length, 638 "The panel contains the expected number of checkboxes" 639 ); 640 641 for (let i = 0; i < classes.length; i++) { 642 is( 643 checkBoxNodeList[i].dataset.name, 644 classes[i].name, 645 `Checkbox ${i} has the right class name` 646 ); 647 is( 648 checkBoxNodeList[i].checked, 649 classes[i].state, 650 `Checkbox ${i} has the right state` 651 ); 652 } 653 } 654 655 /** 656 * Opens the eyedropper from the colorpicker tooltip 657 * by selecting the colorpicker and then selecting the eyedropper icon 658 * 659 * @param {view} ruleView 660 * @param {swatch} color swatch of a particular property 661 */ 662 async function openEyedropper(view, swatch) { 663 const tooltip = view.tooltips.getTooltip("colorPicker").tooltip; 664 665 info("Click on the swatch"); 666 const onColorPickerReady = view.tooltips 667 .getTooltip("colorPicker") 668 .once("ready"); 669 EventUtils.synthesizeMouseAtCenter(swatch, {}, swatch.ownerGlobal); 670 await onColorPickerReady; 671 672 const dropperButton = tooltip.container.querySelector("#eyedropper-button"); 673 674 info("Click on the eyedropper icon"); 675 const onOpened = tooltip.once("eyedropper-opened"); 676 dropperButton.click(); 677 await onOpened; 678 } 679 680 /** 681 * Gets a set of declarations for a rule index. 682 * 683 * @param {ruleView} view 684 * The rule-view instance. 685 * @param {number} ruleIndex 686 * The index we expect the rule to have in the rule-view. If an array, the first 687 * item is the children index in the rule view, and the second item is the child 688 * node index in the retrieved rule view element. This is helpful to select rules 689 * inside the pseudo element section. 690 * @param {boolean} addCompatibilityData 691 * Optional argument to add compatibility dat with the property data 692 * 693 * @returns A Promise that resolves with a Map containing stringified property declarations e.g. 694 * [ 695 * { 696 * "color:red": 697 * { 698 * propertyName: "color", 699 * propertyValue: "red", 700 * warning: "This won't work", 701 * used: true, 702 * compatibilityData: { 703 * isCompatible: true, 704 * }, 705 * } 706 * }, 707 * ... 708 * ] 709 */ 710 async function getPropertiesForRuleIndex( 711 view, 712 ruleIndex, 713 addCompatibilityData = false 714 ) { 715 const declaration = new Map(); 716 let nodeIndex; 717 if (Array.isArray(ruleIndex)) { 718 [ruleIndex, nodeIndex] = ruleIndex; 719 } 720 const ruleEditor = getRuleViewRuleEditor(view, ruleIndex, nodeIndex); 721 722 for (const currProp of ruleEditor?.rule?.textProps || []) { 723 const icon = currProp.editor.inactiveCssState; 724 const unused = currProp.editor.element.classList.contains("inactive-css"); 725 726 let compatibilityData; 727 let compatibilityIcon; 728 if (addCompatibilityData) { 729 compatibilityData = await currProp.isCompatible(); 730 compatibilityIcon = currProp.editor.compatibilityState; 731 } 732 733 declaration.set(`${currProp.name}:${currProp.value}`, { 734 propertyName: currProp.name, 735 propertyValue: currProp.value, 736 icon, 737 data: currProp.getInactiveCssData(), 738 warning: unused, 739 used: !unused, 740 ...(addCompatibilityData 741 ? { 742 compatibilityData, 743 compatibilityIcon, 744 } 745 : {}), 746 }); 747 } 748 749 return declaration; 750 } 751 752 /** 753 * Toggle a declaration disabled or enabled. 754 * 755 * @param {ruleView} view 756 * The rule-view instance 757 * @param {number} ruleIndex 758 * The index of the CSS rule where we can find the declaration to be 759 * toggled. 760 * @param {object} declaration 761 * An object representing the declaration e.g. { color: "red" }. 762 */ 763 async function toggleDeclaration(view, ruleIndex, declaration) { 764 const textProp = getTextProperty(view, ruleIndex, declaration); 765 const [[name, value]] = Object.entries(declaration); 766 const dec = `${name}:${value}`; 767 ok(textProp, `Declaration "${dec}" found`); 768 769 const newStatus = textProp.enabled ? "disabled" : "enabled"; 770 info(`Toggling declaration "${dec}" of rule ${ruleIndex} to ${newStatus}`); 771 772 await togglePropStatus(view, textProp); 773 info("Toggled successfully."); 774 } 775 776 /** 777 * Update a declaration from a CSS rule in the Rules view 778 * by changing its property name, property value or both. 779 * 780 * @param {RuleView} view 781 * Instance of RuleView. 782 * @param {number} ruleIndex 783 * The index of the CSS rule where to find the declaration. 784 * @param {object} declaration 785 * An object representing the target declaration e.g. { color: red }. 786 * @param {object} newDeclaration 787 * An object representing the desired updated declaration e.g. { display: none }. 788 */ 789 async function updateDeclaration( 790 view, 791 ruleIndex, 792 declaration, 793 newDeclaration = {} 794 ) { 795 const textProp = getTextProperty(view, ruleIndex, declaration); 796 const [[name, value]] = Object.entries(declaration); 797 const [[newName, newValue]] = Object.entries(newDeclaration); 798 799 if (newName && name !== newName) { 800 info( 801 `Updating declaration ${name}:${value}; 802 Changing ${name} to ${newName}` 803 ); 804 await renameProperty(view, textProp, newName); 805 } 806 807 if (newValue && value !== newValue) { 808 info( 809 `Updating declaration ${name}:${value}; 810 Changing ${value} to ${newValue}` 811 ); 812 await setProperty(view, textProp, newValue); 813 } 814 } 815 816 /** 817 * Check whether the given CSS declaration is compatible or not 818 * 819 * @param {ruleView} view 820 * The rule-view instance. 821 * @param {number} ruleIndex 822 * The index we expect the rule to have in the rule-view. 823 * @param {object} declaration 824 * An object representing the declaration e.g. { color: "red" }. 825 * @param {object} options 826 * @param {string | undefined} options.expected 827 * Expected message ID for the given incompatible property. 828 * If the expected message is not specified (undefined), the given declaration 829 * is inferred as cross-browser compatible and is tested for same. 830 * @param {string | null | undefined} options.expectedLearnMoreUrl 831 * Expected learn more link. Pass `null` to check that no "Learn more" link is displayed. 832 */ 833 async function checkDeclarationCompatibility( 834 view, 835 ruleIndex, 836 declaration, 837 { expected, expectedLearnMoreUrl } 838 ) { 839 const declarations = await getPropertiesForRuleIndex(view, ruleIndex, true); 840 const [[name, value]] = Object.entries(declaration); 841 const dec = `${name}:${value}`; 842 const { compatibilityData } = declarations.get(dec); 843 844 is( 845 !expected, 846 compatibilityData.isCompatible, 847 `"${dec}" has the correct compatibility status in the payload` 848 ); 849 850 is(compatibilityData.msgId, expected, `"${dec}" has expected message ID`); 851 852 if (expected) { 853 await checkInteractiveTooltip( 854 view, 855 "compatibility-tooltip", 856 ruleIndex, 857 declaration 858 ); 859 } 860 861 if (expectedLearnMoreUrl !== undefined) { 862 // Show the tooltip 863 const tooltip = view.tooltips.getTooltip("interactiveTooltip"); 864 const onTooltipReady = tooltip.once("shown"); 865 const { compatibilityIcon } = declarations.get(dec); 866 await view.tooltips.onInteractiveTooltipTargetHover(compatibilityIcon); 867 tooltip.show(compatibilityIcon); 868 await onTooltipReady; 869 870 const learnMoreEl = tooltip.panel.querySelector(".link"); 871 if (expectedLearnMoreUrl === null) { 872 ok(!learnMoreEl, `"${dec}" has no "Learn more" link`); 873 } else { 874 ok(learnMoreEl, `"${dec}" has a "Learn more" link`); 875 876 const { link } = await simulateLinkClick(learnMoreEl); 877 is( 878 link, 879 expectedLearnMoreUrl, 880 `Click on ${dec} "Learn more" link navigates user to expected url` 881 ); 882 } 883 884 // Hide the tooltip. 885 const onTooltipHidden = tooltip.once("hidden"); 886 tooltip.hide(); 887 await onTooltipHidden; 888 } 889 } 890 891 /** 892 * Check that a declaration is marked inactive and that it has the expected 893 * warning. 894 * 895 * @param {ruleView} view 896 * The rule-view instance. 897 * @param {number} ruleIndex 898 * The index we expect the rule to have in the rule-view. 899 * @param {object} declaration 900 * An object representing the declaration e.g. { color: "red" }. 901 */ 902 async function checkDeclarationIsInactive(view, ruleIndex, declaration) { 903 const declarations = await getPropertiesForRuleIndex(view, ruleIndex); 904 const [[name, value]] = Object.entries(declaration); 905 const dec = `${name}:${value}`; 906 const { used, warning, icon } = declarations.get(dec); 907 908 ok(!used, `"${dec}" is inactive`); 909 ok(warning, `"${dec}" has a warning`); 910 ok( 911 icon.classList.contains("ruleview-inactive-css-warning"), 912 "Icon has expected icon" 913 ); 914 is(icon.hidden, false, "Icon is visible"); 915 916 await checkInteractiveTooltip( 917 view, 918 "inactive-css-tooltip", 919 ruleIndex, 920 declaration 921 ); 922 } 923 924 /** 925 * Check that a declaration is marked active. 926 * 927 * @param {ruleView} view 928 * The rule-view instance. 929 * @param {number | Array} ruleIndex 930 * The index we expect the rule to have in the rule-view. If an array, the first 931 * item is the children index in the rule view, and the second item is the child 932 * node index in the retrieved rule view element. This is helpful to select rules 933 * inside the pseudo element section. 934 * @param {object} declaration 935 * An object representing the declaration e.g. { color: "red" }. 936 */ 937 async function checkDeclarationIsActive(view, ruleIndex, declaration) { 938 const declarations = await getPropertiesForRuleIndex(view, ruleIndex); 939 const [[name, value]] = Object.entries(declaration); 940 const dec = `${name}:${value}`; 941 const { used, warning } = declarations.get(dec); 942 943 ok(used, `${dec} is active`); 944 ok(!warning, `${dec} has no warning`); 945 } 946 947 /** 948 * Check that a tooltip contains the correct value. 949 * 950 * @param {ruleView} view 951 * The rule-view instance. 952 * @param {string} type 953 * The interactive tooltip type being tested. 954 * @param {number} ruleIndex 955 * The index we expect the rule to have in the rule-view. 956 * @param {object} declaration 957 * An object representing the declaration e.g. { color: "red" }. 958 */ 959 async function checkInteractiveTooltip(view, type, ruleIndex, declaration) { 960 // Get the declaration 961 const declarations = await getPropertiesForRuleIndex( 962 view, 963 ruleIndex, 964 type === "compatibility-tooltip" 965 ); 966 const [[name, value]] = Object.entries(declaration); 967 const dec = `${name}:${value}`; 968 969 // Get the relevant icon and tooltip payload data 970 let icon; 971 let data; 972 if (type === "inactive-css-tooltip") { 973 ({ icon, data } = declarations.get(dec)); 974 } else { 975 const { compatibilityIcon, compatibilityData } = declarations.get(dec); 976 icon = compatibilityIcon; 977 data = compatibilityData; 978 } 979 980 // Get the tooltip. 981 const tooltip = view.tooltips.getTooltip("interactiveTooltip"); 982 983 // Get the necessary tooltip helper to fetch the Fluent template. 984 let tooltipHelper; 985 if (type === "inactive-css-tooltip") { 986 tooltipHelper = view.tooltips.inactiveCssTooltipHelper; 987 } else { 988 tooltipHelper = view.tooltips.compatibilityTooltipHelper; 989 } 990 991 // Get the HTML template. 992 const template = tooltipHelper.getTemplate(data, tooltip); 993 994 // Translate the template using Fluent. 995 const { doc } = tooltip; 996 await doc.l10n.translateFragment(template); 997 998 // Get the expected HTML content of the now translated template. 999 const expected = template.firstElementChild.outerHTML; 1000 1001 // Show the tooltip for the correct icon. 1002 const onTooltipReady = tooltip.once("shown"); 1003 await view.tooltips.onInteractiveTooltipTargetHover(icon); 1004 tooltip.show(icon); 1005 await onTooltipReady; 1006 1007 // Get the tooltip's actual HTML content. 1008 const actual = tooltip.panel.firstElementChild.outerHTML; 1009 1010 // Hide the tooltip. 1011 const onTooltipHidden = tooltip.once("hidden"); 1012 tooltip.hide(); 1013 await onTooltipHidden; 1014 1015 // Finally, check the values. 1016 is(actual, expected, "Tooltip contains the correct value."); 1017 } 1018 1019 /** 1020 * CSS compatibility test runner. 1021 * 1022 * @param {ruleView} view 1023 * The rule-view instance. 1024 * @param {InspectorPanel} inspector 1025 * The instance of InspectorPanel currently loaded in the toolbox. 1026 * @param {Array} tests 1027 * An array of test object for this method to consume e.g. 1028 * [ 1029 * { 1030 * selector: "#flex-item", 1031 * rules: [ 1032 * // Rule Index: 0 1033 * { 1034 * // If the object doesn't include the "expected" 1035 * // key, we consider the declaration as 1036 * // cross-browser compatible and test for same 1037 * color: { value: "green" }, 1038 * }, 1039 * // Rule Index: 1 1040 * { 1041 * cursor: 1042 * { 1043 * value: "grab", 1044 * expected: INCOMPATIBILITY_TOOLTIP_MESSAGE.default, 1045 * expectedLearnMoreUrl: "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/cursor", 1046 * }, 1047 * }, 1048 * ], 1049 * }, 1050 * ... 1051 * ] 1052 */ 1053 async function runCSSCompatibilityTests(view, inspector, tests) { 1054 for (const test of tests) { 1055 if (test.selector) { 1056 await selectNode(test.selector, inspector); 1057 } 1058 1059 for (const [ruleIndex, rules] of test.rules.entries()) { 1060 for (const rule in rules) { 1061 await checkDeclarationCompatibility( 1062 view, 1063 ruleIndex, 1064 { 1065 [rule]: rules[rule].value, 1066 }, 1067 { 1068 expected: rules[rule].expected, 1069 expectedLearnMoreUrl: rules[rule].expectedLearnMoreUrl, 1070 } 1071 ); 1072 } 1073 } 1074 } 1075 } 1076 1077 /** 1078 * Inactive CSS test runner. 1079 * 1080 * @param {ruleView} view 1081 * The rule-view instance. 1082 * @param {InspectorPanel} inspector 1083 * The instance of InspectorPanel currently loaded in the toolbox. 1084 * @param {Array} tests 1085 * An array of test object for this method to consume e.g. 1086 * [ 1087 * { 1088 * selector: "#flex-item", 1089 * // or 1090 * selectNode: (inspector) => { // custom select logic } 1091 * activeDeclarations: [ 1092 * { 1093 * declarations: { 1094 * "order": "2", 1095 * }, 1096 * ruleIndex: 0, 1097 * }, 1098 * { 1099 * declarations: { 1100 * "flex-basis": "auto", 1101 * "flex-grow": "1", 1102 * "flex-shrink": "1", 1103 * }, 1104 * ruleIndex: 1, 1105 * }, 1106 * ], 1107 * inactiveDeclarations: [ 1108 * { 1109 * declaration: { 1110 * "flex-direction": "row", 1111 * }, 1112 * ruleIndex: [1, 0], 1113 * }, 1114 * ], 1115 * }, 1116 * ... 1117 * ] 1118 */ 1119 async function runInactiveCSSTests(view, inspector, tests) { 1120 for (const test of tests) { 1121 if (test.selector) { 1122 await selectNode(test.selector, inspector); 1123 } else if (typeof test.selectNode === "function") { 1124 await test.selectNode(inspector); 1125 } 1126 1127 if (test.activeDeclarations) { 1128 info("Checking whether declarations are marked as used."); 1129 1130 for (const activeDeclarations of test.activeDeclarations) { 1131 for (const [name, value] of Object.entries( 1132 activeDeclarations.declarations 1133 )) { 1134 await checkDeclarationIsActive(view, activeDeclarations.ruleIndex, { 1135 [name]: value, 1136 }); 1137 } 1138 } 1139 } 1140 1141 if (test.inactiveDeclarations) { 1142 info("Checking that declarations are unused and have a warning."); 1143 1144 for (const inactiveDeclaration of test.inactiveDeclarations) { 1145 await checkDeclarationIsInactive( 1146 view, 1147 inactiveDeclaration.ruleIndex, 1148 inactiveDeclaration.declaration 1149 ); 1150 } 1151 } 1152 } 1153 } 1154 1155 /** 1156 * Return the checkbox element from the Rules view corresponding 1157 * to the given pseudo-class. 1158 * 1159 * @param {object} view 1160 * Instance of RuleView. 1161 * @param {string} pseudo 1162 * Pseudo-class, like :hover, :active, :focus, etc. 1163 * @return {HTMLElement} 1164 */ 1165 function getPseudoClassCheckbox(view, pseudo) { 1166 return view.pseudoClassCheckboxes.filter( 1167 checkbox => checkbox.value === pseudo 1168 )[0]; 1169 } 1170 1171 /** 1172 * Check that the CSS variable output has the expected class name and data attribute. 1173 * 1174 * @param {RulesView} view 1175 * The RulesView instance. 1176 * @param {string} selector 1177 * Selector name for a rule. (e.g. "div", "div::before" and ".sample" etc); 1178 * @param {string} propertyName 1179 * Property name (e.g. "color" and "padding-top" etc); 1180 * @param {string} expectedClassName 1181 * The class name the variable should have. 1182 * @param {string} expectedDatasetValue 1183 * The variable data attribute value. 1184 */ 1185 function checkCSSVariableOutput( 1186 view, 1187 selector, 1188 propertyName, 1189 expectedClassName, 1190 expectedDatasetValue 1191 ) { 1192 const target = getRuleViewProperty( 1193 view, 1194 selector, 1195 propertyName 1196 ).valueSpan.querySelector(`.${expectedClassName}`); 1197 1198 ok(target, "The target element should exist"); 1199 is(target.dataset.variable, expectedDatasetValue); 1200 } 1201 1202 /** 1203 * Return specific rule ancestor data element (i.e. the one containing @layer / @media 1204 * information) from the Rules view 1205 * 1206 * @param {RulesView} view 1207 * The RulesView instance. 1208 * @param {number} ruleIndex 1209 * @returns {HTMLElement} 1210 */ 1211 function getRuleViewAncestorRulesDataElementByIndex(view, ruleIndex) { 1212 return view.styleDocument 1213 .querySelectorAll(`.ruleview-rule`) 1214 [ruleIndex]?.querySelector(`.ruleview-rule-ancestor-data`); 1215 } 1216 1217 /** 1218 * Return specific rule ancestor data text from the Rules view. 1219 * Will return something like "@layer topLayer\n@media screen\n@layer". 1220 * 1221 * @param {RulesView} view 1222 * The RulesView instance. 1223 * @param {number} ruleIndex 1224 * @returns {string} 1225 */ 1226 function getRuleViewAncestorRulesDataTextByIndex(view, ruleIndex) { 1227 return getRuleViewAncestorRulesDataElementByIndex(view, ruleIndex)?.innerText; 1228 } 1229 1230 /** 1231 * Runs a sequence of tests against the provided property editor. 1232 * 1233 * @param {TextPropertyEditor} propertyEditor 1234 * The TextPropertyEditor instance to test. 1235 * @param {RuleView} view 1236 * The RuleView which owns the propertyEditor. 1237 * @param {Array<object>} test 1238 * The array of tests to run. 1239 */ 1240 async function runIncrementTest(propertyEditor, view, tests) { 1241 propertyEditor.valueSpan.scrollIntoView(); 1242 const editor = await focusEditableField(view, propertyEditor.valueSpan); 1243 1244 for (const testIndex in tests) { 1245 await testIncrement(editor, view, tests[testIndex], testIndex); 1246 } 1247 1248 // Blur the field to put back the UI in its initial state (and avoid pending 1249 // requests when the test ends). 1250 const onRuleViewChanged = view.once("ruleview-changed"); 1251 EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); 1252 view.debounce.flush(); 1253 await onRuleViewChanged; 1254 } 1255 1256 /** 1257 * Individual test runner for increment tests used via runIncrementTest in 1258 * browser_rules_edit-property-increments.js and similar tests. 1259 * 1260 * Will attempt to increment the value of the provided inplace editor based on 1261 * the test options provided. 1262 * 1263 * @param {InplaceEditor} editor 1264 * The InplaceEditor instance to test. 1265 * @param {RuleView} view 1266 * The RuleView which owns the editor. 1267 * @param {object} test 1268 * @param {boolean=} test.alt 1269 * Whether alt should be depressed. 1270 * @param {boolean=} test.ctrl 1271 * Whether ctrl should be depressed. 1272 * @param {number=} test.deltaX 1273 * Only relevant if test.wheel=true, value of the wheel delta on the horizontal axis. 1274 * @param {number=} test.deltaY 1275 * Only relevant if test.wheel=true, value of the wheel delta on the vertical axis. 1276 * @param {boolean=} test.down 1277 * For key increment tests, whether this should simulate pressing the down 1278 * arrow, or the up arrow. down, pagedown and pageup are mutually exclusive. 1279 * @param {string} test.end 1280 * The expected value at the end of the test. 1281 * @param {boolean=} test.pagedown 1282 * For key increment tests, whether this should simulate pressing the 1283 * pagedown key. down, pagedown and pageup are mutually exclusive. 1284 * @param {boolean=} test.pageup 1285 * For key increment tests, whether this should simulate pressing the 1286 * pageup key. down, pagedown and pageup are mutually exclusive. 1287 * @param {boolean=} test.selectAll 1288 * Whether all the input text should be selected. You can also specify a 1289 * range with test.selection. 1290 * @param {Array<number>=} test.selection 1291 * An array of two numbers which corresponds to the initial selection range. 1292 * @param {boolean=} test.shift 1293 * Whether shift should be depressed. 1294 * @param {string} test.start 1295 * The input value at the beginning of the test. 1296 * @param {boolean=} test.wheel 1297 * True if the test should use wheel events to increment the value. 1298 * @param {number} testIndex 1299 * The test index, used for logging. 1300 */ 1301 async function testIncrement(editor, view, test, testIndex) { 1302 editor.input.value = test.start; 1303 const input = editor.input; 1304 1305 if (test.selectAll) { 1306 input.select(); 1307 } else if (test.selection) { 1308 input.setSelectionRange(test.selection[0], test.selection[1]); 1309 } 1310 1311 is(input.value, test.start, "Value initialized at " + test.start); 1312 1313 const onRuleViewChanged = view.once("ruleview-changed"); 1314 1315 let smallIncrementKey = { ctrlKey: test.ctrl }; 1316 if (AppConstants.platform === "macosx") { 1317 smallIncrementKey = { altKey: test.alt }; 1318 } 1319 1320 const options = { 1321 shiftKey: test.shift, 1322 ...smallIncrementKey, 1323 }; 1324 1325 if (test.wheel) { 1326 // If test.wheel is true, we should increment the value using the wheel. 1327 const onWheel = once(input, "wheel"); 1328 input.dispatchEvent( 1329 new view.styleWindow.WheelEvent("wheel", { 1330 deltaX: test.deltaX, 1331 deltaY: test.deltaY, 1332 deltaMode: 0, 1333 ...options, 1334 }) 1335 ); 1336 await onWheel; 1337 } else { 1338 let key; 1339 key = test.down ? "VK_DOWN" : "VK_UP"; 1340 if (test.pageDown) { 1341 key = "VK_PAGE_DOWN"; 1342 } else if (test.pageUp) { 1343 key = "VK_PAGE_UP"; 1344 } 1345 const onKeyUp = once(input, "keyup"); 1346 EventUtils.synthesizeKey(key, options, view.styleWindow); 1347 1348 await onKeyUp; 1349 } 1350 1351 // Only expect a change if the value actually changed! 1352 if (test.start !== test.end) { 1353 view.debounce.flush(); 1354 await onRuleViewChanged; 1355 } 1356 1357 is(input.value, test.end, `[Test ${testIndex}] Value changed to ${test.end}`); 1358 } 1359 1360 function getSmallIncrementKey() { 1361 if (AppConstants.platform === "macosx") { 1362 return { alt: true }; 1363 } 1364 return { ctrl: true }; 1365 } 1366 1367 /** 1368 * Check that the rule view has the expected content 1369 * 1370 * @param {RuleView} view 1371 * @param {object[]} expectedElements 1372 * @param {string} expectedElements[].selector - The expected selector of the rule. Wrap 1373 * unmatched selector with `~~` characters (e.g. "div, ~~unmatched~~") 1374 * @param {boolean} expectedElements[].selectorEditable - Whether or not the selector can 1375 * be edited. Defaults to true. 1376 * @param {boolean} expectedElements[].hasSelectorHighlighterButton - Whether or not a 1377 * selector highlighter button is visible. Defaults to true. 1378 * @param {string[]|null} expectedElements[].ancestorRulesData - An array of the parent 1379 * selectors of the rule, with their indentations and the opening brace. 1380 * e.g. for the following rule `html { body { span {} } }`, for the `span` rule, 1381 * you should pass: 1382 * [ 1383 * `html {`, 1384 * ` & body {`, 1385 * ] 1386 * Pass `null` if the rule doesn't have a parent rule. 1387 * @param {boolean|undefined} expectedElements[].inherited - Is the rule an inherited one. 1388 * Defaults to false. 1389 * @param {object[]} expectedElements[].declarations - The expected declarations of the rule. 1390 * @param {object[]} expectedElements[].declarations[].name - The name of the declaration. 1391 * @param {object[]} expectedElements[].declarations[].value - The value of the declaration. 1392 * @param {boolean|undefined} expectedElements[].declarations[].overridden - Is the declaration 1393 * overridden by another the declaration. Defaults to false. 1394 * @param {boolean|undefined} expectedElements[].declarations[].valid - Is the declaration valid. 1395 * Defaults to true. 1396 * @param {boolean|undefined} expectedElements[].declarations[].dirty - Is the declaration dirty, 1397 * i.e. was it added/modified by the user (should have a left green border). 1398 * Defaults to false 1399 * @param {boolean|undefined} expectedElements[].declarations[].highlighted - Is the declaration 1400 * highlighted by a search. 1401 * @param {boolean|undefined} expectedElements[].declarations[].inactiveCSS - Is the declaration 1402 * inactive. 1403 * @param {string} expectedElements[].header - If we're expecting a header (Inherited from, 1404 * Pseudo-elements, …), the text of said header. 1405 */ 1406 function checkRuleViewContent(view, expectedElements) { 1407 const elementsInView = _getRuleViewElements(view); 1408 is( 1409 elementsInView.length, 1410 expectedElements.length, 1411 "All expected elements are displayed" 1412 ); 1413 1414 expectedElements.forEach((expectedElement, i) => { 1415 info(`Checking element #${i}: ${expectedElement.selector}`); 1416 1417 const elementInView = elementsInView[i]; 1418 1419 if (expectedElement.header) { 1420 is( 1421 elementInView.getAttribute("role"), 1422 "heading", 1423 `Element #${i} is a header` 1424 ); 1425 is( 1426 elementInView.textContent, 1427 expectedElement.header, 1428 `Expected header text for element #${i}` 1429 ); 1430 return; 1431 } 1432 1433 const selector = [ 1434 ...elementInView.querySelectorAll( 1435 // Get the selector parts (.ruleview-selector) 1436 ".ruleview-selectors-container .ruleview-selector," + 1437 // as well as the `element` "fake" selector 1438 ".ruleview-selectors-container.alternative-selector," + 1439 // and read-only selectors 1440 `.ruleview-selectors-container.uneditable-selector` 1441 ), 1442 ] 1443 .map(selectorEl => { 1444 let selectorPart = selectorEl.textContent; 1445 if (selectorEl.classList.contains("unmatched")) { 1446 selectorPart = `~~${selectorPart}~~`; 1447 } 1448 return selectorPart; 1449 }) 1450 .join(", "); 1451 is( 1452 selector, 1453 expectedElement.selector, 1454 `Expected selector for element #${i}` 1455 ); 1456 is( 1457 elementInView.querySelector( 1458 `.ruleview-selectors-container:not(.uneditable-selector)` 1459 ) !== null, 1460 expectedElement.selectorEditable ?? true, 1461 `Selector for element #${i} (${selector}) ${(expectedElement.selectorEditable ?? true) ? "is" : "isn't"} editable` 1462 ); 1463 is( 1464 elementInView.querySelector(`.ruleview-selectorhighlighter`) !== null, 1465 expectedElement.hasSelectorHighlighterButton ?? true, 1466 `Element #${i} (${selector}) ${(expectedElement.hasSelectorHighlighterButton ?? true) ? "has" : "does not have"} a selector highlighter button` 1467 ); 1468 1469 const ancestorData = elementInView.querySelector( 1470 `.ruleview-rule-ancestor-data` 1471 ); 1472 if (expectedElement.ancestorRulesData == null) { 1473 is( 1474 ancestorData, 1475 null, 1476 `No ancestor rules data displayed for ${selector}` 1477 ); 1478 } else { 1479 is( 1480 ancestorData.innerText, 1481 expectedElement.ancestorRulesData.join("\n"), 1482 `Expected ancestor rules data displayed for ${selector}` 1483 ); 1484 } 1485 1486 const isInherited = elementInView.matches(".ruleview-rule-inherited"); 1487 is( 1488 isInherited, 1489 expectedElement.inherited ?? false, 1490 `Element #${i} ("${selector}") is ${expectedElement.inherited ? "inherited" : "not inherited"}` 1491 ); 1492 1493 const ruleViewPropertyElements = 1494 elementInView.querySelectorAll(".ruleview-property"); 1495 is( 1496 ruleViewPropertyElements.length, 1497 expectedElement.declarations.length, 1498 `Got the expected number of declarations for expected element #${i} (${selector})` 1499 ); 1500 ruleViewPropertyElements.forEach((ruleViewPropertyElement, j) => { 1501 const [propName, propValue] = Array.from( 1502 ruleViewPropertyElement.querySelectorAll( 1503 ".ruleview-propertyname, .ruleview-propertyvalue" 1504 ) 1505 ); 1506 1507 const expectedDeclaration = expectedElement.declarations[j]; 1508 is( 1509 propName.innerText, 1510 expectedDeclaration?.name, 1511 "Got expected property name" 1512 ); 1513 if (propName.innerText !== expectedDeclaration?.name) { 1514 // We don't have the expected property name, don't run the other assertions to 1515 // avoid spamming the output 1516 return; 1517 } 1518 1519 is( 1520 propValue.innerText, 1521 expectedDeclaration?.value, 1522 "Got expected property value" 1523 ); 1524 is( 1525 ruleViewPropertyElement.classList.contains("ruleview-overridden"), 1526 !!expectedDeclaration?.overridden, 1527 `Element #${i} ("${selector}") declaration #${j} ("${propName.innerText}: ${propValue.innerText}") is ${expectedDeclaration?.overridden ? "overridden" : "not overridden"} ` 1528 ); 1529 is( 1530 ruleViewPropertyElement.classList.contains("inactive-css"), 1531 !!expectedDeclaration?.inactiveCSS, 1532 `Element #${i} ("${selector}") declaration #${j} ("${propName.innerText}: ${propValue.innerText}") is ${expectedDeclaration?.inactiveCSS ? "inactive" : "not inactive"} ` 1533 ); 1534 const isWarningIconDisplayed = !!ruleViewPropertyElement.querySelector( 1535 ".ruleview-warning:not([hidden])" 1536 ); 1537 const expectedValid = expectedDeclaration?.valid ?? true; 1538 is( 1539 !isWarningIconDisplayed, 1540 expectedValid, 1541 `Element #${i} ("${selector}") declaration #${j} ("${propName.innerText}: ${propValue.innerText}") is ${expectedValid ? "valid" : "invalid"}` 1542 ); 1543 is( 1544 !!ruleViewPropertyElement.hasAttribute("dirty"), 1545 !!expectedDeclaration?.dirty, 1546 `Element #${i} ("${selector}") declaration #${j} ("${propName.innerText}: ${propValue.innerText}") is ${expectedDeclaration?.dirty ? "dirty" : "not dirty"}` 1547 ); 1548 is( 1549 ruleViewPropertyElement.querySelector(".ruleview-highlight") !== null, 1550 !!expectedDeclaration?.highlighted, 1551 `Element #${i} ("${selector}") declaration #${j} ("${propName.innerText}: ${propValue.innerText}") is ${expectedDeclaration?.highlighted ? "highlighted" : "not highlighted"} ` 1552 ); 1553 }); 1554 }); 1555 } 1556 1557 /** 1558 * Get the rule view elements for checkRuleViewContent 1559 * 1560 * @param {RuleView} view 1561 * @returns {Element[]} 1562 */ 1563 function _getRuleViewElements(view) { 1564 const elementsInView = []; 1565 for (const el of view.element.children) { 1566 if (el.classList.contains("registered-properties")) { 1567 // We don't check @property content for now 1568 continue; 1569 } 1570 // Gather all the children of expandable containers (e.g. Pseudo-element, @keyframe, …) 1571 if (el.classList.contains("ruleview-expandable-container")) { 1572 elementsInView.push(...el.children); 1573 } else { 1574 elementsInView.push(el); 1575 } 1576 } 1577 return elementsInView; 1578 } 1579 1580 function getUnusedVariableButton(view, elementIndexInView) { 1581 return view.element.children[elementIndexInView].querySelector( 1582 ".ruleview-show-unused-custom-css-properties" 1583 ); 1584 }