tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }