tor-browser

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

browser_outputparser.js (55542B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 add_task(async function () {
      7  await SpecialPowers.pushPrefEnv({
      8    set: [
      9      ["security.allow_unsafe_parent_loads", true],
     10      ["layout.css.backdrop-filter.enabled", true],
     11      ["layout.css.relative-color-syntax.enabled", true],
     12      ["dom.security.html_serialization_escape_lt_gt", true],
     13    ],
     14  });
     15  await addTab("about:blank");
     16  await performTest();
     17  gBrowser.removeCurrentTab();
     18 });
     19 
     20 async function performTest() {
     21  const OutputParser = require("resource://devtools/client/shared/output-parser.js");
     22 
     23  const { host, doc } = await createHost(
     24    "bottom",
     25    "data:text/html," + "<h1>browser_outputParser.js</h1><div></div>"
     26  );
     27 
     28  const cssProperties = getClientCssProperties();
     29 
     30  const parser = new OutputParser(doc, cssProperties);
     31  testParseCssProperty(doc, parser);
     32  testParseCssVar(doc, parser);
     33  testParseURL(doc, parser);
     34  testParseFilter(doc, parser);
     35  testParseBackdropFilter(doc, parser);
     36  testParseAngle(doc, parser);
     37  testParseShape(doc, parser);
     38  testParseVariable(doc, parser);
     39  testParseColorVariable(doc, parser);
     40  testParseFontFamily(doc, parser);
     41  testParseLightDark(doc, parser);
     42 
     43  host.destroy();
     44 }
     45 
     46 // Class name used in color swatch.
     47 var COLOR_TEST_CLASS = "test-class";
     48 
     49 // Create a new CSS color-parsing test.  |name| is the name of the CSS
     50 // property.  |value| is the CSS text to use.  |segments| is an array
     51 // describing the expected result.  If an element of |segments| is a
     52 // string, it is simply appended to the expected string.  Otherwise,
     53 // it must be an object with a |name| property, which is the color
     54 // name as it appears in the input.
     55 //
     56 // This approach is taken to reduce boilerplate and to make it simpler
     57 // to modify the test when the parseCssProperty output changes.
     58 function makeColorTest(name, value, segments) {
     59  const result = {
     60    name,
     61    value,
     62    expected: "",
     63  };
     64 
     65  for (const segment of segments) {
     66    if (typeof segment === "string") {
     67      result.expected += segment;
     68    } else {
     69      const buttonAttributes = {
     70        class: COLOR_TEST_CLASS,
     71        style: `background-color:${segment.name}`,
     72        tabindex: 0,
     73        role: "button",
     74      };
     75      if (segment.colorFunction) {
     76        buttonAttributes["data-color-function"] = segment.colorFunction;
     77      }
     78      const buttonAttrString = Object.entries(buttonAttributes)
     79        .map(([attr, v]) => `${attr}="${v}"`)
     80        .join(" ");
     81 
     82      // prettier-ignore
     83      result.expected +=
     84        `<span data-color="${segment.name}" class="color-swatch-container">` +
     85          `<span ${buttonAttrString}></span>`+
     86          `<span>${segment.name}</span>` +
     87        `</span>`;
     88    }
     89  }
     90 
     91  result.desc = "Testing " + name + ": " + value;
     92 
     93  return result;
     94 }
     95 
     96 function testParseCssProperty(doc, parser) {
     97  const tests = [
     98    makeColorTest("border", "1px solid red", ["1px solid ", { name: "red" }]),
     99 
    100    makeColorTest(
    101      "background-image",
    102      "linear-gradient(to right, #F60 10%, rgba(0,0,0,1))",
    103      [
    104        "linear-gradient(to right, ",
    105        { name: "#F60", colorFunction: "linear-gradient" },
    106        " 10%, ",
    107        { name: "rgba(0,0,0,1)", colorFunction: "linear-gradient" },
    108        ")",
    109      ]
    110    ),
    111 
    112    // In "arial black", "black" is a font, not a color.
    113    // (The font-family parser creates a span)
    114    makeColorTest("font-family", "arial black", ["<span>arial black</span>"]),
    115 
    116    makeColorTest("box-shadow", "0 0 1em red", ["0 0 1em ", { name: "red" }]),
    117 
    118    makeColorTest("box-shadow", "0 0 1em red, 2px 2px 0 0 rgba(0,0,0,.5)", [
    119      "0 0 1em ",
    120      { name: "red" },
    121      ", 2px 2px 0 0 ",
    122      { name: "rgba(0,0,0,.5)" },
    123    ]),
    124 
    125    makeColorTest("content", '"red"', ['"red"']),
    126 
    127    // Invalid property names should not cause exceptions.
    128    makeColorTest("hellothere", "'red'", ["'red'"]),
    129 
    130    makeColorTest(
    131      "filter",
    132      "blur(1px) drop-shadow(0 0 0 blue) url(red.svg#blue)",
    133      [
    134        '<span data-filters="blur(1px) drop-shadow(0 0 0 blue) ',
    135        'url(red.svg#blue)"><span>',
    136        "blur(1px) drop-shadow(0 0 0 ",
    137        { name: "blue", colorFunction: "drop-shadow" },
    138        ") url(red.svg#blue)</span></span>",
    139      ]
    140    ),
    141 
    142    makeColorTest("color", "currentColor", ["currentColor"]),
    143 
    144    // Test a very long property.
    145    makeColorTest(
    146      "background-image",
    147      "linear-gradient(to left, transparent 0, transparent 5%,#F00 0, #F00 10%,#FF0 0, #FF0 15%,#0F0 0, #0F0 20%,#0FF 0, #0FF 25%,#00F 0, #00F 30%,#800 0, #800 35%,#880 0, #880 40%,#080 0, #080 45%,#088 0, #088 50%,#008 0, #008 55%,#FFF 0, #FFF 60%,#EEE 0, #EEE 65%,#CCC 0, #CCC 70%,#999 0, #999 75%,#666 0, #666 80%,#333 0, #333 85%,#111 0, #111 90%,#000 0, #000 95%,transparent 0, transparent 100%)",
    148      [
    149        "linear-gradient(to left, ",
    150        { name: "transparent", colorFunction: "linear-gradient" },
    151        " 0, ",
    152        { name: "transparent", colorFunction: "linear-gradient" },
    153        " 5%,",
    154        { name: "#F00", colorFunction: "linear-gradient" },
    155        " 0, ",
    156        { name: "#F00", colorFunction: "linear-gradient" },
    157        " 10%,",
    158        { name: "#FF0", colorFunction: "linear-gradient" },
    159        " 0, ",
    160        { name: "#FF0", colorFunction: "linear-gradient" },
    161        " 15%,",
    162        { name: "#0F0", colorFunction: "linear-gradient" },
    163        " 0, ",
    164        { name: "#0F0", colorFunction: "linear-gradient" },
    165        " 20%,",
    166        { name: "#0FF", colorFunction: "linear-gradient" },
    167        " 0, ",
    168        { name: "#0FF", colorFunction: "linear-gradient" },
    169        " 25%,",
    170        { name: "#00F", colorFunction: "linear-gradient" },
    171        " 0, ",
    172        { name: "#00F", colorFunction: "linear-gradient" },
    173        " 30%,",
    174        { name: "#800", colorFunction: "linear-gradient" },
    175        " 0, ",
    176        { name: "#800", colorFunction: "linear-gradient" },
    177        " 35%,",
    178        { name: "#880", colorFunction: "linear-gradient" },
    179        " 0, ",
    180        { name: "#880", colorFunction: "linear-gradient" },
    181        " 40%,",
    182        { name: "#080", colorFunction: "linear-gradient" },
    183        " 0, ",
    184        { name: "#080", colorFunction: "linear-gradient" },
    185        " 45%,",
    186        { name: "#088", colorFunction: "linear-gradient" },
    187        " 0, ",
    188        { name: "#088", colorFunction: "linear-gradient" },
    189        " 50%,",
    190        { name: "#008", colorFunction: "linear-gradient" },
    191        " 0, ",
    192        { name: "#008", colorFunction: "linear-gradient" },
    193        " 55%,",
    194        { name: "#FFF", colorFunction: "linear-gradient" },
    195        " 0, ",
    196        { name: "#FFF", colorFunction: "linear-gradient" },
    197        " 60%,",
    198        { name: "#EEE", colorFunction: "linear-gradient" },
    199        " 0, ",
    200        { name: "#EEE", colorFunction: "linear-gradient" },
    201        " 65%,",
    202        { name: "#CCC", colorFunction: "linear-gradient" },
    203        " 0, ",
    204        { name: "#CCC", colorFunction: "linear-gradient" },
    205        " 70%,",
    206        { name: "#999", colorFunction: "linear-gradient" },
    207        " 0, ",
    208        { name: "#999", colorFunction: "linear-gradient" },
    209        " 75%,",
    210        { name: "#666", colorFunction: "linear-gradient" },
    211        " 0, ",
    212        { name: "#666", colorFunction: "linear-gradient" },
    213        " 80%,",
    214        { name: "#333", colorFunction: "linear-gradient" },
    215        " 0, ",
    216        { name: "#333", colorFunction: "linear-gradient" },
    217        " 85%,",
    218        { name: "#111", colorFunction: "linear-gradient" },
    219        " 0, ",
    220        { name: "#111", colorFunction: "linear-gradient" },
    221        " 90%,",
    222        { name: "#000", colorFunction: "linear-gradient" },
    223        " 0, ",
    224        { name: "#000", colorFunction: "linear-gradient" },
    225        " 95%,",
    226        { name: "transparent", colorFunction: "linear-gradient" },
    227        " 0, ",
    228        { name: "transparent", colorFunction: "linear-gradient" },
    229        " 100%)",
    230      ]
    231    ),
    232 
    233    // Note the lack of a space before the color here.
    234    makeColorTest("border", "1px dotted#f06", [
    235      "1px dotted ",
    236      { name: "#f06" },
    237    ]),
    238 
    239    makeColorTest("color", "color-mix(in srgb, red, blue)", [
    240      "color-mix(in srgb, ",
    241      { name: "red", colorFunction: "color-mix" },
    242      ", ",
    243      { name: "blue", colorFunction: "color-mix" },
    244      ")",
    245    ]),
    246 
    247    makeColorTest(
    248      "background-image",
    249      "linear-gradient(to top, color-mix(in srgb, #008000, rgba(255, 255, 0, 0.9)), blue, contrast-color(#abc))",
    250      [
    251        "linear-gradient(to top, ",
    252        "color-mix(in srgb, ",
    253        { name: "#008000", colorFunction: "color-mix" },
    254        ", ",
    255        { name: "rgba(255, 255, 0, 0.9)", colorFunction: "color-mix" },
    256        "), ",
    257        { name: "blue", colorFunction: "linear-gradient" },
    258        ", ",
    259        "contrast-color(",
    260        { name: "#abc", colorFunction: "contrast-color" },
    261        ")",
    262        ")",
    263      ]
    264    ),
    265 
    266    makeColorTest("color", "light-dark(red, blue)", [
    267      "light-dark(",
    268      { name: "red", colorFunction: "light-dark" },
    269      ", ",
    270      { name: "blue", colorFunction: "light-dark" },
    271      ")",
    272    ]),
    273 
    274    makeColorTest(
    275      "background-image",
    276      "linear-gradient(to top, light-dark(#008000, rgba(255, 255, 0, 0.9)), blue)",
    277      [
    278        "linear-gradient(to top, ",
    279        "light-dark(",
    280        { name: "#008000", colorFunction: "light-dark" },
    281        ", ",
    282        { name: "rgba(255, 255, 0, 0.9)", colorFunction: "light-dark" },
    283        "), ",
    284        { name: "blue", colorFunction: "linear-gradient" },
    285        ")",
    286      ]
    287    ),
    288 
    289    makeColorTest("color", "rgb(from gold r g b)", [
    290      { name: "rgb(from gold r g b)" },
    291    ]),
    292 
    293    makeColorTest("color", "color(from hsl(0 100% 50%) xyz x y 0.5)", [
    294      { name: "color(from hsl(0 100% 50%) xyz x y 0.5)" },
    295    ]),
    296 
    297    makeColorTest(
    298      "color",
    299      "oklab(from red calc(l - 1) calc(a * 2) calc(b + 3) / alpha)",
    300      [{ name: "oklab(from red calc(l - 1) calc(a * 2) calc(b + 3) / alpha)" }]
    301    ),
    302 
    303    makeColorTest(
    304      "color",
    305      "rgb(from color-mix(in lch, plum 40%, pink) r g b)",
    306      [{ name: "rgb(from color-mix(in lch, plum 40%, pink) r g b)" }]
    307    ),
    308 
    309    makeColorTest("color", "rgb(from rgb(from gold r g b) r g b)", [
    310      { name: "rgb(from rgb(from gold r g b) r g b)" },
    311    ]),
    312 
    313    makeColorTest(
    314      "background-image",
    315      "linear-gradient(to right, #F60 10%, rgb(from gold r g b))",
    316      [
    317        "linear-gradient(to right, ",
    318        { name: "#F60", colorFunction: "linear-gradient" },
    319        " 10%, ",
    320        { name: "rgb(from gold r g b)", colorFunction: "linear-gradient" },
    321        ")",
    322      ]
    323    ),
    324 
    325    {
    326      desc: "--a: (min-width:680px)",
    327      name: "--a",
    328      value: "(min-width:680px)",
    329      expected: "(min-width:680px)",
    330    },
    331 
    332    {
    333      desc: "Interactive color swatch",
    334      name: "color",
    335      value: "gold",
    336      expected:
    337        // prettier-ignore
    338        `<span data-color="gold" class="color-swatch-container">` +
    339          `<span class="test-class" style="background-color:gold" tabindex="0" role="button"></span>` +
    340          `<span>gold</span>` +
    341        `</span>`,
    342      parserExtraOptions: {
    343        colorSwatchReadOnly: false,
    344      },
    345    },
    346 
    347    {
    348      desc: "Read-only color swatch",
    349      name: "color",
    350      value: "gold",
    351      expected:
    352        // prettier-ignore
    353        `<span data-color="gold" class="color-swatch-container">` +
    354          `<span class="test-class" style="background-color:gold"></span>` +
    355          `<span>gold</span>` +
    356        `</span>`,
    357      parserExtraOptions: {
    358        colorSwatchReadOnly: true,
    359      },
    360    },
    361 
    362    makeColorTest("color", "contrast-color(red)", [
    363      "contrast-color(",
    364      { name: "red", colorFunction: "contrast-color" },
    365      ")",
    366    ]),
    367 
    368    makeColorTest(
    369      "color",
    370      "color-mix(in srgb, red, contrast-color(hsl(0 100 200)))",
    371      [
    372        "color-mix(in srgb, ",
    373        { name: "red", colorFunction: "color-mix" },
    374        ", ",
    375        "contrast-color(",
    376        { name: "hsl(0 100 200)", colorFunction: "contrast-color" },
    377        ")",
    378        ")",
    379      ]
    380    ),
    381  ];
    382 
    383  const target = doc.querySelector("div");
    384  ok(target, "captain, we have the div");
    385 
    386  for (const test of tests) {
    387    info(test.desc);
    388 
    389    const frag = parser.parseCssProperty(test.name, test.value, {
    390      colorSwatchClass: COLOR_TEST_CLASS,
    391      ...(test.parserExtraOptions || {}),
    392    });
    393 
    394    target.appendChild(frag);
    395 
    396    is(
    397      target.innerHTML,
    398      test.expected,
    399      "CSS property correctly parsed for " + test.name + ": " + test.value
    400    );
    401 
    402    target.innerHTML = "";
    403  }
    404 }
    405 
    406 function testParseCssVar(doc, parser) {
    407  const frag = parser.parseCssProperty("color", "var(--some-kind-of-green)", {
    408    colorSwatchClass: "test-colorswatch",
    409  });
    410 
    411  const target = doc.querySelector("div");
    412  ok(target, "captain, we have the div");
    413  target.appendChild(frag);
    414 
    415  is(
    416    target.innerHTML,
    417    "var(--some-kind-of-green)",
    418    "CSS property correctly parsed"
    419  );
    420 
    421  target.innerHTML = "";
    422 }
    423 
    424 function testParseURL(doc, parser) {
    425  info("Test that URL parsing preserves quoting style");
    426 
    427  const tests = [
    428    {
    429      desc: "simple test without quotes",
    430      leader: "url(",
    431      trailer: ")",
    432    },
    433    {
    434      desc: "simple test with single quotes",
    435      leader: "url('",
    436      trailer: "')",
    437    },
    438    {
    439      desc: "simple test with double quotes",
    440      leader: 'url("',
    441      trailer: '")',
    442    },
    443    {
    444      desc: "test with single quotes and whitespace",
    445      leader: "url( \t'",
    446      trailer: "'\r\n\f)",
    447    },
    448    {
    449      desc: "simple test with uppercase",
    450      leader: "URL(",
    451      trailer: ")",
    452    },
    453    {
    454      desc: "bad url, missing paren",
    455      leader: "url(",
    456      trailer: "",
    457      expectedTrailer: ")",
    458    },
    459    {
    460      desc: "bad url, missing paren, with baseURI",
    461      baseURI: "data:text/html,<style></style>",
    462      leader: "url(",
    463      trailer: "",
    464      expectedTrailer: ")",
    465    },
    466    {
    467      desc: "bad url, double quote, missing paren",
    468      leader: 'url("',
    469      trailer: '"',
    470      expectedTrailer: '")',
    471    },
    472    {
    473      desc: "bad url, single quote, missing paren and quote",
    474      leader: "url('",
    475      trailer: "",
    476      expectedTrailer: "')",
    477    },
    478  ];
    479 
    480  for (const test of tests) {
    481    const url = test.leader + "something.jpg" + test.trailer;
    482    const frag = parser.parseCssProperty("background", url, {
    483      urlClass: "test-urlclass",
    484      baseURI: test.baseURI,
    485    });
    486 
    487    const target = doc.querySelector("div");
    488    target.appendChild(frag);
    489 
    490    const expectedTrailer = test.expectedTrailer || test.trailer;
    491 
    492    const expected =
    493      test.leader +
    494      '<a target="_blank" class="test-urlclass" ' +
    495      'href="something.jpg">something.jpg</a>' +
    496      expectedTrailer;
    497 
    498    is(target.innerHTML, expected, test.desc);
    499 
    500    target.innerHTML = "";
    501  }
    502 }
    503 
    504 function testParseFilter(doc, parser) {
    505  const frag = parser.parseCssProperty("filter", "something invalid", {
    506    filterSwatchClass: "test-filterswatch",
    507  });
    508 
    509  const swatchCount = frag.querySelectorAll(".test-filterswatch").length;
    510  is(swatchCount, 1, "filter swatch was created");
    511 }
    512 
    513 function testParseBackdropFilter(doc, parser) {
    514  const frag = parser.parseCssProperty("backdrop-filter", "something invalid", {
    515    filterSwatchClass: "test-filterswatch",
    516  });
    517 
    518  const swatchCount = frag.querySelectorAll(".test-filterswatch").length;
    519  is(swatchCount, 1, "filter swatch was created for backdrop-filter");
    520 }
    521 
    522 function testParseAngle(doc, parser) {
    523  let frag = parser.parseCssProperty("rotate", "90deg", {
    524    angleSwatchClass: "test-angleswatch",
    525  });
    526 
    527  let swatchCount = frag.querySelectorAll(".test-angleswatch").length;
    528  is(swatchCount, 1, "angle swatch was created");
    529 
    530  frag = parser.parseCssProperty(
    531    "background-image",
    532    "linear-gradient(90deg, red, blue",
    533    {
    534      angleSwatchClass: "test-angleswatch",
    535    }
    536  );
    537 
    538  swatchCount = frag.querySelectorAll(".test-angleswatch").length;
    539  is(swatchCount, 1, "angle swatch was created");
    540 }
    541 
    542 function testParseShape(doc, parser) {
    543  info("Test shape parsing");
    544 
    545  const tests = [
    546    {
    547      desc: "Polygon shape",
    548      definition:
    549        "polygon(evenodd, 0px 0px, 10%200px,30%30% , calc(250px - 10px) 0 ,\n " +
    550        "12em var(--variable), 100% 100%) margin-box",
    551      spanCount: 18,
    552    },
    553    {
    554      desc: "POLYGON()",
    555      definition:
    556        "POLYGON(evenodd, 0px 0px, 10%200px,30%30% , calc(250px - 10px) 0 ,\n " +
    557        "12em var(--variable), 100% 100%) margin-box",
    558      spanCount: 18,
    559    },
    560    {
    561      desc: "Invalid polygon shape",
    562      definition: "polygon(0px 0px 100px 20px, 20% 20%)",
    563      spanCount: 0,
    564    },
    565    {
    566      desc: "Circle shape with all arguments",
    567      definition: "circle(25% at\n 30% 200px) border-box",
    568      spanCount: 4,
    569    },
    570    {
    571      desc: "Circle shape with only one center",
    572      definition: "circle(25em at 40%)",
    573      spanCount: 3,
    574    },
    575    {
    576      desc: "Circle shape with no radius",
    577      definition: "circle(at 30% 40%)",
    578      spanCount: 3,
    579    },
    580    {
    581      desc: "Circle shape with no center",
    582      definition: "circle(12em)",
    583      spanCount: 1,
    584    },
    585    {
    586      desc: "Circle shape with no arguments",
    587      definition: "circle()",
    588      spanCount: 0,
    589    },
    590    {
    591      desc: "Circle shape with no space before at",
    592      definition: "circle(25%at 30% 30%)",
    593      spanCount: 4,
    594    },
    595    {
    596      desc: "CIRCLE",
    597      definition: "CIRCLE(12em)",
    598      spanCount: 1,
    599    },
    600    {
    601      desc: "Invalid circle shape",
    602      definition: "circle(25%at30%30%)",
    603      spanCount: 0,
    604    },
    605    {
    606      desc: "Ellipse shape with all arguments",
    607      definition: "ellipse(200px 10em at 25% 120px) content-box",
    608      spanCount: 5,
    609    },
    610    {
    611      desc: "Ellipse shape with only one center",
    612      definition: "ellipse(200px 10% at 120px)",
    613      spanCount: 4,
    614    },
    615    {
    616      desc: "Ellipse shape with no radius",
    617      definition: "ellipse(at 25% 120px)",
    618      spanCount: 3,
    619    },
    620    {
    621      desc: "Ellipse shape with no center",
    622      definition: "ellipse(200px\n10em)",
    623      spanCount: 2,
    624    },
    625    {
    626      desc: "Ellipse shape with no arguments",
    627      definition: "ellipse()",
    628      spanCount: 0,
    629    },
    630    {
    631      desc: "ELLIPSE()",
    632      definition: "ELLIPSE(200px 10em)",
    633      spanCount: 2,
    634    },
    635    {
    636      desc: "Invalid ellipse shape",
    637      definition: "ellipse(200px100px at 30$ 20%)",
    638      spanCount: 0,
    639    },
    640    {
    641      desc: "Inset shape with 4 arguments",
    642      definition: "inset(200px 100px\n 30%15%)",
    643      spanCount: 4,
    644    },
    645    {
    646      desc: "Inset shape with 3 arguments",
    647      definition: "inset(200px 100px 15%)",
    648      spanCount: 3,
    649    },
    650    {
    651      desc: "Inset shape with 2 arguments",
    652      definition: "inset(200px 100px)",
    653      spanCount: 2,
    654    },
    655    {
    656      desc: "Inset shape with 1 argument",
    657      definition: "inset(200px)",
    658      spanCount: 1,
    659    },
    660    {
    661      desc: "Inset shape with 0 arguments",
    662      definition: "inset()",
    663      spanCount: 0,
    664    },
    665    {
    666      desc: "INSET()",
    667      definition: "INSET(200px)",
    668      spanCount: 1,
    669    },
    670    {
    671      desc: "offset-path property with inset shape value",
    672      property: "offset-path",
    673      definition: "inset(200px)",
    674      spanCount: 1,
    675    },
    676  ];
    677 
    678  for (const { desc, definition, property = "clip-path", spanCount } of tests) {
    679    info(desc);
    680    const frag = parser.parseCssProperty(property, definition, {
    681      shapeClass: "inspector-shape",
    682    });
    683    const spans = frag.querySelectorAll(".inspector-shape-point");
    684    is(spans.length, spanCount, desc + " span count");
    685    is(frag.textContent, definition, desc + " text content");
    686  }
    687 }
    688 
    689 function getJumpToVariableButton(varName) {
    690  return `<button class="ruleview-variable-link jump-definition" data-variable-name="${varName}" title="Jump to variable definition"></button>`;
    691 }
    692 
    693 function testParseVariable(doc, parser) {
    694  const TESTS = [
    695    {
    696      text: "var(--seen)",
    697      variables: { "--seen": "chartreuse" },
    698      expected:
    699        // prettier-ignore
    700        '<span data-color="chartreuse">' +
    701          "<span>var(" +
    702            `<span data-variable="chartreuse">--seen${getJumpToVariableButton("--seen")}</span>)` +
    703          "</span>" +
    704        "</span>",
    705    },
    706    {
    707      text: "var(--seen)",
    708      variables: {
    709        "--seen": { value: "var(--base)", computedValue: "1em" },
    710      },
    711      expected:
    712        // prettier-ignore
    713        "<span>var(" +
    714          `<span data-variable="var(--base)" data-variable-computed="1em">--seen${getJumpToVariableButton("--seen")}</span>)` +
    715        "</span>",
    716    },
    717    {
    718      text: "var(--not-seen)",
    719      variables: {},
    720      expected:
    721        // prettier-ignore
    722        "<span>var(" +
    723          '<span class="unmatched-class" data-variable="--not-seen is not set">--not-seen</span>' +
    724        ")</span>",
    725    },
    726    {
    727      text: "var(--seen, seagreen)",
    728      variables: { "--seen": "chartreuse" },
    729      expected:
    730        // prettier-ignore
    731        '<span data-color="chartreuse">' +
    732          "<span>var(" +
    733            `<span data-variable="chartreuse">--seen${getJumpToVariableButton("--seen")}</span>,` +
    734            '<span class="unmatched-class"> ' +
    735              '<span data-color="seagreen">' +
    736                "<span>seagreen</span>" +
    737              "</span>" +
    738            "</span>)" +
    739          "</span>" +
    740        "</span>",
    741    },
    742    {
    743      text: "var(--not-seen, var(--seen))",
    744      variables: { "--seen": "chartreuse" },
    745      expected:
    746        // prettier-ignore
    747        "<span>var(" +
    748          '<span class="unmatched-class" data-variable="--not-seen is not set">--not-seen</span>,' +
    749          "<span> " +
    750            '<span data-color="chartreuse">' +
    751              "<span>var(" +
    752                `<span data-variable="chartreuse">--seen${getJumpToVariableButton("--seen")}</span>)` +
    753              "</span>" +
    754            "</span>" +
    755          "</span>)" +
    756        "</span>",
    757    },
    758    {
    759      text: "color-mix(in sgrb, var(--x), purple)",
    760      variables: { "--x": "yellow" },
    761      expected:
    762        // prettier-ignore
    763        `color-mix(in sgrb, ` +
    764        `<span data-color="yellow" class="color-swatch-container">` +
    765          `<span class="test-class" style="background-color:yellow" tabindex="0" role="button" data-color-function="color-mix">` +
    766          `</span>` +
    767          `<span>var(<span data-variable="yellow">--x${getJumpToVariableButton("--x")}</span>)</span>` +
    768        `</span>` +
    769        `, ` +
    770        `<span data-color="purple" class="color-swatch-container">` +
    771          `<span class="test-class" style="background-color:purple" tabindex="0" role="button" data-color-function="color-mix">` +
    772          `</span>` +
    773          `<span>purple</span>` +
    774        `</span>` +
    775        `)`,
    776      parserExtraOptions: {
    777        colorSwatchClass: COLOR_TEST_CLASS,
    778      },
    779    },
    780    {
    781      text: "light-dark(var(--light), var(--dark))",
    782      variables: { "--light": "yellow", "--dark": "gold" },
    783      expected:
    784        // prettier-ignore
    785        `light-dark(` +
    786        `<span data-color="yellow" class="color-swatch-container">` +
    787          `<span class="test-class" style="background-color:yellow" tabindex="0" role="button" data-color-function="light-dark">` +
    788          `</span>` +
    789          `<span>var(<span data-variable="yellow">--light${getJumpToVariableButton("--light")}</span>)</span>` +
    790        `</span>` +
    791        `, ` +
    792        `<span data-color="gold" class="color-swatch-container">` +
    793          `<span class="test-class" style="background-color:gold" tabindex="0" role="button" data-color-function="light-dark">` +
    794          `</span>` +
    795          `<span>var(<span data-variable="gold">--dark${getJumpToVariableButton("--dark")}</span>)</span>` +
    796        `</span>` +
    797        `)`,
    798      parserExtraOptions: {
    799        colorSwatchClass: COLOR_TEST_CLASS,
    800      },
    801    },
    802    {
    803      text: "1px solid var(--seen, seagreen)",
    804      // See Bug 1911974
    805      skipVariableDeclarationTest: true,
    806      variables: { "--seen": "chartreuse" },
    807      expected:
    808        // prettier-ignore
    809        '1px solid ' +
    810        '<span data-color="chartreuse">' +
    811          "<span>var(" +
    812            `<span data-variable="chartreuse">--seen${getJumpToVariableButton("--seen")}</span>,` +
    813            '<span class="unmatched-class"> ' +
    814              '<span data-color="seagreen">' +
    815                "<span>seagreen</span>" +
    816              "</span>" +
    817            "</span>)" +
    818          "</span>" +
    819        "</span>",
    820    },
    821    {
    822      text: "1px solid var(--not-seen, seagreen)",
    823      // See Bug 1911975
    824      skipVariableDeclarationTest: true,
    825      variables: {},
    826      expected:
    827        // prettier-ignore
    828        `1px solid ` +
    829        `<span>var(` +
    830          `<span class="unmatched-class" data-variable="--not-seen is not set">--not-seen</span>,` +
    831          `<span> ` +
    832            `<span data-color="seagreen">` +
    833              `<span>seagreen</span>` +
    834            `</span>` +
    835          `</span>)` +
    836        `</span>`,
    837    },
    838    {
    839      text: "rgba(var(--r), 0, 0, var(--a))",
    840      variables: { "--r": "255", "--a": "0.5" },
    841      expected:
    842        // prettier-ignore
    843        '<span data-color="rgba(255, 0, 0, 0.5)">' +
    844          "<span>rgba("+
    845            "<span>" +
    846              `var(<span data-variable="255">--r${getJumpToVariableButton("--r")}</span>)` +
    847            "</span>, 0, 0, " +
    848            "<span>" +
    849              `var(<span data-variable="0.5">--a${getJumpToVariableButton("--a")}</span>)` +
    850            "</span>" +
    851          ")</span>" +
    852        "</span>",
    853    },
    854    {
    855      text: "rgba(from var(--base) r g 0 / calc(var(--a) * 0.5))",
    856      variables: { "--base": "red", "--a": "0.8" },
    857      expected:
    858        // prettier-ignore
    859        '<span data-color="rgba(from red r g 0 / calc(0.8 * 0.5))">' +
    860          "<span>rgba("+
    861            "from " +
    862            "<span>" +
    863              `var(<span data-variable="red">--base${getJumpToVariableButton("--base")}</span>)` +
    864            "</span> r g 0 / " +
    865            "calc(" +
    866            "<span>" +
    867              `var(<span data-variable="0.8">--a${getJumpToVariableButton("--a")}</span>)` +
    868            "</span>" +
    869            " * 0.5)" +
    870          ")</span>" +
    871        "</span>",
    872    },
    873    {
    874      text: "rgb(var(--not-seen, 255), 0, 0)",
    875      variables: {},
    876      expected:
    877        // prettier-ignore
    878        '<span data-color="rgb( 255, 0, 0)">' +
    879          "<span>rgb("+
    880            "<span>var(" +
    881              `<span class="unmatched-class" data-variable="--not-seen is not set">--not-seen</span>,` +
    882              `<span> 255</span>` +
    883            ")</span>, 0, 0" +
    884          ")</span>" +
    885        "</span>",
    886    },
    887    {
    888      text: "rgb(var(--not-seen), 0, 0)",
    889      variables: {},
    890      expected:
    891        // prettier-ignore
    892        `rgb(` +
    893          `<span>` +
    894            `var(` +
    895              `<span class="unmatched-class" data-variable="--not-seen is not set">` +
    896                `--not-seen` +
    897              `</span>` +
    898            `)` +
    899          `</span>` +
    900          `, 0, 0` +
    901        `)`,
    902    },
    903    {
    904      text: "var(--registered)",
    905      variables: {
    906        "--registered": {
    907          value: "chartreuse",
    908          registeredProperty: {
    909            syntax: "<color>",
    910            inherits: true,
    911            initialValue: "hotpink",
    912          },
    913        },
    914      },
    915      expected:
    916        // prettier-ignore
    917        '<span data-color="chartreuse">' +
    918          "<span>var(" +
    919            '<span ' +
    920              'data-variable="chartreuse" ' +
    921              'data-registered-property-initial-value="hotpink" ' +
    922              'data-registered-property-syntax="&lt;color&gt;" ' +
    923              'data-registered-property-inherits="true"' +
    924            `>--registered${getJumpToVariableButton("--registered")}</span>)` +
    925          "</span>" +
    926        "</span>",
    927    },
    928    {
    929      text: "var(--registered-universal)",
    930      variables: {
    931        "--registered-universal": {
    932          value: "chartreuse",
    933          registeredProperty: {
    934            syntax: "*",
    935            inherits: false,
    936          },
    937        },
    938      },
    939      expected:
    940        // prettier-ignore
    941        '<span data-color="chartreuse">' +
    942          "<span>var(" +
    943            '<span ' +
    944              'data-variable="chartreuse" ' +
    945              'data-registered-property-syntax="*" ' +
    946              'data-registered-property-inherits="false"' +
    947            `>--registered-universal${getJumpToVariableButton("--registered-universal")}</span>)` +
    948          "</span>" +
    949        "</span>",
    950    },
    951    {
    952      text: "var(--x)",
    953      variables: {
    954        "--x": "light-dark(red, blue)",
    955      },
    956      parserExtraOptions: {
    957        isDarkColorScheme: false,
    958      },
    959      expected: `<span>var(<span data-variable="light-dark(red, blue)">--x${getJumpToVariableButton("--x")}</span>)</span>`,
    960    },
    961    {
    962      text: "var(--x)",
    963      variables: {
    964        "--x": "color-mix(in srgb, red 50%, blue)",
    965      },
    966      parserExtraOptions: {
    967        isDarkColorScheme: false,
    968      },
    969      expected:
    970        // prettier-ignore
    971        '<span data-color="color-mix(in srgb, red 50%, blue)">' +
    972          '<span>var(' +
    973            `<span data-variable="color-mix(in srgb, red 50%, blue)">--x${getJumpToVariableButton("--x")}</span>` +
    974          ')</span>' +
    975        '</span>',
    976    },
    977    {
    978      text: "var(--x)",
    979      variables: {
    980        "--x": "contrast-color(blue)",
    981      },
    982      parserExtraOptions: {
    983        isDarkColorScheme: false,
    984      },
    985      expected:
    986        // prettier-ignore
    987        '<span data-color="contrast-color(blue)">' +
    988          '<span>var(' +
    989            `<span data-variable="contrast-color(blue)">--x${getJumpToVariableButton("--x")}</span>` +
    990          ')</span>' +
    991        '</span>',
    992    },
    993    {
    994      text: "var(--refers-empty)",
    995      variables: {
    996        "--refers-empty": { value: "var(--empty)", computedValue: "" },
    997      },
    998      expected:
    999        // prettier-ignore
   1000        "<span>var(" +
   1001          `<span data-variable="var(--empty)" data-variable-computed="">--refers-empty${getJumpToVariableButton("--refers-empty")}</span>)` +
   1002        "</span>",
   1003    },
   1004    {
   1005      text: "hsl(50, 70%, var(--foo))",
   1006      variables: {
   1007        "--foo": "40%",
   1008      },
   1009      expected:
   1010        // prettier-ignore
   1011        `<span data-color="hsl(50, 70%, 40%)">` +
   1012          `<span>`+
   1013            `hsl(50, 70%, ` +
   1014            `<span>` +
   1015              `var(` +
   1016                `<span data-variable="40%">--foo${getJumpToVariableButton("--foo")}</span>` +
   1017              `)` +
   1018            `</span>)` +
   1019          `</span>` +
   1020        `</span>`,
   1021    },
   1022    {
   1023      text: "var(--bar)",
   1024      variables: {
   1025        "--foo": "40%",
   1026        "--bar": "hsl(50, 70%, var(--foo))",
   1027      },
   1028      expected:
   1029        // prettier-ignore
   1030        `<span data-color="hsl(50, 70%, 40%)">` +
   1031          `<span>` +
   1032            `var(` +
   1033              `<span data-variable="hsl(50, 70%, var(--foo))" data-variable-computed="hsl(50, 70%, 40%)">--bar${getJumpToVariableButton("--bar")}</span>` +
   1034            `)` +
   1035          `</span>` +
   1036        `</span>`,
   1037    },
   1038    {
   1039      text: "var(--primary)",
   1040      variables: {
   1041        "--primary": "hsl(10, 100%, var(--fur))",
   1042        "--fur": "var(--bar)",
   1043        "--bar": "var(--foo)",
   1044        "--foo": "50%",
   1045      },
   1046      expected:
   1047        // prettier-ignore
   1048        `<span data-color="hsl(10, 100%, 50%)">` +
   1049          `<span>` +
   1050            `var(` +
   1051              `<span data-variable="hsl(10, 100%, var(--fur))" data-variable-computed="hsl(10, 100%, 50%)">--primary${getJumpToVariableButton("--primary")}</span>` +
   1052            `)` +
   1053          `</span>` +
   1054        `</span>`,
   1055    },
   1056    {
   1057      text: "oklch(var(--fur) 20 var(--boo))",
   1058      variables: {
   1059        "--fur": "var(--baz)",
   1060        "--baz": "var(--foo)",
   1061        "--foo": "10",
   1062        "--boo": "30",
   1063      },
   1064      expected:
   1065        // prettier-ignore
   1066        `<span data-color="oklch(10 20 30)">` +
   1067          `<span>oklch(` +
   1068            `<span>` +
   1069              `var(` +
   1070                `<span data-variable="var(--baz)" data-variable-computed="10">--fur${getJumpToVariableButton("--fur")}</span>` +
   1071              `)` +
   1072            `</span>` +
   1073            ` 20 ` +
   1074            `<span>` +
   1075              `var(` +
   1076                `<span data-variable="30">--boo${getJumpToVariableButton("--boo")}</span>` +
   1077              `)` +
   1078            `</span>` +
   1079          `)</span>` +
   1080        `</span>`,
   1081    },
   1082    {
   1083      text: "var(--x)",
   1084      variables: {
   1085        "--x": "10px",
   1086      },
   1087      parserExtraOptions: {
   1088        showJumpToVariableButton: false,
   1089      },
   1090      // This shouldn't have a Jump to variable button
   1091      expected: `<span>var(<span data-variable="10px">--x</span>)</span>`,
   1092    },
   1093  ];
   1094 
   1095  const target = doc.querySelector("div");
   1096 
   1097  const VAR_NAME_TO_DEFINE = "--test-parse-variable";
   1098  for (const test of TESTS) {
   1099    // VAR_NAME_TO_DEFINE is used to test parsing the test.text if it's set for a
   1100    // variable declaration, so it shouldn't be set in test.variables to avoid
   1101    // messing with the test results.
   1102    if (VAR_NAME_TO_DEFINE in test.variables) {
   1103      throw new Error(`${VAR_NAME_TO_DEFINE} shouldn't be set in variables`);
   1104    }
   1105 
   1106    // Also set the variable we're going to define, so its value can be computed as well
   1107    const variables = {
   1108      ...(test.variables || {}),
   1109      [VAR_NAME_TO_DEFINE]: test.text,
   1110    };
   1111    // Set the variables to an element so we can get their computed values
   1112    for (const [varName, varData] of Object.entries(variables)) {
   1113      doc.body.style.setProperty(
   1114        varName,
   1115        typeof varData === "string" ? varData : varData.value
   1116      );
   1117    }
   1118 
   1119    const getVariableData = function (varName) {
   1120      if (typeof variables[varName] === "string") {
   1121        const value = variables[varName];
   1122        const computedValue = getComputedStyle(doc.body).getPropertyValue(
   1123          varName
   1124        );
   1125        return { value, computedValue };
   1126      }
   1127 
   1128      return variables[varName] || {};
   1129    };
   1130 
   1131    const frag = parser.parseCssProperty("color", test.text, {
   1132      getVariableData,
   1133      unmatchedClass: "unmatched-class",
   1134      ...(test.parserExtraOptions || {}),
   1135    });
   1136 
   1137    target.appendChild(frag);
   1138 
   1139    is(
   1140      target.innerHTML,
   1141      test.expected,
   1142      `"color: ${test.text}" is parsed as expected`
   1143    );
   1144 
   1145    target.innerHTML = "";
   1146 
   1147    if (test.skipVariableDeclarationTest) {
   1148      continue;
   1149    }
   1150 
   1151    const varFrag = parser.parseCssProperty(
   1152      "--test-parse-variable",
   1153      test.text,
   1154      {
   1155        getVariableData,
   1156        unmatchedClass: "unmatched-class",
   1157        ...(test.parserExtraOptions || {}),
   1158      }
   1159    );
   1160 
   1161    target.appendChild(varFrag);
   1162 
   1163    is(
   1164      target.innerHTML,
   1165      test.expected,
   1166      `"--test-parse-variable: ${test.text}" is parsed as expected`
   1167    );
   1168 
   1169    target.innerHTML = "";
   1170 
   1171    // Remove the variables to an element so we can get their computed values
   1172    for (const varName in variables || {}) {
   1173      doc.body.style.removeProperty(varName);
   1174    }
   1175  }
   1176 }
   1177 
   1178 function testParseColorVariable(doc, parser) {
   1179  const testCategories = [
   1180    {
   1181      desc: "Test for CSS variable defining color",
   1182      tests: [
   1183        makeColorTest("--test-var", "lime", [{ name: "lime" }]),
   1184        makeColorTest("--test-var", "#000", [{ name: "#000" }]),
   1185      ],
   1186    },
   1187    {
   1188      desc: "Test for CSS variable not defining color",
   1189      tests: [
   1190        makeColorTest("--foo", "something", ["something"]),
   1191        makeColorTest("--bar", "Arial Black", ["Arial Black"]),
   1192        makeColorTest("--baz", "10vmin", ["10vmin"]),
   1193      ],
   1194    },
   1195    {
   1196      desc: "Test for non CSS variable defining color",
   1197      tests: [
   1198        makeColorTest("non-css-variable", "lime", ["lime"]),
   1199        makeColorTest("-non-css-variable", "#000", ["#000"]),
   1200      ],
   1201    },
   1202  ];
   1203 
   1204  for (const category of testCategories) {
   1205    info(category.desc);
   1206 
   1207    for (const test of category.tests) {
   1208      info(test.desc);
   1209      const target = doc.querySelector("div");
   1210 
   1211      const frag = parser.parseCssProperty(test.name, test.value, {
   1212        colorSwatchClass: COLOR_TEST_CLASS,
   1213      });
   1214 
   1215      target.appendChild(frag);
   1216 
   1217      is(
   1218        target.innerHTML,
   1219        test.expected,
   1220        `The parsed result for '${test.name}: ${test.value}' is correct`
   1221      );
   1222 
   1223      target.innerHTML = "";
   1224    }
   1225  }
   1226 }
   1227 
   1228 function testParseFontFamily(doc, parser) {
   1229  info("Test font-family parsing");
   1230  const tests = [
   1231    {
   1232      desc: "No fonts",
   1233      definition: "",
   1234      families: [],
   1235    },
   1236    {
   1237      desc: "List of fonts",
   1238      definition: "Arial,Helvetica,sans-serif",
   1239      families: ["Arial", "Helvetica", "sans-serif"],
   1240    },
   1241    {
   1242      desc: "Fonts with spaces",
   1243      definition: "Open Sans",
   1244      families: ["Open Sans"],
   1245    },
   1246    {
   1247      desc: "Quoted fonts",
   1248      definition: "\"Arial\",'Open Sans'",
   1249      families: ["Arial", "Open Sans"],
   1250    },
   1251    {
   1252      desc: "Fonts with extra whitespace",
   1253      definition: " Open  Sans  ",
   1254      families: ["Open Sans"],
   1255    },
   1256  ];
   1257 
   1258  const textContentTests = [
   1259    {
   1260      desc: "No whitespace between fonts",
   1261      definition: "Arial,Helvetica,sans-serif",
   1262      output: "Arial,Helvetica,sans-serif",
   1263    },
   1264    {
   1265      desc: "Whitespace between fonts",
   1266      definition: "Arial ,  Helvetica,   sans-serif",
   1267      output: "Arial , Helvetica, sans-serif",
   1268    },
   1269    {
   1270      desc: "Whitespace before first font trimmed",
   1271      definition: "  Arial,Helvetica,sans-serif",
   1272      output: "Arial,Helvetica,sans-serif",
   1273    },
   1274    {
   1275      desc: "Whitespace after last font trimmed",
   1276      definition: "Arial,Helvetica,sans-serif  ",
   1277      output: "Arial,Helvetica,sans-serif",
   1278    },
   1279    {
   1280      desc: "Whitespace between quoted fonts",
   1281      definition: "'Arial' ,  \"Helvetica\" ",
   1282      output: "'Arial' , \"Helvetica\"",
   1283    },
   1284    {
   1285      desc: "Whitespace within font preserved",
   1286      definition: "'  Ari al '",
   1287      output: "'  Ari al '",
   1288    },
   1289  ];
   1290 
   1291  for (const { desc, definition, families } of tests) {
   1292    info(desc);
   1293    const frag = parser.parseCssProperty("font-family", definition, {
   1294      fontFamilyClass: "ruleview-font-family",
   1295    });
   1296    const spans = frag.querySelectorAll(".ruleview-font-family");
   1297 
   1298    is(spans.length, families.length, desc + " span count");
   1299    for (let i = 0; i < spans.length; i++) {
   1300      is(spans[i].textContent, families[i], desc + " span contents");
   1301    }
   1302  }
   1303 
   1304  info("Test font-family text content");
   1305  for (const { desc, definition, output } of textContentTests) {
   1306    info(desc);
   1307    const frag = parser.parseCssProperty("font-family", definition, {});
   1308    is(frag.textContent, output, desc + " text content matches");
   1309  }
   1310 
   1311  info("Test font-family with custom properties");
   1312  const frag = parser.parseCssProperty(
   1313    "font-family",
   1314    "var(--family, Georgia, serif)",
   1315    {
   1316      getVariableData: () => ({}),
   1317      unmatchedClass: "unmatched-class",
   1318      fontFamilyClass: "ruleview-font-family",
   1319    }
   1320  );
   1321  const target = doc.createElement("div");
   1322  target.appendChild(frag);
   1323  is(
   1324    target.innerHTML,
   1325    // prettier-ignore
   1326    `<span>var(` +
   1327      `<span class="unmatched-class" data-variable="--family is not set">` +
   1328        `--family` +
   1329      `</span>` +
   1330      `,` +
   1331      `<span> ` +
   1332        `<span class="ruleview-font-family">Georgia</span>` +
   1333        `, ` +
   1334        `<span class="ruleview-font-family">serif</span>` +
   1335      `</span>)` +
   1336    `</span>`,
   1337    "Got expected output for font-family with custom properties"
   1338  );
   1339 }
   1340 
   1341 function testParseLightDark(doc, parser) {
   1342  const TESTS = [
   1343    {
   1344      message:
   1345        "Not passing isDarkColorScheme doesn't add unmatched classes to parameters",
   1346      propertyName: "color",
   1347      propertyValue: "light-dark(red, blue)",
   1348      expected:
   1349        // prettier-ignore
   1350        `light-dark(` +
   1351        `<span data-color="red" class="color-swatch-container">` +
   1352          `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1353          `<span>red</span>` +
   1354        `</span>, ` +
   1355        `<span data-color="blue" class="color-swatch-container">` +
   1356          `<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1357          `<span>blue</span>` +
   1358        `</span>` +
   1359      `)`,
   1360    },
   1361    {
   1362      message: "in light mode, the second parameter gets the unmatched class",
   1363      propertyName: "color",
   1364      propertyValue: "light-dark(red, blue)",
   1365      isDarkColorScheme: false,
   1366      expected:
   1367        // prettier-ignore
   1368        `light-dark(` +
   1369        `<span data-color="red" class="color-swatch-container">` +
   1370          `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1371          `<span>red</span>` +
   1372        `</span>, ` +
   1373        `<span data-color="blue" class="color-swatch-container unmatched-class">` +
   1374          `<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1375          `<span>blue</span>` +
   1376        `</span>` +
   1377      `)`,
   1378    },
   1379    {
   1380      message: "in dark mode, the first parameter gets the unmatched class",
   1381      propertyName: "color",
   1382      propertyValue: "light-dark(red, blue)",
   1383      isDarkColorScheme: true,
   1384      expected:
   1385        // prettier-ignore
   1386        `light-dark(` +
   1387        `<span data-color="red" class="color-swatch-container unmatched-class">` +
   1388          `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1389          `<span>red</span>` +
   1390        `</span>, ` +
   1391        `<span data-color="blue" class="color-swatch-container">` +
   1392          `<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1393          `<span>blue</span>` +
   1394        `</span>` +
   1395      `)`,
   1396    },
   1397    {
   1398      message: "light-dark gets parsed as expected in shorthands in light mode",
   1399      propertyName: "border",
   1400      propertyValue: "1px solid light-dark(red, blue)",
   1401      isDarkColorScheme: false,
   1402      expected:
   1403        // prettier-ignore
   1404        `1px solid light-dark(` +
   1405        `<span data-color="red" class="color-swatch-container">` +
   1406          `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1407          `<span>red</span>` +
   1408        `</span>, ` +
   1409        `<span data-color="blue" class="color-swatch-container unmatched-class">` +
   1410          `<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1411          `<span>blue</span>` +
   1412        `</span>` +
   1413      `)`,
   1414    },
   1415    {
   1416      message: "light-dark gets parsed as expected in shorthands in dark mode",
   1417      propertyName: "border",
   1418      propertyValue: "1px solid light-dark(red, blue)",
   1419      isDarkColorScheme: true,
   1420      expected:
   1421        // prettier-ignore
   1422        `1px solid light-dark(` +
   1423        `<span data-color="red" class="color-swatch-container unmatched-class">` +
   1424          `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1425          `<span>red</span>` +
   1426        `</span>, ` +
   1427        `<span data-color="blue" class="color-swatch-container">` +
   1428          `<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1429          `<span>blue</span>` +
   1430        `</span>` +
   1431      `)`,
   1432    },
   1433    {
   1434      message: "Nested light-dark gets parsed as expected in light mode",
   1435      propertyName: "background",
   1436      propertyValue:
   1437        "linear-gradient(45deg, light-dark(red, blue), light-dark(pink, cyan))",
   1438      isDarkColorScheme: false,
   1439      expected:
   1440        // prettier-ignore
   1441        `linear-gradient(` +
   1442          `<span data-angle="45deg"><span>45deg</span></span>, ` +
   1443          `light-dark(` +
   1444            `<span data-color="red" class="color-swatch-container">` +
   1445              `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>`+
   1446              `<span>red</span>`+
   1447            `</span>, `+
   1448            `<span data-color="blue" class="color-swatch-container unmatched-class">` +
   1449              `<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1450              `<span>blue</span>` +
   1451            `</span>` +
   1452          `), ` +
   1453          `light-dark(` +
   1454            `<span data-color="pink" class="color-swatch-container">` +
   1455              `<span class="test-class" style="background-color:pink" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1456              `<span>pink</span>` +
   1457            `</span>, ` +
   1458            `<span data-color="cyan" class="color-swatch-container unmatched-class">` +
   1459              `<span class="test-class" style="background-color:cyan" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1460              `<span>cyan</span>` +
   1461            `</span>` +
   1462          `)` +
   1463        `)`,
   1464    },
   1465    {
   1466      message: "Nested light-dark gets parsed as expected in dark mode",
   1467      propertyName: "background",
   1468      propertyValue:
   1469        "linear-gradient(33deg, light-dark(red, blue), light-dark(pink, cyan))",
   1470      isDarkColorScheme: true,
   1471      expected:
   1472        // prettier-ignore
   1473        `linear-gradient(` +
   1474          `<span data-angle="33deg"><span>33deg</span></span>, ` +
   1475          `light-dark(` +
   1476            `<span data-color="red" class="color-swatch-container unmatched-class">` +
   1477              `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>`+
   1478              `<span>red</span>`+
   1479            `</span>, `+
   1480            `<span data-color="blue" class="color-swatch-container">` +
   1481              `<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1482              `<span>blue</span>` +
   1483            `</span>` +
   1484          `), ` +
   1485          `light-dark(` +
   1486            `<span data-color="pink" class="color-swatch-container unmatched-class">` +
   1487              `<span class="test-class" style="background-color:pink" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1488              `<span>pink</span>` +
   1489            `</span>, ` +
   1490            `<span data-color="cyan" class="color-swatch-container">` +
   1491              `<span class="test-class" style="background-color:cyan" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1492              `<span>cyan</span>` +
   1493            `</span>` +
   1494          `)` +
   1495        `)`,
   1496    },
   1497    {
   1498      message:
   1499        "in light mode, the second parameter gets the unmatched class when it's a variable",
   1500      propertyName: "color",
   1501      propertyValue: "light-dark(var(--x), var(--y))",
   1502      isDarkColorScheme: false,
   1503      variables: { "--x": "red", "--y": "blue" },
   1504      expected:
   1505        // prettier-ignore
   1506        `light-dark(` +
   1507          `<span data-color="red" class="color-swatch-container">` +
   1508            `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1509            `<span>var(` +
   1510              `<span data-variable="red">--x${getJumpToVariableButton("--x")}</span>` +
   1511            `)</span>` +
   1512          `</span>, ` +
   1513          `<span data-color="blue" class="color-swatch-container unmatched-class">` +
   1514            `<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1515            `<span>var(` +
   1516              `<span data-variable="blue">--y${getJumpToVariableButton("--y")}</span>` +
   1517            `)</span>` +
   1518          `</span>` +
   1519        `)`,
   1520    },
   1521    {
   1522      message:
   1523        "in light mode, the second parameter gets the unmatched class when some param are not parsed",
   1524      propertyName: "color",
   1525      // Using `notacolor` so we don't get a wrapping Node for it (contrary to colors).
   1526      // The value is still valid at parse time since we're using a variable,
   1527      // so the OutputParser will actually parse the different parts
   1528      propertyValue: "light-dark(var(--x),notacolor)",
   1529      isDarkColorScheme: false,
   1530      variables: { "--x": "red" },
   1531      expected:
   1532        // prettier-ignore
   1533        `light-dark(` +
   1534          `<span data-color="red" class="color-swatch-container">` +
   1535            `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1536            `<span>` +
   1537              `var(<span data-variable="red">--x${getJumpToVariableButton("--x")}</span>)` +
   1538            `</span>` +
   1539          `</span>,` +
   1540          `<span class="unmatched-class">notacolor</span>` +
   1541        `)`,
   1542    },
   1543    {
   1544      message:
   1545        "in dark mode, the first parameter gets the unmatched class when some param are not parsed",
   1546      propertyName: "color",
   1547      // Using `notacolor` so we don't get a wrapping Node for it (contrary to colors).
   1548      // The value is still valid at parse time since we're using a variable,
   1549      // so the OutputParser will actually parse the different parts
   1550      propertyValue: "light-dark(notacolor,var(--x))",
   1551      isDarkColorScheme: true,
   1552      variables: { "--x": "red" },
   1553      expected:
   1554        // prettier-ignore
   1555        `light-dark(` +
   1556          `<span class="unmatched-class">notacolor</span>,` +
   1557          `<span data-color="red" class="color-swatch-container">` +
   1558            `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1559            `<span>` +
   1560              `var(<span data-variable="red">--x${getJumpToVariableButton("--x")}</span>)` +
   1561            `</span>` +
   1562          `</span>` +
   1563        `)`,
   1564    },
   1565    {
   1566      message:
   1567        "in light mode, the second parameter gets the unmatched class, comments are stripped out and whitespace are preserved",
   1568      propertyName: "color",
   1569      propertyValue:
   1570        "light-dark( /* 1st param */ var(--x) /* delim */ , /*  2nd param */ notacolor /* delim */ )",
   1571      isDarkColorScheme: false,
   1572      variables: { "--x": "red" },
   1573      expected:
   1574        // prettier-ignore
   1575        `light-dark(  ` +
   1576          `<span data-color="red" class="color-swatch-container">` +
   1577            `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1578            `<span>` +
   1579              `var(<span data-variable="red">--x${getJumpToVariableButton("--x")}</span>)` +
   1580            `</span>` +
   1581          `</span>  ,  ` +
   1582          `<span class="unmatched-class">notacolor</span>  ` +
   1583        `)`,
   1584    },
   1585    {
   1586      message:
   1587        "in dark mode, the first parameter gets the unmatched class, comments are stripped out and whitespace are preserved",
   1588      propertyName: "color",
   1589      propertyValue:
   1590        "light-dark( /* 1st param */ notacolor /* delim */ , /*  2nd param */ var(--x) /* delim */ )",
   1591      isDarkColorScheme: true,
   1592      variables: { "--x": "red" },
   1593      expected:
   1594        // prettier-ignore
   1595        `light-dark(  ` +
   1596          `<span class="unmatched-class">notacolor</span>  ,  ` +
   1597          `<span data-color="red" class="color-swatch-container">` +
   1598            `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1599            `<span>` +
   1600              `var(<span data-variable="red">--x${getJumpToVariableButton("--x")}</span>)` +
   1601            `</span>` +
   1602          `</span>  ` +
   1603        `)`,
   1604    },
   1605    {
   1606      message:
   1607        "in light mode with a single parameter, we don't strike through any parameter (TODO wrap with IACVT - Bug 1910845)",
   1608      propertyName: "color",
   1609      propertyValue: "light-dark(var(--x))",
   1610      isDarkColorScheme: false,
   1611      variables: { "--x": "red" },
   1612      expected:
   1613        // prettier-ignore
   1614        `light-dark(` +
   1615          `<span data-color="red" class="color-swatch-container">` +
   1616            `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1617            `<span>` +
   1618              `var(<span data-variable="red">--x${getJumpToVariableButton("--x")}</span>)` +
   1619            `</span>` +
   1620          `</span>` +
   1621        `)`,
   1622    },
   1623    {
   1624      message:
   1625        "in dark mode with a single parameter, we don't strike through any parameter (TODO wrap with IACVT - Bug 1910845)",
   1626      propertyName: "color",
   1627      propertyValue: "light-dark(var(--x))",
   1628      isDarkColorScheme: true,
   1629      variables: { "--x": "red" },
   1630      expected:
   1631        // prettier-ignore
   1632        `light-dark(` +
   1633          `<span data-color="red" class="color-swatch-container">` +
   1634            `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1635            `<span>` +
   1636              `var(<span data-variable="red">--x${getJumpToVariableButton("--x")}</span>)` +
   1637            `</span>` +
   1638          `</span>` +
   1639        `)`,
   1640    },
   1641    {
   1642      message:
   1643        "in light mode with 3 parameters, we don't strike through any parameter (TODO wrap with IACVT - Bug 1910845)",
   1644      propertyName: "color",
   1645      propertyValue: "light-dark(var(--x),a,b)",
   1646      isDarkColorScheme: false,
   1647      variables: { "--x": "red" },
   1648      expected:
   1649        // prettier-ignore
   1650        `light-dark(` +
   1651          `<span data-color="red" class="color-swatch-container">` +
   1652            `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1653            `<span>` +
   1654              `var(<span data-variable="red">--x${getJumpToVariableButton("--x")}</span>)` +
   1655            `</span>` +
   1656          `</span>,a,b` +
   1657        `)`,
   1658    },
   1659    {
   1660      message:
   1661        "in dark mode with 3 parameters, we don't strike through any parameter (TODO wrap with IACVT - Bug 1910845)",
   1662      propertyName: "color",
   1663      propertyValue: "light-dark(var(--x),a,b)",
   1664      isDarkColorScheme: true,
   1665      variables: { "--x": "red" },
   1666      expected:
   1667        // prettier-ignore
   1668        `light-dark(` +
   1669          `<span data-color="red" class="color-swatch-container">` +
   1670            `<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
   1671            `<span>` +
   1672              `var(<span data-variable="red">--x${getJumpToVariableButton("--x")}</span>)` +
   1673            `</span>` +
   1674          `</span>,a,b` +
   1675        `)`,
   1676    },
   1677  ];
   1678 
   1679  for (const test of TESTS) {
   1680    const frag = parser.parseCssProperty(
   1681      test.propertyName,
   1682      test.propertyValue,
   1683      {
   1684        isDarkColorScheme: test.isDarkColorScheme,
   1685        unmatchedClass: "unmatched-class",
   1686        colorSwatchClass: COLOR_TEST_CLASS,
   1687        getVariableData: varName => {
   1688          if (typeof test.variables[varName] === "string") {
   1689            return { value: test.variables[varName] };
   1690          }
   1691 
   1692          return test.variables[varName] || {};
   1693        },
   1694      }
   1695    );
   1696 
   1697    const target = doc.querySelector("div");
   1698    target.appendChild(frag);
   1699 
   1700    is(target.innerHTML, test.expected, test.message);
   1701    target.innerHTML = "";
   1702  }
   1703 }