tor-browser

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

browser_styleeditor_at_rules_sidebar.js (12201B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 // https rather than chrome to improve coverage
      7 const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules.html";
      8 const SIDEBAR_PREF = "devtools.styleeditor.showAtRulesSidebar";
      9 
     10 const RESIZE_W = 300;
     11 const RESIZE_H = 450;
     12 const LABELS = [
     13  "not all",
     14  "all",
     15  "(max-width: 550px)",
     16  "(min-height: 300px) and (max-height: 320px)",
     17  "(max-width: 750px)",
     18  "",
     19  "print",
     20 ];
     21 const LINE_NOS = [1, 7, 19, 25, 31, 34, 39];
     22 const NEW_RULE = `
     23  @media (max-width: 750px) {
     24    div {
     25      color: blue;
     26      @layer {
     27        border-color: tomato;
     28      }
     29    }
     30 
     31    @media print {
     32      body {
     33        filter: grayscale(100%);
     34      }
     35    }
     36  }`;
     37 
     38 waitForExplicitFinish();
     39 
     40 add_task(async function () {
     41  // Enable @property rules
     42  await pushPref("layout.css.properties-and-values.enabled", true);
     43  // Enable anchor positioning
     44  await pushPref("layout.css.anchor-positioning.enabled", true);
     45  // Enable @custom-media
     46  await pushPref("layout.css.custom-media.enabled", true);
     47 
     48  const { ui } = await openStyleEditorForURL(TESTCASE_URI);
     49 
     50  is(ui.editors.length, 4, "correct number of editors");
     51 
     52  info("Test first plain css editor");
     53  const plainEditor = ui.editors[0];
     54  await openEditor(plainEditor);
     55  testPlainEditor(plainEditor);
     56 
     57  info("Test editor for inline sheet with at-rules");
     58  const inlineAtRulesEditor = ui.editors[3];
     59  await openEditor(inlineAtRulesEditor);
     60  await testInlineAtRulesEditor(ui, inlineAtRulesEditor);
     61 
     62  info("Test editor with @media rules");
     63  const mediaEditor = ui.editors[1];
     64  await openEditor(mediaEditor);
     65  await testMediaEditor(ui, mediaEditor);
     66 
     67  info("Test that sidebar hides when flipping pref");
     68  await testShowHide(ui, mediaEditor);
     69 
     70  info("Test adding a rule updates the list");
     71  await testMediaRuleAdded(ui, mediaEditor);
     72 
     73  info("Test resizing and seeing @media matching state change");
     74  const originalWidth = window.outerWidth;
     75  const originalHeight = window.outerHeight;
     76 
     77  const onMatchesChange = ui.once("at-rules-list-changed");
     78  window.resizeTo(RESIZE_W, RESIZE_H);
     79  await onMatchesChange;
     80 
     81  testMediaMatchChanged(mediaEditor);
     82 
     83  window.resizeTo(originalWidth, originalHeight);
     84 });
     85 
     86 function testPlainEditor(editor) {
     87  const sidebar = editor.details.querySelector(".stylesheet-sidebar");
     88  is(sidebar.hidden, true, "sidebar is hidden on editor without @media");
     89 }
     90 
     91 async function testInlineAtRulesEditor(ui, editor) {
     92  const sidebar = editor.details.querySelector(".stylesheet-sidebar");
     93  is(sidebar.hidden, false, "sidebar is showing on editor with @media");
     94 
     95  const entries = sidebar.querySelectorAll(".at-rule-label");
     96  is(entries.length, 14, "14 at-rules displayed in sidebar");
     97 
     98  await testRule({
     99    ui,
    100    editor,
    101    rule: entries[0],
    102    conditionText: "screen",
    103    matches: true,
    104    line: 2,
    105    type: "media",
    106  });
    107 
    108  await testRule({
    109    ui,
    110    editor,
    111    rule: entries[1],
    112    conditionText: "(display: flex)",
    113    line: 7,
    114    type: "support",
    115  });
    116 
    117  await testRule({
    118    ui,
    119    editor,
    120    rule: entries[2],
    121    conditionText: "(1px < height < 10000px)",
    122    matches: true,
    123    line: 8,
    124    type: "media",
    125  });
    126 
    127  await testRule({
    128    ui,
    129    editor,
    130    rule: entries[3],
    131    line: 16,
    132    type: "layer",
    133    layerName: "myLayer",
    134  });
    135 
    136  await testRule({
    137    ui,
    138    editor,
    139    rule: entries[4],
    140    conditionText: "(min-width: 1px)",
    141    line: 17,
    142    type: "container",
    143  });
    144 
    145  await testRule({
    146    ui,
    147    editor,
    148    rule: entries[5],
    149    conditionText: "selector(&)",
    150    line: 21,
    151    type: "support",
    152  });
    153 
    154  await testRule({
    155    ui,
    156    editor,
    157    rule: entries[6],
    158    line: 30,
    159    type: "property",
    160    propertyName: "--my-property",
    161  });
    162 
    163  await testRule({
    164    ui,
    165    editor,
    166    rule: entries[7],
    167    line: 36,
    168    type: "position-try",
    169    positionTryName: "--pt-custom-bottom",
    170  });
    171 
    172  await testRule({
    173    ui,
    174    editor,
    175    rule: entries[8],
    176    line: 42,
    177    type: "custom-media",
    178    customMediaName: "--mobile-breakpoint",
    179    customMediaQuery: [
    180      { text: "(width < 320px) and (height < 1420px)" },
    181      { text: ", " },
    182      { text: "not print" },
    183    ],
    184  });
    185 
    186  await testRule({
    187    ui,
    188    editor,
    189    rule: entries[9],
    190    line: 43,
    191    type: "custom-media",
    192    customMediaName: "--enabled",
    193    customMediaQuery: [{ text: "true" }],
    194  });
    195 
    196  await testRule({
    197    ui,
    198    editor,
    199    rule: entries[10],
    200    line: 44,
    201    type: "custom-media",
    202    customMediaName: "--disabled",
    203    customMediaQuery: [{ text: "false", matches: false }],
    204  });
    205 
    206  await testRule({
    207    ui,
    208    editor,
    209    rule: entries[11],
    210    line: 49,
    211    type: "media",
    212    conditionText: "(--mobile-breakpoint)",
    213    matches: false,
    214  });
    215 
    216  await testRule({
    217    ui,
    218    editor,
    219    rule: entries[12],
    220    line: 53,
    221    type: "media",
    222    conditionText: "(--enabled)",
    223    matches: false,
    224  });
    225 
    226  await testRule({
    227    ui,
    228    editor,
    229    rule: entries[13],
    230    line: 57,
    231    type: "media",
    232    conditionText: "(--disabled)",
    233    matches: false,
    234  });
    235 }
    236 
    237 async function testMediaEditor(ui, editor) {
    238  const sidebar = editor.details.querySelector(".stylesheet-sidebar");
    239  is(sidebar.hidden, false, "sidebar is showing on editor with @media");
    240 
    241  const entries = [...sidebar.querySelectorAll(".at-rule-label")];
    242  is(entries.length, 4, "four @media rules displayed in sidebar");
    243 
    244  await testRule({
    245    ui,
    246    editor,
    247    rule: entries[0],
    248    conditionText: LABELS[0],
    249    matches: false,
    250    line: LINE_NOS[0],
    251  });
    252  await testRule({
    253    ui,
    254    editor,
    255    rule: entries[1],
    256    conditionText: LABELS[1],
    257    matches: true,
    258    line: LINE_NOS[1],
    259  });
    260  await testRule({
    261    ui,
    262    editor,
    263    rule: entries[2],
    264    conditionText: LABELS[2],
    265    matches: false,
    266    line: LINE_NOS[2],
    267  });
    268  await testRule({
    269    ui,
    270    editor,
    271    rule: entries[3],
    272    conditionText: LABELS[3],
    273    matches: false,
    274    line: LINE_NOS[3],
    275  });
    276 }
    277 
    278 function testMediaMatchChanged(editor) {
    279  const sidebar = editor.details.querySelector(".stylesheet-sidebar");
    280 
    281  const cond = sidebar.querySelectorAll(".at-rule-condition")[2];
    282  is(
    283    cond.textContent,
    284    "(max-width: 550px)",
    285    "third rule condition text is correct"
    286  );
    287  ok(
    288    !cond.classList.contains("media-condition-unmatched"),
    289    "media rule is now matched after resizing"
    290  );
    291 }
    292 
    293 async function testShowHide(ui, editor) {
    294  let sidebarChange = ui.once("at-rules-list-changed");
    295  Services.prefs.setBoolPref(SIDEBAR_PREF, false);
    296  await sidebarChange;
    297 
    298  const sidebar = editor.details.querySelector(".stylesheet-sidebar");
    299  is(sidebar.hidden, true, "sidebar is hidden after flipping pref");
    300 
    301  sidebarChange = ui.once("at-rules-list-changed");
    302  Services.prefs.clearUserPref(SIDEBAR_PREF);
    303  await sidebarChange;
    304 
    305  is(sidebar.hidden, false, "sidebar is showing after flipping pref back");
    306 }
    307 
    308 async function testMediaRuleAdded(ui, editor) {
    309  await editor.getSourceEditor();
    310  const sidebar = editor.details.querySelector(".stylesheet-sidebar");
    311  is(
    312    sidebar.querySelectorAll(".at-rule-label").length,
    313    4,
    314    "4 @media rules after changing text"
    315  );
    316 
    317  let text = editor.sourceEditor.getText();
    318  text += NEW_RULE;
    319 
    320  const listChange = ui.once("at-rules-list-changed");
    321  editor.sourceEditor.setText(text);
    322  await listChange;
    323 
    324  const entries = [...sidebar.querySelectorAll(".at-rule-label")];
    325  is(entries.length, 7, "7 @media rules after changing text");
    326 
    327  await testRule({
    328    ui,
    329    editor,
    330    rule: entries[4],
    331    conditionText: LABELS[4],
    332    matches: false,
    333    line: LINE_NOS[4],
    334  });
    335 
    336  await testRule({
    337    ui,
    338    editor,
    339    rule: entries[5],
    340    type: "layer",
    341    conditionText: LABELS[5],
    342    line: LINE_NOS[5],
    343  });
    344 
    345  await testRule({
    346    ui,
    347    editor,
    348    rule: entries[6],
    349    conditionText: LABELS[6],
    350    matches: false,
    351    line: LINE_NOS[6],
    352  });
    353 }
    354 
    355 /**
    356 * Run assertion on given rule
    357 *
    358 * @param {object} options
    359 * @param {StyleEditorUI} options.ui
    360 * @param {StyleSheetEditor} options.editor: The editor the rule is displayed in
    361 * @param {Element} options.rule: The rule element in the media sidebar
    362 * @param {string} options.conditionText: at-rule condition text (for @media, @container, @support)
    363 * @param {boolean} options.matches: Whether or not the document matches the rule
    364 * @param {string} options.layerName: Optional name of the @layer
    365 * @param {string} options.positionTryName: Name of the @position-try if type is "position-try"
    366 * @param {string} options.propertyName: Name of the @property if type is "property"
    367 * @param {string} options.customMediaName: Name of the @custom-media if type is "custom-media"
    368 * @param {Array<object>} options.customMediaQuery: query parts of the @custom-media if type is "custom-media"
    369 * @param {string} options.customMediaQuery[].text: the query string of the part of the @custom-media
    370 *        if type is "custom-media"
    371 * @param {boolean} options.customMediaQuery[].matches: whether or not this part is style as matching,
    372 *        if type is "custom-media". Defaults to true.
    373 * @param {number} options.line: Line of the rule
    374 * @param {string} options.type: The type of the rule (container, layer, media, support, property ).
    375 *                               Defaults to "media".
    376 */
    377 async function testRule({
    378  ui,
    379  editor,
    380  rule,
    381  conditionText = "",
    382  matches,
    383  layerName,
    384  positionTryName,
    385  propertyName,
    386  customMediaName,
    387  customMediaQuery,
    388  line,
    389  type = "media",
    390 }) {
    391  const atTypeEl = rule.querySelector(".at-rule-type");
    392  let name;
    393  if (type === "layer") {
    394    name = layerName;
    395  } else if (type === "property") {
    396    name = propertyName;
    397  } else if (type === "position-try") {
    398    name = positionTryName;
    399  }
    400 
    401  if (type === "custom-media") {
    402    const atTypeChilNodes = Array.from(atTypeEl.childNodes);
    403    is(
    404      atTypeChilNodes.shift().textContent,
    405      `@custom-media\u00A0`,
    406      "label for @custom-media is correct"
    407    );
    408    is(
    409      atTypeChilNodes.shift().textContent,
    410      `${customMediaName} `,
    411      "name for @custom-media is correct"
    412    );
    413    is(
    414      atTypeChilNodes.length,
    415      customMediaQuery.length,
    416      `Got expected number of children of @custom-media (got ${JSON.stringify(atTypeChilNodes.map(n => n.textContent))})`
    417    );
    418    for (let i = 0; i < atTypeChilNodes.length; i++) {
    419      const node = atTypeChilNodes[i];
    420      is(
    421        node.textContent,
    422        customMediaQuery[i].text,
    423        `Got expected text for part #${i} of @custom-media`
    424      );
    425      if (customMediaQuery[i].matches ?? true) {
    426        ok(
    427          // handle TextNode
    428          !node.classList ||
    429            !node.classList.contains("media-condition-unmatched"),
    430          `Text for part #${i} of @custom-media ("${node.textContent}") does not have unmatching class`
    431        );
    432      } else {
    433        ok(
    434          node.classList.contains("media-condition-unmatched"),
    435          `Text for part #${i} of @custom-media ("${node.textContent}") has expected unmatching class`
    436        );
    437      }
    438    }
    439  } else {
    440    is(
    441      atTypeEl.textContent,
    442      `@${type}\u00A0${name ? `${name}\u00A0` : ""}`,
    443      "label for at-rule type is correct"
    444    );
    445  }
    446 
    447  const cond = rule.querySelector(".at-rule-condition");
    448  is(
    449    cond.textContent,
    450    conditionText,
    451    "condition label is correct for " + conditionText
    452  );
    453 
    454  if (type == "media") {
    455    const matched = !cond.classList.contains("media-condition-unmatched");
    456    ok(
    457      matches ? matched : !matched,
    458      "media rule is " + (matches ? "matched" : "unmatched")
    459    );
    460  }
    461 
    462  const ruleLine = rule.querySelector(".at-rule-line");
    463  is(ruleLine.textContent, ":" + line, "correct line number shown");
    464 
    465  info(
    466    "Check that clicking on the rule jumps to the expected position in the stylesheet"
    467  );
    468  rule.click();
    469  await waitFor(
    470    () =>
    471      ui.selectedEditor == editor &&
    472      editor.sourceEditor.getCursor().line == line - 1
    473  );
    474  ok(true, "Jumped to the expected location");
    475 }
    476 
    477 /* Helpers */
    478 
    479 function openEditor(editor) {
    480  getLinkFor(editor).click();
    481 
    482  return editor.getSourceEditor();
    483 }
    484 
    485 function getLinkFor(editor) {
    486  return editor.summary.querySelector(".stylesheet-name");
    487 }