tor-browser

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

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 }