tor-browser

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

browser_rules_variables-jump-to-definition.js (14223B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 // Tests that the rule view custom properties (css variables) jump to definition works
      7 // as expected.
      8 
      9 const TEST_URI = `
     10  <style type='text/css'>
     11  @property --my-registered-color {
     12    syntax: "<color>";
     13    inherits: true;
     14    initial-value: gold;
     15  }
     16 
     17  :root {
     18    --my-color-1: tomato;
     19    --my-color-2: cyan;
     20    --my-color-3: green;
     21  }
     22 
     23  @starting-style {
     24    #testid {
     25      --my-color-1: hotpink;
     26    }
     27  }
     28 
     29  #testid {
     30    color: var(--my-color-1);
     31    background-color: var(--my-registered-color);
     32    border-width: 1px;
     33    outline-width: var(--undefined);
     34 
     35    @starting-style {
     36      outline-color: var(--my-color-1, var(--my-color-2));
     37    }
     38  }
     39 
     40  h1 {
     41    background-color: linear-gradient(var(--my-color-1), var(--my-color-2, var(--my-color-3)));
     42  }
     43 
     44  h2 {
     45    --my-color-2: chartreuse;
     46    color: var(--my-color-1);
     47  }
     48 
     49  h2::after {
     50    --my-color-1: azure;
     51    content: "-";
     52    color: var(--my-color-1);
     53  }
     54 
     55  h2::before {
     56    --my-color-1: blueviolet;
     57    content: "+";
     58    color: var(--my-color-1, var(--my-color-2, var(--my-color-3)));
     59  }
     60 
     61  </style>
     62  <h1 id='testid'>Styled Node</h1>
     63  <h2>sub</h2>
     64 `;
     65 
     66 add_task(async function () {
     67  await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
     68  const { inspector, view } = await openRuleView();
     69  await selectNode("h1#testid", inspector);
     70 
     71  info("Check that the correct rules are visible");
     72  is(
     73    view.styleDocument.querySelectorAll(`.ruleview-rule`).length,
     74    7,
     75    "Should have 7 rules."
     76  );
     77 
     78  let rule = getRuleViewRuleEditor(view, 2).rule;
     79  is(rule.selectorText, "#testid", "Second rule is #testid.");
     80 
     81  info(
     82    "Check that property not using variable don't have a jump to definition button"
     83  );
     84  let variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, {
     85    "border-width": "1px",
     86  });
     87  is(
     88    variableButtonEls.length,
     89    0,
     90    "border-width property does not have custom properties and no variable jump to definition is displayed."
     91  );
     92 
     93  info(
     94    "Check that property using unset variable don't have a jump to definition button"
     95  );
     96  variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, {
     97    "outline-width": "var(--undefined)",
     98  });
     99  is(
    100    variableButtonEls.length,
    101    0,
    102    "outline-width property has an undefined custom properties, so no variable jump to definition is displayed."
    103  );
    104 
    105  info(
    106    "Check that there's a jump to definition button for `color: var(--my-color-1)`"
    107  );
    108  variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, {
    109    color: "var(--my-color-1)",
    110  });
    111  is(
    112    variableButtonEls.length,
    113    1,
    114    "color property has custom property and variable jump to definition is displayed."
    115  );
    116 
    117  info("Click the --my-color-1 jump to definition button");
    118  await highlightProperty(view, variableButtonEls[0], "--my-color-1", "tomato");
    119 
    120  info(
    121    "Check that there's a jump to definition button for `background-color var(--my-registered-color)`"
    122  );
    123  variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, {
    124    "background-color": "var(--my-registered-color)",
    125  });
    126  is(
    127    variableButtonEls.length,
    128    1,
    129    "background-color property has a registered custom property and variable jump to definition is displayed."
    130  );
    131 
    132  info("Click the --my-registered-color jump to definition button");
    133  // Collapse the `@property` section to check that it gets expanded when clicking on the
    134  // jump to definition button.
    135  const registerPropertyToggle = view.styleDocument.querySelector(
    136    `[aria-controls="registered-properties-container"]`
    137  );
    138  registerPropertyToggle.click();
    139  is(
    140    registerPropertyToggle.ariaExpanded,
    141    "false",
    142    "@property section is collapsed"
    143  );
    144 
    145  const onHighlightProperty = view.once("element-highlighted");
    146  variableButtonEls[0].click();
    147  const highlightedElement = await onHighlightProperty;
    148  is(
    149    highlightedElement.querySelector(".ruleview-registered-property-name")
    150      .innerText,
    151    "--my-registered-color",
    152    "The expected element was highlighted"
    153  );
    154  is(
    155    registerPropertyToggle.ariaExpanded,
    156    "true",
    157    "@property section is expanded after clicking on the jump to definition button"
    158  );
    159 
    160  info(
    161    "Check that there are multiple jump to definition buttons when using multiple variables"
    162  );
    163  rule = getRuleViewRuleEditor(view, 4).rule;
    164  is(rule.selectorText, "h1", "Fifth rule is h1.");
    165  variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, {
    166    "background-color":
    167      "linear-gradient(var(--my-color-1), var(--my-color-2, var(--my-color-3)))",
    168  });
    169  Assert.deepEqual(
    170    [...variableButtonEls].map(el => el.dataset.variableName),
    171    ["--my-color-1", "--my-color-2", "--my-color-3"]
    172  );
    173 
    174  info(`Click the "--my-color-2" variable jump to definition button`);
    175  await highlightProperty(view, variableButtonEls[1], "--my-color-2", "cyan");
    176 
    177  info(`Click the fallback "--my-color-3" variable jump to definition button`);
    178  await highlightProperty(view, variableButtonEls[2], "--my-color-3", "green");
    179 
    180  info("Check that we can jump in @starting-style rule`");
    181  rule = getRuleViewRuleEditor(view, 1).rule;
    182  ok(rule.isInStartingStyle(), "Got expected starting style rule");
    183  variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, {
    184    "outline-color": "var(--my-color-1, var(--my-color-2))",
    185  });
    186 
    187  Assert.deepEqual(
    188    [...variableButtonEls].map(el => el.dataset.variableName),
    189    ["--my-color-1", "--my-color-2"]
    190  );
    191 
    192  info(
    193    "Click the --my-color-1 jump to definition button in @starting-style rule"
    194  );
    195  await highlightProperty(
    196    view,
    197    variableButtonEls[0],
    198    "--my-color-1",
    199    "hotpink"
    200  );
    201 
    202  info(
    203    "Click the --my-color-2 jump to definition button in @starting-style rule"
    204  );
    205  await highlightProperty(view, variableButtonEls[1], "--my-color-2", "cyan");
    206 
    207  info("Check that jump to definition works well with pseudo elements");
    208  await selectNode("h2", inspector);
    209 
    210  info("Expand the pseudo element section");
    211  const pseudoElementToggle = view.styleDocument.querySelector(
    212    `[aria-controls="pseudo-elements-container"]`
    213  );
    214  // sanity check
    215  is(
    216    pseudoElementToggle.ariaExpanded,
    217    "false",
    218    "pseudo element section is collapsed at first"
    219  );
    220  pseudoElementToggle.click();
    221  is(
    222    pseudoElementToggle.ariaExpanded,
    223    "true",
    224    "pseudo element section is now expanded"
    225  );
    226 
    227  rule = getRuleViewRuleEditor(view, 1, 0).rule;
    228  is(rule.selectorText, "h2::after", "First rule is h2::after");
    229 
    230  await highlightProperty(
    231    view,
    232    getJumpToDefinitionButtonForDeclaration(rule, {
    233      color: "var(--my-color-1)",
    234    })[0],
    235    "--my-color-1",
    236    "azure"
    237  );
    238 
    239  rule = getRuleViewRuleEditor(view, 1, 1).rule;
    240  is(rule.selectorText, "h2::before", "First rule is h2::before");
    241 
    242  variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, {
    243    color: "var(--my-color-1, var(--my-color-2, var(--my-color-3)))",
    244  });
    245  await highlightProperty(
    246    view,
    247    variableButtonEls[0],
    248    "--my-color-1",
    249    // definition in h2::before
    250    "blueviolet"
    251  );
    252  await highlightProperty(
    253    view,
    254    variableButtonEls[1],
    255    "--my-color-2",
    256    // definition in h2
    257    "chartreuse"
    258  );
    259  await highlightProperty(
    260    view,
    261    variableButtonEls[2],
    262    "--my-color-3",
    263    // definition in :root
    264    "green"
    265  );
    266 
    267  rule = getRuleViewRuleEditor(view, 4).rule;
    268  is(rule.selectorText, "h2", "Got expected h2 rule");
    269  await highlightProperty(
    270    view,
    271    getJumpToDefinitionButtonForDeclaration(rule, {
    272      color: "var(--my-color-1)",
    273    })[0],
    274    "--my-color-1",
    275    // definition in :root
    276    "tomato"
    277  );
    278 });
    279 
    280 add_task(async function checkClearSearch() {
    281  const fillerDeclarations = Array.from({ length: 50 }, (_, i) => ({
    282    name: `line-height`,
    283    value: i.toString(),
    284    overridden: i !== 49,
    285  }));
    286 
    287  await addTab(
    288    "data:text/html;charset=utf-8," +
    289      encodeURIComponent(`
    290        <style type='text/css'>
    291          :root {
    292            --my-color-1: tomato;
    293          }
    294 
    295          h1#title {
    296            ${
    297              // Add a lot of declaration so the --my-color-1
    298              // declaration would be out of view
    299              fillerDeclarations
    300                .map(({ name, value }) => `${name}: ${value};`)
    301                .join("")
    302            }
    303          }
    304 
    305          h1 {
    306            --my-unique-var: var(--my-color-1);
    307          }
    308        </style>
    309        <h1 id="title">Filter</h1>
    310  `)
    311  );
    312 
    313  const { inspector, view } = await openRuleView();
    314  await selectNode("h1", inspector);
    315 
    316  info("Check that search is cleared when clicking on the jump button");
    317  await setSearchFilter(view, "--my-unique-var");
    318 
    319  // check that the rule view is filtered as expected
    320  await checkRuleViewContent(view, [
    321    { selector: "element", selectorEditable: false, declarations: [] },
    322    {
    323      selector: "h1",
    324      declarations: [
    325        {
    326          name: "--my-unique-var",
    327          value: "var(--my-color-1)",
    328          highlighted: true,
    329        },
    330      ],
    331    },
    332  ]);
    333  const rule = getRuleViewRuleEditor(view, 1).rule;
    334  is(rule.selectorText, "h1", "Got expected rule");
    335  await highlightProperty(
    336    view,
    337    getJumpToDefinitionButtonForDeclaration(rule, {
    338      "--my-unique-var": "var(--my-color-1)",
    339    })[0],
    340    "--my-color-1",
    341    // definition in :root
    342    "tomato"
    343  );
    344  is(view.searchField.value, "", "Search input was cleared");
    345 
    346  // check that the rule view is no longer filtered
    347  await checkRuleViewContent(view, [
    348    { selector: "element", selectorEditable: false, declarations: [] },
    349    { selector: "h1#title", declarations: fillerDeclarations },
    350    {
    351      selector: "h1",
    352      declarations: [{ name: "--my-unique-var", value: "var(--my-color-1)" }],
    353    },
    354    {
    355      header: "Inherited from html",
    356    },
    357    {
    358      selector: ":root",
    359      inherited: true,
    360      declarations: [{ name: "--my-color-1", value: "tomato" }],
    361    },
    362  ]);
    363 });
    364 
    365 add_task(async function checkJumpToUnusedVariable() {
    366  await addTab(
    367    "data:text/html;charset=utf-8," +
    368      encodeURIComponent(`
    369        <style>
    370          :where(h3) {
    371            ${Array.from({ length: 15 }, (_, i) => `--unused-${i}: ${i};`).join("\n")}
    372          }
    373 
    374          h3 {
    375            --another-unused: var(--unused-5);
    376          }
    377        </style>
    378        <h3>for unused variables</h3>
    379  `)
    380  );
    381 
    382  const { inspector, view } = await openRuleView();
    383  await selectNode("h1", inspector);
    384 
    385  info("Check that jump to definition of unused variables do work");
    386  // If you have 2 rules, one with hidden custom properties, and the other one with
    387  // custom properties not being hidden because we're not entering the threshold
    388  // Make sure the clicking the Jump to definition button will reveal the hidden property
    389  await selectNode("h3", inspector);
    390 
    391  await checkRuleViewContent(view, [
    392    {
    393      selector: "element",
    394      selectorEditable: false,
    395      declarations: [],
    396    },
    397    {
    398      selector: "h3",
    399      declarations: [{ name: "--another-unused", value: "var(--unused-5)" }],
    400    },
    401    {
    402      // Contains the hidden variables
    403      selector: ":where(h3)",
    404      // All the variables are hidden
    405      declarations: [],
    406    },
    407  ]);
    408 
    409  is(
    410    getUnusedVariableButton(view, 2)?.textContent,
    411    "Show 15 unused custom CSS properties",
    412    "Show unused variables button has expected text"
    413  );
    414 
    415  const rule = getRuleViewRuleEditor(view, 1).rule;
    416  is(rule.selectorText, "h3", "Got expected rule");
    417 
    418  const variableButtonEls = getJumpToDefinitionButtonForDeclaration(rule, {
    419    "--another-unused": "var(--unused-5)",
    420  });
    421  is(variableButtonEls.length, 1, "There's one jump to variable button");
    422  await highlightProperty(view, variableButtonEls[0], "--unused-5", "5");
    423 
    424  await checkRuleViewContent(view, [
    425    {
    426      selector: "element",
    427      selectorEditable: false,
    428      declarations: [],
    429    },
    430    {
    431      selector: "h3",
    432      declarations: [{ name: "--another-unused", value: "var(--unused-5)" }],
    433    },
    434    {
    435      // Contains the hidden variables
    436      selector: ":where(h3)",
    437      declarations: [{ name: "--unused-5", value: "5" }],
    438    },
    439  ]);
    440 
    441  is(
    442    getUnusedVariableButton(view, 2)?.textContent,
    443    "Show 14 unused custom CSS properties",
    444    "Show unused variables button has expected text"
    445  );
    446 });
    447 
    448 function getJumpToDefinitionButtonForDeclaration(rule, declaration) {
    449  const [[name, value]] = Object.entries(declaration);
    450  const textProp = rule.textProps.find(prop => {
    451    return prop.name === name && prop.value === value;
    452  });
    453 
    454  if (!textProp) {
    455    throw Error(`Declaration ${name}:${value} not found on rule`);
    456  }
    457 
    458  return textProp.editor.element.querySelectorAll(".ruleview-variable-link");
    459 }
    460 
    461 /**
    462 * Click on the provided jump to definition button and wait for the element-highlighted
    463 * event to be emitted.
    464 *
    465 * @param {RuleView} view
    466 * @param {Element} jumpToDefinitionButton
    467 * @param {string} expectedPropertyName: The name of the property that should be highlighted
    468 * @param {string} expectedPropertyValue: The value of the property that should be highlighted
    469 */
    470 async function highlightProperty(
    471  view,
    472  jumpToDefinitionButton,
    473  expectedPropertyName,
    474  expectedPropertyValue
    475 ) {
    476  info(`Highlight "${expectedPropertyName}: ${expectedPropertyValue}"`);
    477  const onHighlightProperty = view.once("element-highlighted");
    478  jumpToDefinitionButton.click();
    479  const highlightedElement = await onHighlightProperty;
    480  is(
    481    highlightedElement.querySelector(".ruleview-propertyname").innerText,
    482    expectedPropertyName,
    483    "The expected element was highlighted"
    484  );
    485  is(
    486    highlightedElement.querySelector(".ruleview-propertyvalue").innerText,
    487    expectedPropertyValue,
    488    "The expected element was highlighted"
    489  );
    490 
    491  // check that the declaration we jumped to is into view
    492  ok(
    493    isInViewport(highlightedElement, view.styleWindow),
    494    `Highlighted element is in view`
    495  );
    496 }
    497 
    498 function isInViewport(element, win) {
    499  const { top, left, bottom, right } = element.getBoundingClientRect();
    500  return (
    501    top >= 0 &&
    502    bottom <= win.innerHeight &&
    503    left >= 0 &&
    504    right <= win.innerWidth
    505  );
    506 }