browser_rules_pseudo-element_01.js (16650B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 // Test that pseudoelements are displayed correctly in the rule view 7 8 const TEST_URI = URL_ROOT + "doc_pseudoelement.html?#:~:text=fox"; 9 const PSEUDO_PREF = "devtools.inspector.show_pseudo_elements"; 10 11 add_task(async function () { 12 await pushPref(PSEUDO_PREF, true); 13 await pushPref("dom.customHighlightAPI.enabled", true); 14 await pushPref("dom.text_fragments.enabled", true); 15 await pushPref("layout.css.modern-range-pseudos.enabled", true); 16 await pushPref("full-screen-api.transition-duration.enter", "0 0"); 17 await pushPref("full-screen-api.transition-duration.leave", "0 0"); 18 19 await addTab(TEST_URI); 20 const { inspector, view } = await openRuleView(); 21 22 await testTopLeft(inspector, view); 23 await testTopRight(inspector, view); 24 await testBottomRight(inspector, view); 25 await testBottomLeft(inspector, view); 26 await testParagraph(inspector, view); 27 await testBody(inspector, view); 28 await testListAfterElement(inspector, view); 29 await testListItem(inspector, view); 30 await testCustomHighlight(inspector, view); 31 await testSlider(inspector, view); 32 await testUrlFragmentTextDirective(inspector, view); 33 await testDetailsContent(inspector, view); 34 // keep this one last as it makes the browser go fullscreen and seem to impact other tests 35 await testBackdrop(inspector, view); 36 }); 37 38 async function testTopLeft(inspector, view) { 39 const id = "#topleft"; 40 const rules = await assertPseudoElementRulesNumbersForSelector( 41 id, 42 inspector, 43 view, 44 { 45 elementRules: 4, 46 firstLineRules: 2, 47 firstLetterRules: 1, 48 selectionRules: 1, 49 markerRules: 0, 50 afterRules: 1, 51 beforeRules: 2, 52 } 53 ); 54 55 const gutters = assertGutters(view); 56 57 info("Make sure that clicking on the twisty hides pseudo elements"); 58 const expander = gutters[0].querySelector(".ruleview-expander"); 59 ok(!view.element.children[1].hidden, "Pseudo Elements are expanded"); 60 61 expander.click(); 62 ok( 63 view.element.children[1].hidden, 64 "Pseudo Elements are collapsed by twisty" 65 ); 66 67 expander.click(); 68 ok(!view.element.children[1].hidden, "Pseudo Elements are expanded again"); 69 70 info( 71 "Make sure that dblclicking on the header container also toggles " + 72 "the pseudo elements" 73 ); 74 EventUtils.synthesizeMouseAtCenter( 75 gutters[0], 76 { clickCount: 2 }, 77 view.styleWindow 78 ); 79 ok( 80 view.element.children[1].hidden, 81 "Pseudo Elements are collapsed by dblclicking" 82 ); 83 84 const elementRuleView = getRuleViewRuleEditor(view, 3); 85 86 const elementFirstLineRule = rules.firstLineRules[0]; 87 const elementFirstLineRuleView = [ 88 ...view.element.children[1].children, 89 ].filter(e => { 90 return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule; 91 })[0]._ruleEditor; 92 93 is( 94 convertTextPropsToString(elementFirstLineRule.textProps), 95 "color: orange", 96 "TopLeft firstLine properties are correct" 97 ); 98 99 let onAdded = view.once("ruleview-changed"); 100 let firstProp = elementFirstLineRuleView.addProperty( 101 "background-color", 102 "rgb(0, 255, 0)", 103 "", 104 true 105 ); 106 await onAdded; 107 108 onAdded = view.once("ruleview-changed"); 109 const secondProp = elementFirstLineRuleView.addProperty( 110 "font-style", 111 "italic", 112 "", 113 true 114 ); 115 await onAdded; 116 117 is( 118 firstProp, 119 elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 2], 120 "First added property is on back of array" 121 ); 122 is( 123 secondProp, 124 elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 1], 125 "Second added property is on back of array" 126 ); 127 128 is( 129 await getComputedStyleProperty(id, ":first-line", "background-color"), 130 "rgb(0, 255, 0)", 131 "Added property should have been used." 132 ); 133 is( 134 await getComputedStyleProperty(id, ":first-line", "font-style"), 135 "italic", 136 "Added property should have been used." 137 ); 138 is( 139 await getComputedStyleProperty(id, null, "text-decoration-line"), 140 "none", 141 "Added property should not apply to element" 142 ); 143 144 await togglePropStatus(view, firstProp); 145 146 is( 147 await getComputedStyleProperty(id, ":first-line", "background-color"), 148 "rgb(255, 0, 0)", 149 "Disabled property should now have been used." 150 ); 151 is( 152 await getComputedStyleProperty(id, null, "background-color"), 153 "rgb(221, 221, 221)", 154 "Added property should not apply to element" 155 ); 156 157 await togglePropStatus(view, firstProp); 158 159 is( 160 await getComputedStyleProperty(id, ":first-line", "background-color"), 161 "rgb(0, 255, 0)", 162 "Added property should have been used." 163 ); 164 is( 165 await getComputedStyleProperty(id, null, "text-decoration-line"), 166 "none", 167 "Added property should not apply to element" 168 ); 169 170 onAdded = view.once("ruleview-changed"); 171 firstProp = elementRuleView.addProperty( 172 "background-color", 173 "rgb(0, 0, 255)", 174 "", 175 true 176 ); 177 await onAdded; 178 179 is( 180 await getComputedStyleProperty(id, null, "background-color"), 181 "rgb(0, 0, 255)", 182 "Added property should have been used." 183 ); 184 is( 185 await getComputedStyleProperty(id, ":first-line", "background-color"), 186 "rgb(0, 255, 0)", 187 "Added prop does not apply to pseudo" 188 ); 189 } 190 191 async function testTopRight(inspector, view) { 192 await assertPseudoElementRulesNumbersForSelector( 193 "#topright", 194 inspector, 195 view, 196 { 197 elementRules: 4, 198 firstLineRules: 1, 199 firstLetterRules: 1, 200 selectionRules: 0, 201 markerRules: 0, 202 beforeRules: 2, 203 afterRules: 1, 204 } 205 ); 206 207 const gutters = assertGutters(view); 208 209 const expander = gutters[0].querySelector(".ruleview-expander"); 210 ok( 211 !view.element.firstChild.classList.contains("show-expandable-container"), 212 "Pseudo Elements remain collapsed after switching element" 213 ); 214 215 expander.scrollIntoView(); 216 expander.click(); 217 ok( 218 !view.element.children[1].hidden, 219 "Pseudo Elements are shown again after clicking twisty" 220 ); 221 } 222 223 async function testBottomRight(inspector, view) { 224 await assertPseudoElementRulesNumbersForSelector( 225 "#bottomright", 226 inspector, 227 view, 228 { 229 elementRules: 4, 230 firstLineRules: 1, 231 firstLetterRules: 1, 232 selectionRules: 0, 233 markerRules: 0, 234 beforeRules: 3, 235 afterRules: 1, 236 } 237 ); 238 } 239 240 async function testBottomLeft(inspector, view) { 241 await assertPseudoElementRulesNumbersForSelector( 242 "#bottomleft", 243 inspector, 244 view, 245 { 246 elementRules: 4, 247 firstLineRules: 1, 248 firstLetterRules: 1, 249 selectionRules: 0, 250 markerRules: 0, 251 beforeRules: 2, 252 afterRules: 1, 253 } 254 ); 255 } 256 257 async function testParagraph(inspector, view) { 258 const rules = await assertPseudoElementRulesNumbersForSelector( 259 "#bottomleft p", 260 inspector, 261 view, 262 { 263 elementRules: 3, 264 firstLineRules: 1, 265 firstLetterRules: 1, 266 selectionRules: 2, 267 markerRules: 0, 268 beforeRules: 0, 269 afterRules: 0, 270 } 271 ); 272 273 assertGutters(view); 274 275 const elementFirstLineRule = rules.firstLineRules[0]; 276 is( 277 convertTextPropsToString(elementFirstLineRule.textProps), 278 "background: blue", 279 "Paragraph first-line properties are correct" 280 ); 281 282 const elementFirstLetterRule = rules.firstLetterRules[0]; 283 is( 284 convertTextPropsToString(elementFirstLetterRule.textProps), 285 "color: red; font-size: 130%", 286 "Paragraph first-letter properties are correct" 287 ); 288 289 const elementSelectionRule = rules.selectionRules[0]; 290 is( 291 convertTextPropsToString(elementSelectionRule.textProps), 292 "color: white; background: black", 293 "Paragraph first-letter properties are correct" 294 ); 295 } 296 297 async function testBody(inspector, view) { 298 await selectNode("body", inspector); 299 300 const gutters = getGutters(view); 301 is(gutters.length, 0, "There are no gutter headings"); 302 } 303 304 async function testListAfterElement(inspector, view) { 305 // Test that ::after::marker is displayed in the pseudo element section when 306 // selecting the #list::after node. 307 const listNode = await getNodeFront("#list", inspector); 308 const listChildren = await inspector.markup.walker.children(listNode); 309 const listAfterNode = listChildren.nodes.at(-1); 310 is( 311 listAfterNode.tagName, 312 "_moz_generated_content_after", 313 "tag name is correct for #list::after" 314 ); 315 await selectNode(listAfterNode, inspector); 316 317 await assertPseudoElementRulesNumbers(view, "#list::after", { 318 elementRules: 3, 319 markerRules: 1, 320 }); 321 322 Assert.deepEqual( 323 getGutters(view).map(gutter => gutter.textContent), 324 [ 325 "Pseudo-elements", 326 "This Element", 327 "Inherited from ol#list", 328 "Inherited from body", 329 ], 330 "Got expected gutter headings when selecting #list::after" 331 ); 332 } 333 334 async function testListItem(inspector, view) { 335 await assertPseudoElementRulesNumbersForSelector( 336 "#list-item", 337 inspector, 338 view, 339 { 340 elementRules: 4, 341 firstLineRules: 1, 342 firstLetterRules: 1, 343 selectionRules: 0, 344 markerRules: 1, 345 beforeRules: 1, 346 afterRules: 1, 347 } 348 ); 349 350 assertGutters(view); 351 } 352 353 async function testBackdrop(inspector, view) { 354 info("Test ::backdrop for dialog element"); 355 await assertPseudoElementRulesNumbersForSelector("dialog", inspector, view, { 356 elementRules: 3, 357 backdropRules: 1, 358 }); 359 360 info("Test ::backdrop for popover element"); 361 await assertPseudoElementRulesNumbersForSelector( 362 "#in-dialog[popover]", 363 inspector, 364 view, 365 { 366 elementRules: 3, 367 backdropRules: 1, 368 } 369 ); 370 371 assertGutters(view); 372 373 info("Test ::backdrop rules are displayed when elements is fullscreen"); 374 375 // Wait for the document being activated, so that 376 // fullscreen request won't be denied. 377 const onTabFocused = SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { 378 return ContentTaskUtils.waitForCondition( 379 () => content.browsingContext.isActive && content.document.hasFocus(), 380 "document is active" 381 ); 382 }); 383 gBrowser.selectedBrowser.focus(); 384 await onTabFocused; 385 386 info("Request fullscreen"); 387 // Entering fullscreen is triggering an update, wait for it so it doesn't impact 388 // the rest of the test 389 let onInspectorUpdated = view.once("ruleview-refreshed"); 390 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { 391 const canvas = content.document.querySelector("canvas"); 392 canvas.requestFullscreen(); 393 394 await ContentTaskUtils.waitForCondition( 395 () => content.document.fullscreenElement === canvas, 396 "canvas is fullscreen" 397 ); 398 }); 399 await onInspectorUpdated; 400 401 await assertPseudoElementRulesNumbersForSelector("canvas", inspector, view, { 402 elementRules: 3, 403 backdropRules: 1, 404 }); 405 406 assertGutters(view); 407 408 // Exiting fullscreen is triggering an update, wait for it so it doesn't impact 409 // the rest of the test 410 onInspectorUpdated = view.once("ruleview-refreshed"); 411 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { 412 content.document.exitFullscreen(); 413 await ContentTaskUtils.waitForCondition( 414 () => content.document.fullscreenElement === null, 415 "canvas is no longer fullscreen" 416 ); 417 }); 418 await onInspectorUpdated; 419 420 info( 421 "Test ::backdrop rules are not displayed when elements are not fullscreen" 422 ); 423 await assertPseudoElementRulesNumbersForSelector("canvas", inspector, view, { 424 elementRules: 3, 425 backdropRules: 0, 426 }); 427 } 428 429 async function testCustomHighlight(inspector, view) { 430 const { highlightRules } = await assertPseudoElementRulesNumbersForSelector( 431 ".highlights-container", 432 inspector, 433 view, 434 { 435 elementRules: 4, 436 highlightRules: 3, 437 } 438 ); 439 440 is( 441 highlightRules[0].pseudoElement, 442 "::highlight(search)", 443 "First highlight rule is for the search highlight" 444 ); 445 is( 446 highlightRules[1].pseudoElement, 447 "::highlight(search)", 448 "Second highlight rule is also for the search highlight" 449 ); 450 is( 451 highlightRules[2].pseudoElement, 452 "::highlight(filter)", 453 "Third highlight rule is for the filter highlight" 454 ); 455 is(highlightRules.length, 3, "Got all 3 active rules, but not unused one"); 456 457 // Check that properties are marked as overridden only when they're on the same Highlight 458 is( 459 convertTextPropsToString(highlightRules[0].textProps), 460 `color: white`, 461 "Got expected properties for first search highlight" 462 ); 463 is( 464 convertTextPropsToString(highlightRules[1].textProps), 465 `background-color: tomato; ~~color: gold~~`, 466 "Got expected properties for second search highlight, `color` is marked as overridden" 467 ); 468 is( 469 convertTextPropsToString(highlightRules[2].textProps), 470 `background-color: purple`, 471 "Got expected properties for filter highlight" 472 ); 473 474 assertGutters(view); 475 } 476 477 async function testSlider(inspector, view) { 478 await assertPseudoElementRulesNumbersForSelector( 479 "input[type=range].slider", 480 inspector, 481 view, 482 { 483 elementRules: 3, 484 sliderFillRules: 1, 485 sliderThumbRules: 1, 486 sliderTrackRules: 1, 487 } 488 ); 489 assertGutters(view); 490 491 info( 492 "Check that ::slider-* pseudo elements are not displayed for non-range inputs" 493 ); 494 await assertPseudoElementRulesNumbersForSelector( 495 "input[type=text].slider", 496 inspector, 497 view, 498 { 499 elementRules: 3, 500 sliderFillRules: 0, 501 sliderThumbRules: 0, 502 sliderTrackRules: 0, 503 } 504 ); 505 } 506 507 async function testUrlFragmentTextDirective(inspector, view) { 508 await assertPseudoElementRulesNumbersForSelector( 509 ".url-fragment-text-directives", 510 inspector, 511 view, 512 { 513 elementRules: 3, 514 targetTextRules: 1, 515 } 516 ); 517 assertGutters(view); 518 } 519 520 async function testDetailsContent(inspector, view) { 521 await assertPseudoElementRulesNumbersForSelector("details", inspector, view, { 522 // `element`, `*`, and inherited `body` 523 elementRules: 3, 524 detailsContentRules: 1, 525 }); 526 assertGutters(view); 527 } 528 529 function convertTextPropsToString(textProps) { 530 return textProps 531 .map( 532 t => 533 `${t.overridden ? "~~" : ""}${t.name}: ${t.value}${ 534 t.overridden ? "~~" : "" 535 }` 536 ) 537 .join("; "); 538 } 539 540 const PSEUDO_DICT = { 541 firstLineRules: "::first-line", 542 firstLetterRules: "::first-letter", 543 selectionRules: "::selection", 544 markerRules: "::marker", 545 beforeRules: "::before", 546 afterRules: "::after", 547 backdropRules: "::backdrop", 548 highlightRules: "::highlight", 549 sliderFillRules: "::slider-fill", 550 sliderThumbRules: "::slider-thumb", 551 sliderTrackRules: "::slider-track", 552 targetTextRules: "::target-text", 553 detailsContentRules: "::details-content", 554 }; 555 556 async function assertPseudoElementRulesNumbersForSelector( 557 selector, 558 inspector, 559 view, 560 ruleNbs 561 ) { 562 await selectNode(selector, inspector); 563 return assertPseudoElementRulesNumbers(view, selector, ruleNbs); 564 } 565 566 async function assertPseudoElementRulesNumbers( 567 view, 568 elementDescription, 569 ruleNbs 570 ) { 571 // Wait for the expected pseudo classes to be displayed 572 await waitFor(() => 573 Object.entries(ruleNbs).every(([key, nb]) => { 574 if (!PSEUDO_DICT[key]) { 575 return true; 576 } 577 return ( 578 Array.from( 579 view.element.querySelectorAll(".ruleview-selector-pseudo-class") 580 ).filter(el => el.textContent.startsWith(PSEUDO_DICT[key])).length === 581 nb 582 ); 583 }) 584 ); 585 586 const rules = { 587 elementRules: view._elementStyle.rules.filter(rule => !rule.pseudoElement), 588 ...Object.fromEntries( 589 Object.entries(PSEUDO_DICT).map(([key, pseudoElementSelector]) => [ 590 key, 591 view._elementStyle.rules.filter(rule => 592 rule.pseudoElement.startsWith(pseudoElementSelector) 593 ), 594 ]) 595 ), 596 }; 597 598 is( 599 rules.elementRules.length, 600 ruleNbs.elementRules || 0, 601 elementDescription + " has the correct number of non pseudo element rules" 602 ); 603 604 // Go through all the pseudo element types and assert that we have the expected number 605 for (const key in PSEUDO_DICT) { 606 is( 607 rules[key].length, 608 ruleNbs[key] || 0, 609 `${elementDescription} has the correct number of ${key} rules` 610 ); 611 } 612 613 return rules; 614 } 615 616 function getGutters(view) { 617 return Array.from(view.element.querySelectorAll(".ruleview-header")); 618 } 619 620 function assertGutters(view) { 621 const gutters = getGutters(view); 622 623 is(gutters.length, 3, "There are 3 gutter headings"); 624 is(gutters[0].textContent, "Pseudo-elements", "Gutter heading is correct"); 625 is(gutters[1].textContent, "This Element", "Gutter heading is correct"); 626 is( 627 gutters[2].textContent, 628 "Inherited from body", 629 "Gutter heading is correct" 630 ); 631 632 return gutters; 633 }