/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; add_task(async function () { await SpecialPowers.pushPrefEnv({ set: [ ["dom.security.html_serialization_escape_lt_gt", true], ["layout.css.alpha-color-function.enabled", true], ["layout.css.attr.enabled", true], ["layout.css.backdrop-filter.enabled", true], ["layout.css.color-mix-multi-color.enabled", true], ["security.allow_unsafe_parent_loads", true], ], }); await addTab("about:blank"); await performTest(); gBrowser.removeCurrentTab(); }); async function performTest() { const OutputParser = require("resource://devtools/client/shared/output-parser.js"); const { host, doc } = await createHost( "bottom", "data:text/html," + "

browser_outputParser.js

" ); const cssProperties = getClientCssProperties(); const parser = new OutputParser(doc, cssProperties); testParseCssProperty(doc, parser); testParseCssVar(doc, parser); testParseURL(doc, parser); testParseFilter(doc, parser); testParseBackdropFilter(doc, parser); testParseAngle(doc, parser); testParseShape(doc, parser); testParseVariable(doc, parser); testParseColorVariable(doc, parser); testParseFontFamily(doc, parser); testParseLightDark(doc, parser); testParseAttr(doc, parser); testParseFunctionsForCssExplainers(doc, parser); host.destroy(); } // Class name used in color swatch. var COLOR_TEST_CLASS = "test-class"; // Create a new CSS color-parsing test. |name| is the name of the CSS // property. |value| is the CSS text to use. |segments| is an array // describing the expected result. If an element of |segments| is a // string, it is simply appended to the expected string. Otherwise, // it must be an object with a |name| property, which is the color // name as it appears in the input. // // This approach is taken to reduce boilerplate and to make it simpler // to modify the test when the parseCssProperty output changes. function makeColorTest(name, value, segments) { const result = { name, value, expected: "", }; for (const segment of segments) { if (typeof segment === "string") { result.expected += segment; } else { result.expected += getColorMarkup({ color: segment.name, colorFunction: segment.colorFunction, }); } } return result; } function getColorMarkup({ color, colorFunction, content }) { const buttonAttributes = { class: COLOR_TEST_CLASS, style: `background-color:${color}`, tabindex: 0, role: "button", }; if (colorFunction) { buttonAttributes["data-color-function"] = colorFunction; } const buttonAttrString = Object.entries(buttonAttributes) .map(([attr, v]) => `${attr}="${v}"`) .join(" "); // prettier-ignore return ( `` + `` + `${content ?? color}` + `` ); } function testParseCssProperty(doc, parser) { const tests = [ makeColorTest("border", "1px solid red", ["1px solid ", { name: "red" }]), makeColorTest( "background-image", "linear-gradient(to right, #F60 10%, rgba(0,0,0,1))", [ "linear-gradient(to right, ", { name: "#F60", colorFunction: "linear-gradient" }, " 10%, ", { name: "rgba(0,0,0,1)", colorFunction: "linear-gradient" }, ")", ] ), // In "arial black", "black" is a font, not a color. // (The font-family parser creates a span) makeColorTest("font-family", "arial black", ["arial black"]), makeColorTest("box-shadow", "0 0 1em red", ["0 0 1em ", { name: "red" }]), makeColorTest("box-shadow", "0 0 1em red, 2px 2px 0 0 rgba(0,0,0,.5)", [ "0 0 1em ", { name: "red" }, ", 2px 2px 0 0 ", { name: "rgba(0,0,0,.5)" }, ]), makeColorTest("content", '"red"', ['"red"']), // Invalid property names should not cause exceptions. makeColorTest("hellothere", "'red'", ["'red'"]), makeColorTest( "filter", "blur(1px) drop-shadow(0 0 0 blue) url(red.svg#blue)", [ '', "blur(1px) drop-shadow(0 0 0 ", { name: "blue", colorFunction: "drop-shadow" }, ") url(red.svg#blue)", ] ), makeColorTest("color", "currentColor", ["currentColor"]), // Test a very long property. makeColorTest( "background-image", "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%)", [ "linear-gradient(to left, ", { name: "transparent", colorFunction: "linear-gradient" }, " 0, ", { name: "transparent", colorFunction: "linear-gradient" }, " 5%,", { name: "#F00", colorFunction: "linear-gradient" }, " 0, ", { name: "#F00", colorFunction: "linear-gradient" }, " 10%,", { name: "#FF0", colorFunction: "linear-gradient" }, " 0, ", { name: "#FF0", colorFunction: "linear-gradient" }, " 15%,", { name: "#0F0", colorFunction: "linear-gradient" }, " 0, ", { name: "#0F0", colorFunction: "linear-gradient" }, " 20%,", { name: "#0FF", colorFunction: "linear-gradient" }, " 0, ", { name: "#0FF", colorFunction: "linear-gradient" }, " 25%,", { name: "#00F", colorFunction: "linear-gradient" }, " 0, ", { name: "#00F", colorFunction: "linear-gradient" }, " 30%,", { name: "#800", colorFunction: "linear-gradient" }, " 0, ", { name: "#800", colorFunction: "linear-gradient" }, " 35%,", { name: "#880", colorFunction: "linear-gradient" }, " 0, ", { name: "#880", colorFunction: "linear-gradient" }, " 40%,", { name: "#080", colorFunction: "linear-gradient" }, " 0, ", { name: "#080", colorFunction: "linear-gradient" }, " 45%,", { name: "#088", colorFunction: "linear-gradient" }, " 0, ", { name: "#088", colorFunction: "linear-gradient" }, " 50%,", { name: "#008", colorFunction: "linear-gradient" }, " 0, ", { name: "#008", colorFunction: "linear-gradient" }, " 55%,", { name: "#FFF", colorFunction: "linear-gradient" }, " 0, ", { name: "#FFF", colorFunction: "linear-gradient" }, " 60%,", { name: "#EEE", colorFunction: "linear-gradient" }, " 0, ", { name: "#EEE", colorFunction: "linear-gradient" }, " 65%,", { name: "#CCC", colorFunction: "linear-gradient" }, " 0, ", { name: "#CCC", colorFunction: "linear-gradient" }, " 70%,", { name: "#999", colorFunction: "linear-gradient" }, " 0, ", { name: "#999", colorFunction: "linear-gradient" }, " 75%,", { name: "#666", colorFunction: "linear-gradient" }, " 0, ", { name: "#666", colorFunction: "linear-gradient" }, " 80%,", { name: "#333", colorFunction: "linear-gradient" }, " 0, ", { name: "#333", colorFunction: "linear-gradient" }, " 85%,", { name: "#111", colorFunction: "linear-gradient" }, " 0, ", { name: "#111", colorFunction: "linear-gradient" }, " 90%,", { name: "#000", colorFunction: "linear-gradient" }, " 0, ", { name: "#000", colorFunction: "linear-gradient" }, " 95%,", { name: "transparent", colorFunction: "linear-gradient" }, " 0, ", { name: "transparent", colorFunction: "linear-gradient" }, " 100%)", ] ), // Note the lack of a space before the color here. makeColorTest("border", "1px dotted#f06", [ "1px dotted ", { name: "#f06" }, ]), { name: "color", value: "color-mix(in srgb, red, blue)", expected: getColorMarkup({ color: "color-mix(in srgb, red, blue)", content: `color-mix(in srgb, ` + // we have a nested color spans for the color-mix() params, `red` and `blue` getColorMarkup({ color: "red", colorFunction: "color-mix" }) + `, ` + getColorMarkup({ color: "blue", colorFunction: "color-mix" }) + `)`, }), }, { name: "background-image", value: "linear-gradient(to top, color-mix(in srgb, #008000, rgba(255, 255, 0, 0.9)), blue, contrast-color(#abc))", expected: `linear-gradient(to top, ` + // first we have a nested color span for resulting color of color-mix() getColorMarkup({ color: "color-mix(in srgb, #008000, rgba(255, 255, 0, 0.9))", colorFunction: "linear-gradient", content: `color-mix(in srgb, ` + // we have a nested color spans for the color-mix() params, `#008000` and `rgba(255, 255, 0, 0.9)` getColorMarkup({ color: "#008000", colorFunction: "color-mix" }) + `, ` + getColorMarkup({ color: "rgba(255, 255, 0, 0.9)", colorFunction: "color-mix", }) + // closing the `color-mix()` function `)`, }) + ", " + // second param for the gradient, `blue` getColorMarkup({ color: "blue", colorFunction: "linear-gradient", }) + ", " + // third param for the gradient, a `contrast-color()` getColorMarkup({ color: "contrast-color(#abc)", colorFunction: "linear-gradient", content: `contrast-color(` + getColorMarkup({ // we have a nested color spans for the contrast-color() param, `#abc` color: "#abc", colorFunction: "contrast-color", }) + `)`, }) + // closing the `linear-gradient()` function ")", }, makeColorTest("color", "light-dark(red, blue)", [ "light-dark(", { name: "red", colorFunction: "light-dark" }, ", ", { name: "blue", colorFunction: "light-dark" }, ")", ]), makeColorTest( "background-image", "linear-gradient(to top, light-dark(#008000, rgba(255, 255, 0, 0.9)), blue)", [ "linear-gradient(to top, ", "light-dark(", { name: "#008000", colorFunction: "light-dark" }, ", ", { name: "rgba(255, 255, 0, 0.9)", colorFunction: "light-dark" }, "), ", { name: "blue", colorFunction: "linear-gradient" }, ")", ] ), { name: "color", value: "rgb(from gold r g b)", expected: getColorMarkup({ color: "rgb(from gold r g b)", // we have a nested color span for the `gold` after `from` content: `rgb(from ${getColorMarkup({ color: "gold", colorFunction: "rgb" })} r g b)`, }), }, { name: "color", value: "color(from hsl(0 100% 50%) xyz x y 0.5)", expected: getColorMarkup({ color: "color(from hsl(0 100% 50%) xyz x y 0.5)", // we have a nested color span for the inner `hsl()` after `from` content: `color(from ` + getColorMarkup({ color: "hsl(0 100% 50%)", colorFunction: "color" }) + ` xyz x y 0.5)`, }), }, { name: "color", value: "oklab(from red calc(l - 1) calc(a * 2) calc(b + 3) / alpha)", expected: getColorMarkup({ color: "oklab(from red calc(l - 1) calc(a * 2) calc(b + 3) / alpha)", // we have a nested color span for the inner `red` after `from` content: `oklab(from ` + getColorMarkup({ color: "red", colorFunction: "oklab" }) + ` calc(l - 1) calc(a * 2) calc(b + 3) / alpha)`, }), }, { name: "color", value: "rgb(from color-mix(in lch, plum 40%, pink) r g b)", expected: getColorMarkup({ color: "rgb(from color-mix(in lch, plum 40%, pink) r g b)", content: `rgb(from ` + // we have a nested color span for the inner `color-mix()` after `from` getColorMarkup({ color: "color-mix(in lch, plum 40%, pink)", colorFunction: "rgb", content: `color-mix(in lch, ` + // and we have nested colors representing the color-mix color params (plum and pink) getColorMarkup({ color: "plum", colorFunction: "color-mix" }) + ` 40%, ` + getColorMarkup({ color: "pink", colorFunction: "color-mix" }) + `)`, }) + ` r g b)`, }), }, { name: "color", value: "rgb(from rgb(from gold r g b) r g b)", expected: getColorMarkup({ color: "rgb(from rgb(from gold r g b) r g b)", content: `rgb(from ` + // we have a nested color span for the inner `rgb()` after `from` getColorMarkup({ color: "rgb(from gold r g b)", colorFunction: "rgb", content: `rgb(from ` + // we have a nested color span for `gold` after `from` getColorMarkup({ color: "gold", colorFunction: "rgb" }) + ` r g b)`, }) + ` r g b)`, }), }, { name: "color", value: "alpha(from red / calc(alpha * 0.5))", expected: getColorMarkup({ color: "alpha(from red / calc(alpha * 0.5))", // we have a nested color span for the inner `red` after `from` content: `alpha(from ` + getColorMarkup({ color: "red", colorFunction: "alpha" }) + ` / calc(alpha * 0.5))`, }), }, { name: "background-image", value: `image(rgb(255 0 0 / 0.5)), url("bg-image.png")`, expected: `image(` + getColorMarkup({ color: "rgb(255 0 0 / 0.5)", colorFunction: "image", }) + `), url("bg-image.png")`, }, { name: "background-image", value: "linear-gradient(to right, #F60 10%, rgb(from gold r g b))", expected: `linear-gradient(to right, ` + getColorMarkup({ color: "#F60", colorFunction: "linear-gradient" }) + " 10%, " + getColorMarkup({ color: "rgb(from gold r g b)", colorFunction: "linear-gradient", content: `rgb(from ` + // nested color span for `gold` after `from` getColorMarkup({ color: "gold", colorFunction: "rgb", }) + " r g b)", }) + // closing linear-gradient() ")", }, { desc: "--a: (min-width:680px)", name: "--a", value: "(min-width:680px)", expected: "(min-width:680px)", }, { desc: "Interactive color swatch", name: "color", value: "gold", expected: // prettier-ignore `` + `` + `gold` + ``, parserExtraOptions: { colorSwatchReadOnly: false, }, }, { desc: "Read-only color swatch", name: "color", value: "gold", expected: // prettier-ignore `` + `` + `gold` + ``, parserExtraOptions: { colorSwatchReadOnly: true, }, }, { name: "color", value: "contrast-color(red)", expected: getColorMarkup({ color: "contrast-color(red)", content: "contrast-color(" + // color span for the `color()` param, `red` getColorMarkup({ color: "red", colorFunction: "contrast-color" }) + ")", }), }, { name: "color", value: "color-mix(in srgb, red, contrast-color(hsl(0 100 200)))", expected: getColorMarkup({ color: "color-mix(in srgb, red, contrast-color(hsl(0 100 200)))", content: "color-mix(in srgb, " + // color span for the `color-mix()` param, `red` getColorMarkup({ color: "red", colorFunction: "color-mix" }) + ", " + // color span for the resulting color of `contrast-color()` getColorMarkup({ color: "contrast-color(hsl(0 100 200))", colorFunction: "color-mix", content: "contrast-color(" + // color span for the `color()` param, `hsl(0 100 200)` getColorMarkup({ color: "hsl(0 100 200)", colorFunction: "contrast-color", }) + ")", }) + // closing `color-mix()` ")", }), }, { name: "color", value: "color-mix(in srgb, red, blue, green)", expected: getColorMarkup({ color: "color-mix(in srgb, red, blue, green)", content: "color-mix(in srgb, " + // color span for the first `color-mix()` param, `red` getColorMarkup({ color: "red", colorFunction: "color-mix" }) + ", " + // color span for the second `color-mix()` param, `blue` getColorMarkup({ color: "blue", colorFunction: "color-mix" }) + ", " + // color span for the third `color-mix()` param, `green` getColorMarkup({ color: "green", colorFunction: "color-mix" }) + // closing `color-mix() ")", }), }, { name: "color", value: "color-mix(in srgb, red)", expected: getColorMarkup({ color: "color-mix(in srgb, red)", content: "color-mix(in srgb, " + // color span for the first `color-mix()` param, `red` getColorMarkup({ color: "red", colorFunction: "color-mix" }) + // closing `color-mix() ")", }), }, { name: "color", value: "hsl(100 sibling-index() sibling-count())", // Check what happens without provided siblingCount/siblingIndex parserExtraOptions: { siblingCount: null, siblingIndex: null, }, expected: getColorMarkup({ color: "hsl(100 sibling-index() sibling-count())", }), }, { name: "color", value: "hsl(100 sibling-index() sibling-count())", parserExtraOptions: { siblingCount: 20, siblingIndex: 10, }, expected: getColorMarkup({ content: "hsl(100 sibling-index() sibling-count())", color: "hsl(100 10 20)", }), }, { name: "color", value: "oklch(from hsl(100 sibling-count() 50) l c calc(h * sibling-index()))", parserExtraOptions: { siblingCount: 11, siblingIndex: 7, }, expected: getColorMarkup({ color: "oklch(from hsl(100 11 50) l c calc(h * 7))", content: "oklch(from " + getColorMarkup({ color: "hsl(100 11 50)", colorFunction: "oklch", content: "hsl(100 sibling-count() 50)", }) + " l c calc(h * sibling-index()))", }), }, ]; const target = doc.querySelector("div"); ok(target, "captain, we have the div"); for (const test of tests) { info(`Testing "${test.name}: ${test.value}"`); const frag = parser.parseCssProperty(test.name, test.value, { colorSwatchClass: COLOR_TEST_CLASS, ...(test.parserExtraOptions || {}), }); target.appendChild(frag); is( target.innerHTML, test.expected, `CSS property correctly parsed for "${test.name}: ${test.value}"` ); target.innerHTML = ""; } } function testParseCssVar(doc, parser) { const frag = parser.parseCssProperty("color", "var(--some-kind-of-green)", { colorSwatchClass: "test-colorswatch", }); const target = doc.querySelector("div"); ok(target, "captain, we have the div"); target.appendChild(frag); is( target.innerHTML, "var(--some-kind-of-green)", "CSS property correctly parsed" ); target.innerHTML = ""; } function testParseURL(doc, parser) { info("Test that URL parsing preserves quoting style"); const tests = [ { desc: "simple test without quotes", leader: "url(", trailer: ")", }, { desc: "simple test with single quotes", leader: "url('", trailer: "')", }, { desc: "simple test with double quotes", leader: 'url("', trailer: '")', }, { desc: "test with single quotes and whitespace", leader: "url( \t'", trailer: "'\r\n\f)", }, { desc: "simple test with uppercase", leader: "URL(", trailer: ")", }, { desc: "bad url, missing paren", leader: "url(", trailer: "", expectedTrailer: ")", }, { desc: "bad url, missing paren, with baseURI", baseURI: "data:text/html,", leader: "url(", trailer: "", expectedTrailer: ")", }, { desc: "bad url, double quote, missing paren", leader: 'url("', trailer: '"', expectedTrailer: '")', }, { desc: "bad url, single quote, missing paren and quote", leader: "url('", trailer: "", expectedTrailer: "')", }, ]; const target = doc.querySelector("div"); for (const test of tests) { const url = test.leader + "something.jpg" + test.trailer; const frag = parser.parseCssProperty("background", url, { urlClass: "test-urlclass", baseURI: test.baseURI, }); target.replaceChildren(frag); const expectedTrailer = test.expectedTrailer || test.trailer; const expected = test.leader + 'something.jpg' + expectedTrailer; is(target.innerHTML, expected, test.desc); } info("Check that long URLs get the class for visual truncation"); const LONG_URL = `something.jpg?${"a".repeat(5000)}`; target.replaceChildren( parser.parseCssProperty("background", `url(${LONG_URL})`, { urlClass: "test-urlclass", }) ); is( target.innerHTML, // prettier-ignore `url(` + `` + LONG_URL + `` + `)`, "long url is wrapped in an element with a specific class" ); target.replaceChildren( parser.parseCssProperty("background", `url(${LONG_URL})`, {}) ); is( target.innerHTML, `url(${LONG_URL})`, "long url is wrapped in an element with a specific class, even when urlClass option is not set" ); target.innerHTML = ""; } function testParseFilter(doc, parser) { const frag = parser.parseCssProperty("filter", "something invalid", { filterSwatchClass: "test-filterswatch", }); const swatchCount = frag.querySelectorAll(".test-filterswatch").length; is(swatchCount, 1, "filter swatch was created"); } function testParseBackdropFilter(doc, parser) { const frag = parser.parseCssProperty("backdrop-filter", "something invalid", { filterSwatchClass: "test-filterswatch", }); const swatchCount = frag.querySelectorAll(".test-filterswatch").length; is(swatchCount, 1, "filter swatch was created for backdrop-filter"); } function testParseAngle(doc, parser) { let frag = parser.parseCssProperty("rotate", "90deg", { angleSwatchClass: "test-angleswatch", }); let swatchCount = frag.querySelectorAll(".test-angleswatch").length; is(swatchCount, 1, "angle swatch was created"); frag = parser.parseCssProperty( "background-image", "linear-gradient(90deg, red, blue", { angleSwatchClass: "test-angleswatch", } ); swatchCount = frag.querySelectorAll(".test-angleswatch").length; is(swatchCount, 1, "angle swatch was created"); } function testParseShape(doc, parser) { info("Test shape parsing"); const tests = [ { desc: "simple polygon()", definition: "polygon(0px 0px, 10px 10px, 10px 20px)", markup: // prettier-ignore `` + `` + `` + `polygon(` + `` + `0px` + ` ` + `0px` + `` + `, ` + `` + `10px` + ` ` + `10px` + `` + `, ` + `` + `10px` + ` ` + `20px` + `` + `)` + `` + ``, }, { desc: "simple polygon() with extra spaces", definition: "polygon( 0px 0px , 10px 10px , 10px 20px )", markup: // prettier-ignore `` + `` + `` + `polygon( ` + `` + `0px` + ` ` + `0px` + `` + ` , ` + `` + `10px` + ` ` + `10px` + `` + ` , ` + `` + `10px` + ` ` + `20px` + `` + ` )` + `` + ``, }, { desc: "polygon() with fill rule", definition: "polygon(nonzero, 0px 0px, 10px 10px, 10px 20px)", markup: // prettier-ignore `` + `` + `` + `polygon(nonzero, ` + `` + `0px` + ` ` + `0px` + `` + `, ` + `` + `10px` + ` ` + `10px` + `` + `, ` + `` + `10px` + ` ` + `20px` + `` + `)` + `` + ``, }, { desc: "complex polygon()", definition: "polygon(evenodd, 0px 0px, 10%200px,30%30% , calc(250px - 10px) 0 ,\n " + "12em var(--variable), 100% 100%) margin-box", markup: // prettier-ignore `` + `` + `` + `polygon(` + `evenodd, ` + `` + `0px` + ` ` + `0px` + `` + `, ` + `` + `10%` + `200px` + `` + `,` + `` + `30%` + `30%` + `` + ` , ` + `` + `calc(250px - 10px)` + ` ` + `0` + `` + ` ,\n ` + `` + `12em` + ` ` + `var(--variable)` + `` + `, ` + `` + `100%` + ` ` + `100%` + `` + `)` + `` + `` + ` margin-box`, }, { desc: "complex POLYGON()", definition: "POLYGON(evenodd, 0px 0px, 10%200px,30%30% , calc(250px - 10px) 0 ,\n " + "12em var(--variable), 100% 100%) margin-box", markup: // prettier-ignore `` + `` + `` + `POLYGON(` + `evenodd, ` + `` + `0px` + ` ` + `0px` + `` + `, ` + `` + `10%` + `200px` + `` + `,` + `` + `30%` + `30%` + `` + ` , ` + `` + `calc(250px - 10px)` + ` ` + `0` + `` + ` ,\n ` + `` + `12em` + ` ` + `var(--variable)` + `` + `, ` + `` + `100%` + ` ` + `100%` + `` + `)` + `` + `` + ` margin-box`, }, { desc: "Invalid polygon shape", definition: "polygon(0px 0px 100px 20px, 20% 20%)", markup: "polygon(0px 0px 100px 20px, 20% 20%)", }, { desc: "Circle shape with all arguments", definition: "circle(25% at\n 30% 200px) border-box", markup: // prettier-ignore `` + `` + `` + `circle(` + `25%` + ` at\n ` + `` + `30%` + ` ` + `200px` + `` + `)` + `` + `` + ` border-box`, }, { desc: "Circle shape with only one center", definition: "circle(25em at 40%)", markup: // prettier-ignore `` + `` + `` + `circle(` + `25em` + ` at ` + `` + `40%` + `` + `)` + `` + ``, }, { desc: "Circle shape with no radius", definition: "circle(at 30% 40%)", markup: // prettier-ignore `` + `` + `` + `circle(` + `at ` + `` + `30%` + ` ` + `40%` + `` + `)` + `` + ``, }, { desc: "Circle shape with no radius and keyword position", definition: "circle(at right center)", markup: // prettier-ignore `` + `` + `` + `circle(` + `at ` + `` + `right` + ` ` + `center` + `` + `)` + `` + ``, }, { desc: "Circle shape with no radius and 4 positions", definition: "circle(at left 10px top 15px)", markup: // prettier-ignore `` + `` + `` + `circle(` + `at ` + `` + `left` + ` ` + `10px` + ` ` + `top` + ` ` + `15px` + `` + `)` + `` + ``, }, { desc: "Circle shape with no center", definition: "circle(12em)", markup: // prettier-ignore `` + `` + `` + `circle(` + `12em` + `)` + `` + ``, }, { desc: "Circle shape with no center and keyword radius size", definition: "circle(farthest-side)", markup: // prettier-ignore `` + `` + `` + `circle(` + `farthest-side` + `)` + `` + ``, }, { desc: "Circle shape with no center and computed radius size", definition: "circle(calc(10% + 12em))", markup: // prettier-ignore `` + `` + `` + `circle(` + `calc(10% + 12em)` + `)` + `` + ``, }, { desc: "Circle shape with computed position", definition: "circle(25% at calc(30% + 1px) calc(200px - 10%))", markup: // prettier-ignore `` + `` + `` + `circle(` + `25%` + ` at ` + `` + `calc(30% + 1px)` + ` ` + `calc(200px - 10%)` + `` + `)` + `` + ``, }, { desc: "Circle shape with no arguments", definition: "circle()", markup: // prettier-ignore `` + `` + `` + `circle()` + `` + ``, }, { desc: "Circle shape with no space before at", definition: "circle(25%at 30% 30%)", markup: // prettier-ignore `` + `` + `` + `circle(` + `25%` + `at ` + `` + `30%` + ` ` + `30%` + `` + `)` + `` + ``, }, { desc: "CIRCLE", definition: "CIRCLE(12em)", markup: // prettier-ignore `` + `` + `` + `CIRCLE(` + `12em` + `)` + `` + ``, }, { desc: "Invalid circle shape", definition: "circle(25%at30%30%)", markup: "circle(25%at30%30%)", }, { desc: "Ellipse shape with all arguments", definition: "ellipse(200px 10em at 25% 120px) content-box", markup: // prettier-ignore `` + `` + `` + `ellipse(` + `200px` + ` ` + `10em` + ` at ` + `` + `25%` + ` ` + `120px` + `` + `)` + `` + `` + ` content-box`, }, { desc: "Ellipse shape with only one center", definition: "ellipse(200px 10% at 120px)", markup: // prettier-ignore `` + `` + `` + `ellipse(` + `200px` + ` ` + `10%` + ` at ` + `` + `120px` + `` + `)` + `` + ``, }, { desc: "Ellipse shape with no radius", definition: "ellipse(at 25% 120px)", markup: // prettier-ignore `` + `` + `` + `ellipse(` + `at ` + `` + `25%` + ` ` + `120px` + `` + `)` + `` + ``, }, { desc: "Ellipse shape with no center", definition: "ellipse(200px\n10em)", markup: // prettier-ignore `` + `` + `` + `ellipse(` + `200px` + `\n` + `10em` + `)` + `` + ``, }, { desc: "Ellipse shape with no center and keyword radii", definition: "ellipse(farthest-side closest-side)", markup: // prettier-ignore `` + `` + `` + `ellipse(` + `farthest-side` + ` ` + `closest-side` + `)` + `` + ``, }, { desc: "Ellipse shape with computed position and radiis", definition: "ellipse(calc(25% + 1px) calc(50% - 2px) at calc(30% + 1px) calc(200px - 10%))", markup: // prettier-ignore `` + `` + `` + `ellipse(` + `calc(25% + 1px)` + ` ` + `calc(50% - 2px)` + ` at ` + `` + `calc(30% + 1px)` + ` ` + `calc(200px - 10%)` + `` + `)` + `` + ``, }, { desc: "Ellipse shape with no arguments", definition: "ellipse()", markup: // prettier-ignore `` + `` + `` + `ellipse()` + `` + ``, }, { desc: "ELLIPSE()", definition: "ELLIPSE(200px 10em)", markup: // prettier-ignore `` + `` + `` + `ELLIPSE(` + `200px` + ` ` + `10em` + `)` + `` + ``, }, { desc: "Invalid ellipse shape", definition: "ellipse(200px100px at 30$ 20%)", markup: "ellipse(200px100px at 30$ 20%)", }, { desc: "Inset shape with 4 arguments", definition: "inset(200px 100px\n 30%15%)", markup: // prettier-ignore `` + `` + `` + `inset(` + `200px` + ` ` + `100px` + `\n ` + `30%` + `15%` + `)` + `` + ``, }, { desc: "Inset shape with 3 arguments", definition: "inset(200px 100px 15%)", markup: // prettier-ignore `` + `` + `` + `inset(` + `200px` + ` ` + `100px` + ` ` + `15%` + `)` + `` + ``, }, { desc: "Inset shape with 2 arguments", definition: "inset(200px 100px)", markup: // prettier-ignore `` + `` + `` + `inset(` + `200px` + ` ` + `100px` + `)` + `` + ``, }, { desc: "Inset shape with 1 argument", definition: "inset(200px)", markup: // prettier-ignore `` + `` + `` + `inset(` + `200px` + `)` + `` + ``, }, { desc: "Inset shape with 0 arguments", definition: "inset()", markup: "inset()", }, { desc: "INSET()", definition: "INSET(200px)", markup: // prettier-ignore `` + `` + `` + `INSET(` + `200px` + `)` + `` + ``, }, { desc: "offset-path property with inset shape value", property: "offset-path", definition: "inset(200px)", markup: // prettier-ignore `` + `` + `` + `inset(` + `200px` + `)` + `` + ``, }, { desc: "Inset shape with nested function", definition: "inset(200px calc(100px + 10%) 15%)", markup: // prettier-ignore `` + `` + `` + `inset(` + `200px` + ` ` + `calc(100px + 10%)` + ` ` + `15%` + `)` + `` + ``, }, { desc: "Inset shape with round keyword", definition: "inset(4rem round 1rem 2rem 3rem 4rem)", markup: // prettier-ignore `` + `` + `` + `inset(` + `4rem` + ` round 1rem 2rem 3rem 4rem` + `)` + `` + ``, }, ]; for (const { desc, definition, property = "clip-path", markup } of tests) { info(desc); const frag = parser.parseCssProperty(property, definition, { shapeClass: "inspector-shape", shapeSwatchClass: "inspector-shape-swatch", }); const el = frag.ownerDocument.createElement("span"); el.append(frag); is(el.innerHTML, markup, desc + " markup"); is(el.textContent, definition, desc + " text content"); const swatchlessFrag = parser.parseCssProperty(property, definition, { shapeClass: "inspector-shape", }); is( swatchlessFrag.querySelector("button"), null, `${desc} does not have a swatch button when shapeSwatchClass option is not passed` ); } } function getJumpToVariableButton(varName) { return ``; } function testParseVariable(doc, parser) { const TESTS = [ { text: "var(--seen)", variables: { "--seen": "chartreuse" }, expected: // prettier-ignore '' + "var(" + `--seen${getJumpToVariableButton("--seen")})` + "" + "", }, { text: "var(--seen)", variables: { "--seen": { value: "var(--base)", computedValue: "1em" }, }, expected: // prettier-ignore "var(" + `--seen${getJumpToVariableButton("--seen")})` + "", }, { text: "var(--not-seen)", variables: {}, expected: // prettier-ignore "var(" + '--not-seen' + ")", }, { text: "var(--seen, seagreen)", variables: { "--seen": "chartreuse" }, expected: // prettier-ignore '' + "var(" + `--seen${getJumpToVariableButton("--seen")}` + `,` + ' ' + '' + "seagreen" + "" + ")" + "" + "", }, { text: "var(--not-seen, var(--seen))", variables: { "--seen": "chartreuse" }, expected: // prettier-ignore `` + "var(" + '--not-seen'+ ',' + " " + '' + "var(" + `--seen${getJumpToVariableButton("--seen")})` + "" + "" + ")" + "" + "", }, { text: "color-mix(in srgb, var(--x), purple)", variables: { "--x": "yellow" }, expected: // prettier-ignore `` + `` + `` + `color-mix(in srgb, ` + `` + `` + `` + `var(--x${getJumpToVariableButton("--x")})` + `` + `, ` + `` + `` + `` + `purple` + `` + // closing `color-mix()` `)` + `` + ``, parserExtraOptions: { colorSwatchClass: COLOR_TEST_CLASS, }, }, { text: "light-dark(var(--light), var(--dark))", variables: { "--light": "yellow", "--dark": "gold" }, expected: // prettier-ignore `light-dark(` + `` + `` + `` + `var(--light${getJumpToVariableButton("--light")})` + `` + `, ` + `` + `` + `` + `var(--dark${getJumpToVariableButton("--dark")})` + `` + `)`, parserExtraOptions: { colorSwatchClass: COLOR_TEST_CLASS, }, }, { text: "1px solid var(--seen, seagreen)", // See Bug 1911974 skipVariableDeclarationTest: true, variables: { "--seen": "chartreuse" }, expected: // prettier-ignore '1px solid ' + '' + "var(" + `--seen${getJumpToVariableButton("--seen")}` + `,` + ' ' + '' + "seagreen" + "" + ")" + "" + "", }, { text: "1px solid var(--not-seen, seagreen)", // See Bug 1911975 skipVariableDeclarationTest: true, variables: {}, expected: // prettier-ignore `1px solid ` + `` + `var(` + `--not-seen` + `,` + ` ` + `` + `seagreen` + `` + `)` + `` + ``, }, { text: "rgba(var(--r), 0, 0, var(--a))", variables: { "--r": "255", "--a": "0.5" }, expected: // prettier-ignore '' + "rgba("+ "" + `var(--r${getJumpToVariableButton("--r")})` + ", 0, 0, " + "" + `var(--a${getJumpToVariableButton("--a")})` + "" + ")" + "", }, { text: "rgba(from var(--base) r g 0 / calc(var(--a) * 0.5))", variables: { "--base": "red", "--a": "0.8" }, expected: // prettier-ignore '' + "rgba("+ "from " + `` + "" + `var(--base${getJumpToVariableButton("--base")})` + "" + "" + " r g 0 / " + "calc(" + "" + `var(--a${getJumpToVariableButton("--a")})` + "" + " * 0.5)" + ")" + "", }, { text: "rgb(var(--not-seen, 255), 0, 0)", variables: {}, expected: // prettier-ignore '' + "rgb("+ "var(" + `--not-seen` + `,` + ` 255` + "), 0, 0" + ")" + "", }, { text: "rgb(var(--not-seen), 0, 0)", variables: {}, expected: // prettier-ignore `rgb(` + `` + `var(` + `` + `--not-seen` + `` + `)` + `` + `, 0, 0` + `)`, }, { text: "var(--registered)", variables: { "--registered": { value: "chartreuse", registeredProperty: { syntax: "", inherits: true, initialValue: "hotpink", }, }, }, expected: // prettier-ignore '' + "var(" + '--registered${getJumpToVariableButton("--registered")})` + "" + "", }, { text: "var(--registered-universal)", variables: { "--registered-universal": { value: "chartreuse", registeredProperty: { syntax: "*", inherits: false, }, }, }, expected: // prettier-ignore '' + "var(" + '--registered-universal${getJumpToVariableButton("--registered-universal")})` + "" + "", }, { text: "var(--x)", variables: { "--x": "light-dark(red, blue)", }, parserExtraOptions: { isDarkColorScheme: false, }, expected: `var(--x${getJumpToVariableButton("--x")})`, }, { text: "var(--x)", variables: { "--x": "color-mix(in srgb, red 50%, blue)", }, parserExtraOptions: { isDarkColorScheme: false, }, expected: // prettier-ignore '' + 'var(' + `--x${getJumpToVariableButton("--x")}` + ')' + '', }, { text: "var(--x)", variables: { "--x": "contrast-color(blue)", }, parserExtraOptions: { isDarkColorScheme: false, }, expected: // prettier-ignore '' + 'var(' + `--x${getJumpToVariableButton("--x")}` + ')' + '', }, { text: "var(--refers-empty)", variables: { "--refers-empty": { value: "var(--empty)", computedValue: "" }, }, expected: // prettier-ignore "var(" + `--refers-empty${getJumpToVariableButton("--refers-empty")})` + "", }, { text: "hsl(50, 70%, var(--foo))", variables: { "--foo": "40%", }, expected: // prettier-ignore `` + ``+ `hsl(50, 70%, ` + `` + `var(` + `--foo${getJumpToVariableButton("--foo")}` + `)` + `)` + `` + ``, }, { text: "var(--bar)", variables: { "--foo": "40%", "--bar": "hsl(50, 70%, var(--foo))", }, expected: // prettier-ignore `` + `` + `var(` + `--bar${getJumpToVariableButton("--bar")}` + `)` + `` + ``, }, { text: "var(--primary)", variables: { "--primary": "hsl(10, 100%, var(--fur))", "--fur": "var(--bar)", "--bar": "var(--foo)", "--foo": "50%", }, expected: // prettier-ignore `` + `` + `var(` + `--primary${getJumpToVariableButton("--primary")}` + `)` + `` + ``, }, { text: "oklch(var(--fur) 20 var(--boo))", variables: { "--fur": "var(--baz)", "--baz": "var(--foo)", "--foo": "10", "--boo": "30", }, expected: // prettier-ignore `` + `oklch(` + `` + `var(` + `--fur${getJumpToVariableButton("--fur")}` + `)` + `` + ` 20 ` + `` + `var(` + `--boo${getJumpToVariableButton("--boo")}` + `)` + `` + `)` + ``, }, { text: "var(--x)", variables: { "--x": "10px", }, parserExtraOptions: { showJumpToVariableButton: false, }, // This shouldn't have a Jump to variable button expected: `var(--x)`, }, { // var() with spaces between params/parenthesis text: "var( --foo , 500px )", variables: { "--foo": "1px" }, expected: // prettier-ignore `` + `var( ` + `--foo${getJumpToVariableButton("--foo")}` + ` ,` + ` 500px` + ` )` + ``, }, { // multiline var() text: "var(\n--foo, 500px\n)", variables: { "--foo": "1px" }, expected: // prettier-ignore `` + `var(\n` + `--foo${getJumpToVariableButton("--foo")}` + `,` + ` 500px` + `\n)` + ``, }, ]; const target = doc.querySelector("div"); const VAR_NAME_TO_DEFINE = "--test-parse-variable"; for (const test of TESTS) { // VAR_NAME_TO_DEFINE is used to test parsing the test.text if it's set for a // variable declaration, so it shouldn't be set in test.variables to avoid // messing with the test results. if (VAR_NAME_TO_DEFINE in test.variables) { throw new Error(`${VAR_NAME_TO_DEFINE} shouldn't be set in variables`); } // Also set the variable we're going to define, so its value can be computed as well const variables = { ...(test.variables || {}), [VAR_NAME_TO_DEFINE]: test.text, }; // Set the variables to an element so we can get their computed values for (const [varName, varData] of Object.entries(variables)) { doc.body.style.setProperty( varName, typeof varData === "string" ? varData : varData.value ); } const getVariableData = function (varName) { if (typeof variables[varName] === "string") { const value = variables[varName]; const computedValue = getComputedStyle(doc.body).getPropertyValue( varName ); return { value, computedValue }; } return variables[varName] || {}; }; const frag = parser.parseCssProperty("color", test.text, { getVariableData, unmatchedClass: "unmatched-class", ...(test.parserExtraOptions || {}), }); target.appendChild(frag); is( target.innerHTML, test.expected, `"color: ${test.text}" is parsed as expected` ); target.innerHTML = ""; if (test.skipVariableDeclarationTest) { continue; } const varFrag = parser.parseCssProperty( "--test-parse-variable", test.text, { getVariableData, unmatchedClass: "unmatched-class", ...(test.parserExtraOptions || {}), } ); target.appendChild(varFrag); is( target.innerHTML, test.expected, `"--test-parse-variable: ${test.text}" is parsed as expected` ); target.innerHTML = ""; // Remove the variables to an element so we can get their computed values for (const varName in variables || {}) { doc.body.style.removeProperty(varName); } } } function testParseColorVariable(doc, parser) { const testCategories = [ { desc: "Test for CSS variable defining color", tests: [ makeColorTest("--test-var", "lime", [{ name: "lime" }]), makeColorTest("--test-var", "#000", [{ name: "#000" }]), ], }, { desc: "Test for CSS variable not defining color", tests: [ makeColorTest("--foo", "something", ["something"]), makeColorTest("--bar", "Arial Black", ["Arial Black"]), makeColorTest("--baz", "10vmin", ["10vmin"]), ], }, { desc: "Test for non CSS variable defining color", tests: [ makeColorTest("non-css-variable", "lime", ["lime"]), makeColorTest("-non-css-variable", "#000", ["#000"]), ], }, ]; for (const category of testCategories) { info(category.desc); for (const test of category.tests) { info(test.desc); const target = doc.querySelector("div"); const frag = parser.parseCssProperty(test.name, test.value, { colorSwatchClass: COLOR_TEST_CLASS, }); target.appendChild(frag); is( target.innerHTML, test.expected, `The parsed result for '${test.name}: ${test.value}' is correct` ); target.innerHTML = ""; } } } function testParseFontFamily(doc, parser) { info("Test font-family parsing"); const tests = [ { desc: "No fonts", definition: "", families: [], }, { desc: "List of fonts", definition: "Arial,Helvetica,sans-serif", families: ["Arial", "Helvetica", "sans-serif"], }, { desc: "Fonts with spaces", definition: "Open Sans", families: ["Open Sans"], }, { desc: "Quoted fonts", definition: "\"Arial\",'Open Sans'", families: ["Arial", "Open Sans"], }, { desc: "Fonts with extra whitespace", definition: " Open Sans ", families: ["Open Sans"], }, ]; const textContentTests = [ { desc: "No whitespace between fonts", definition: "Arial,Helvetica,sans-serif", output: "Arial,Helvetica,sans-serif", }, { desc: "Whitespace between fonts", definition: "Arial , Helvetica, sans-serif", output: "Arial , Helvetica, sans-serif", }, { desc: "Whitespace before first font trimmed", definition: " Arial,Helvetica,sans-serif", output: "Arial,Helvetica,sans-serif", }, { desc: "Whitespace after last font trimmed", definition: "Arial,Helvetica,sans-serif ", output: "Arial,Helvetica,sans-serif", }, { desc: "Whitespace between quoted fonts", definition: "'Arial' , \"Helvetica\" ", output: "'Arial' , \"Helvetica\"", }, { desc: "Whitespace within font preserved", definition: "' Ari al '", output: "' Ari al '", }, ]; for (const { desc, definition, families } of tests) { info(desc); const frag = parser.parseCssProperty("font-family", definition, { fontFamilyClass: "ruleview-font-family", }); const spans = frag.querySelectorAll(".ruleview-font-family"); is(spans.length, families.length, desc + " span count"); for (let i = 0; i < spans.length; i++) { is(spans[i].textContent, families[i], desc + " span contents"); } } info("Test font-family text content"); for (const { desc, definition, output } of textContentTests) { info(desc); const frag = parser.parseCssProperty("font-family", definition, {}); is(frag.textContent, output, desc + " text content matches"); } info("Test font-family with custom properties"); const frag = parser.parseCssProperty( "font-family", "MonoLisa, var(--family, Georgia black, 'Helvetica Neue', serif), monospace !important", { getVariableData: () => ({}), unmatchedClass: "unmatched-class", fontFamilyClass: "ruleview-font-family", } ); const target = doc.createElement("div"); target.appendChild(frag); is( target.innerHTML, // prettier-ignore `MonoLisa` + `, ` + `var(` + `` + `--family` + `` + `,` + ` ` + `Georgia black` + `, ` + `'Helvetica Neue'` + `, ` + `serif` + `)` + `` + `, ` + `monospace` + ` !important`, "Got expected output for font-family with custom properties" ); } function testParseLightDark(doc, parser) { const TESTS = [ { message: "Not passing isDarkColorScheme doesn't add unmatched classes to parameters", propertyName: "color", propertyValue: "light-dark(red, blue)", expected: // prettier-ignore `light-dark(` + `` + `` + `red` + `, ` + `` + `` + `blue` + `` + `)`, }, { message: "in light mode, the second parameter gets the unmatched class", propertyName: "color", propertyValue: "light-dark(red, blue)", isDarkColorScheme: false, expected: // prettier-ignore `light-dark(` + `` + `` + `red` + `, ` + `` + `` + `blue` + `` + `)`, }, { message: "in dark mode, the first parameter gets the unmatched class", propertyName: "color", propertyValue: "light-dark(red, blue)", isDarkColorScheme: true, expected: // prettier-ignore `light-dark(` + `` + `` + `red` + `, ` + `` + `` + `blue` + `` + `)`, }, { message: "light-dark gets parsed as expected in shorthands in light mode", propertyName: "border", propertyValue: "1px solid light-dark(red, blue)", isDarkColorScheme: false, expected: // prettier-ignore `1px solid light-dark(` + `` + `` + `red` + `, ` + `` + `` + `blue` + `` + `)`, }, { message: "light-dark gets parsed as expected in shorthands in dark mode", propertyName: "border", propertyValue: "1px solid light-dark(red, blue)", isDarkColorScheme: true, expected: // prettier-ignore `1px solid light-dark(` + `` + `` + `red` + `, ` + `` + `` + `blue` + `` + `)`, }, { message: "Nested light-dark gets parsed as expected in light mode", propertyName: "background", propertyValue: "linear-gradient(45deg, light-dark(red, blue), light-dark(pink, cyan))", isDarkColorScheme: false, expected: // prettier-ignore `linear-gradient(` + `45deg, ` + `light-dark(` + `` + ``+ `red`+ `, `+ `` + `` + `blue` + `` + `), ` + `light-dark(` + `` + `` + `pink` + `, ` + `` + `` + `cyan` + `` + `)` + `)`, }, { message: "Nested light-dark gets parsed as expected in dark mode", propertyName: "background", propertyValue: "linear-gradient(33deg, light-dark(red, blue), light-dark(pink, cyan))", isDarkColorScheme: true, expected: // prettier-ignore `linear-gradient(` + `33deg, ` + `light-dark(` + `` + ``+ `red`+ `, `+ `` + `` + `blue` + `` + `), ` + `light-dark(` + `` + `` + `pink` + `, ` + `` + `` + `cyan` + `` + `)` + `)`, }, { message: "in light mode, the second parameter gets the unmatched class when it's a variable", propertyName: "color", propertyValue: "light-dark(var(--x), var(--y))", isDarkColorScheme: false, variables: { "--x": "red", "--y": "blue" }, expected: // prettier-ignore `light-dark(` + `` + `` + `var(` + `--x${getJumpToVariableButton("--x")}` + `)` + `, ` + `` + `` + `var(` + `--y${getJumpToVariableButton("--y")}` + `)` + `` + `)`, }, { message: "in light mode, the second parameter gets the unmatched class when some param are not parsed", propertyName: "color", // Using `notacolor` so we don't get a wrapping Node for it (contrary to colors). // The value is still valid at parse time since we're using a variable, // so the OutputParser will actually parse the different parts propertyValue: "light-dark(var(--x),notacolor)", isDarkColorScheme: false, variables: { "--x": "red" }, expected: // prettier-ignore `light-dark(` + `` + `` + `` + `var(--x${getJumpToVariableButton("--x")})` + `` + `,` + `notacolor` + `)`, }, { message: "in dark mode, the first parameter gets the unmatched class when some param are not parsed", propertyName: "color", // Using `notacolor` so we don't get a wrapping Node for it (contrary to colors). // The value is still valid at parse time since we're using a variable, // so the OutputParser will actually parse the different parts propertyValue: "light-dark(notacolor,var(--x))", isDarkColorScheme: true, variables: { "--x": "red" }, expected: // prettier-ignore `light-dark(` + `notacolor,` + `` + `` + `` + `var(--x${getJumpToVariableButton("--x")})` + `` + `` + `)`, }, { message: "in light mode, the second parameter gets the unmatched class, comments are stripped out and whitespace are preserved", propertyName: "color", propertyValue: "light-dark( /* 1st param */ var(--x) /* delim */ , /* 2nd param */ notacolor /* delim */ )", isDarkColorScheme: false, variables: { "--x": "red" }, expected: // prettier-ignore `light-dark( ` + `` + `` + `` + `var(--x${getJumpToVariableButton("--x")})` + `` + ` , ` + `notacolor ` + `)`, }, { message: "in dark mode, the first parameter gets the unmatched class, comments are stripped out and whitespace are preserved", propertyName: "color", propertyValue: "light-dark( /* 1st param */ notacolor /* delim */ , /* 2nd param */ var(--x) /* delim */ )", isDarkColorScheme: true, variables: { "--x": "red" }, expected: // prettier-ignore `light-dark( ` + `notacolor , ` + `` + `` + `` + `var(--x${getJumpToVariableButton("--x")})` + `` + ` ` + `)`, }, { message: "in light mode with a single parameter, we don't strike through any parameter (TODO wrap with IACVT - Bug 1910845)", propertyName: "color", propertyValue: "light-dark(var(--x))", isDarkColorScheme: false, variables: { "--x": "red" }, expected: // prettier-ignore `light-dark(` + `` + `` + `` + `var(--x${getJumpToVariableButton("--x")})` + `` + `` + `)`, }, { message: "in dark mode with a single parameter, we don't strike through any parameter (TODO wrap with IACVT - Bug 1910845)", propertyName: "color", propertyValue: "light-dark(var(--x))", isDarkColorScheme: true, variables: { "--x": "red" }, expected: // prettier-ignore `light-dark(` + `` + `` + `` + `var(--x${getJumpToVariableButton("--x")})` + `` + `` + `)`, }, { message: "in light mode with 3 parameters, we don't strike through any parameter (TODO wrap with IACVT - Bug 1910845)", propertyName: "color", propertyValue: "light-dark(var(--x),a,b)", isDarkColorScheme: false, variables: { "--x": "red" }, expected: // prettier-ignore `light-dark(` + `` + `` + `` + `var(--x${getJumpToVariableButton("--x")})` + `` + `,a,b` + `)`, }, { message: "in dark mode with 3 parameters, we don't strike through any parameter (TODO wrap with IACVT - Bug 1910845)", propertyName: "color", propertyValue: "light-dark(var(--x),a,b)", isDarkColorScheme: true, variables: { "--x": "red" }, expected: // prettier-ignore `light-dark(` + `` + `` + `` + `var(--x${getJumpToVariableButton("--x")})` + `` + `,a,b` + `)`, }, { message: "in light mode with images, the second parameter gets the unmatched class", propertyName: "background-image", propertyValue: `light-dark(url("a.png"), url("b.png"))`, isDarkColorScheme: false, expected: `light-dark(url("a.png"), url("b.png"))`, }, { message: "in dark mode with images, the first parameter gets the unmatched class", propertyName: "background-image", propertyValue: `light-dark(url("a.png"), url("b.png"))`, isDarkColorScheme: true, expected: `light-dark(url("a.png"), url("b.png"))`, }, { message: "in light mode with gradients, the second parameter gets the unmatched class", propertyName: "background-image", propertyValue: "light-dark(linear-gradient(white, blue), linear-gradient(black, red))", isDarkColorScheme: false, expected: // prettier-ignore `light-dark(` + `linear-gradient(` + `` + `` + `white` + `` + `, ` + `` + `` + `blue` + `` + `)` + `, ` + `` + `linear-gradient(` + `` + `` + `black` + `` + `, ` + `` + `` + `red` + `` + `)` + `` + `)`, }, { message: "in dark mode with gradients, the first parameter gets the unmatched class", propertyName: "background-image", propertyValue: "light-dark(linear-gradient(white, blue), linear-gradient(black, red))", isDarkColorScheme: true, expected: // prettier-ignore `light-dark(` + `` + `linear-gradient(` + `` + `` + `white` + `` + `, ` + `` + `` + `blue` + `` + `)` + `` + `, ` + `linear-gradient(` + `` + `` + `black` + `` + `, ` + `` + `` + `red` + `` + `)` + `)`, }, ]; for (const test of TESTS) { const frag = parser.parseCssProperty( test.propertyName, test.propertyValue, { isDarkColorScheme: test.isDarkColorScheme, unmatchedClass: "unmatched-class", colorSwatchClass: COLOR_TEST_CLASS, getVariableData: varName => { if (typeof test.variables[varName] === "string") { return { value: test.variables[varName] }; } return test.variables[varName] || {}; }, } ); const target = doc.querySelector("div"); target.appendChild(frag); is(target.innerHTML, test.expected, test.message); target.innerHTML = ""; } } function testParseAttr(doc, parser) { const TESTS = [ { message: "Not passing getAttributeValue doesn't add unmatched classes to attribute name", propertyValue: "attr(unknown)", attributes: {}, getAttributeValue: null, expected: `attr(unknown)`, }, { message: "Passing known attribute doesn't add unmatched classes to attribute name", propertyValue: "attr(data-x)", attributes: { "data-x": "" }, // prettier-ignore expected: `attr(` + `` + `data-x` + `` + `)`, }, { message: "Passing unknown attribute adds unmatched classes to attribute name", propertyValue: "attr(data-x)", attributes: {}, // prettier-ignore expected: `attr(` + `` + `data-x` + `` + `)`, }, { message: "Passing unknown attribute adds unmatched classes to attribute name, not to fallback", propertyValue: `attr(data-x, "fallback")`, attributes: {}, // prettier-ignore expected: `attr(` + `` + `data-x` + `` + `, ` + `"fallback"` + `)`, }, { message: "Passing known attribute adds unmatched classes to fallback", propertyValue: `attr(data-x, "fallback")`, attributes: { "data-x": "" }, // prettier-ignore expected: `attr(` + `` + `data-x` + `` + `, ` + `"fallback"` + `)`, }, { message: "Checking attr() + spaces", propertyValue: `attr( data-x , "fallback" )`, attributes: { "data-x": "" }, // prettier-ignore expected: `attr(` + ` ` + `` + `data-x` + `` + ` , ` + `"fallback"` + ` )`, }, { message: "Modern attr() with known attribute and simple type", propertyValue: "attr(data-x raw-string)", attributes: { "data-x": "x" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` raw-string` + `` + `)`, }, { message: "Modern attr() with known attribute and type()", propertyName: "width", propertyValue: "attr(data-x type( | ))", attributes: { "data-x": "10px" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` type(<length> | <percentage>)` + `` + `)`, }, { message: "Modern attr() with unknown attribute and simple type", propertyValue: "attr(data-x raw-string)", attributes: {}, // prettier-ignore expected: `attr(` + `` + `data-x` + ` raw-string` + `` + `)`, }, { message: "Modern attr() with unknown attribute and type()", propertyValue: "attr(data-x type( | ))", attributes: {}, // prettier-ignore expected: `attr(` + `` + `data-x` + ` type(<length> | <percentage>)` + `` + `)`, }, { message: "Modern attr() with known attribute, simple type and simple fallback", propertyValue: `attr(data-x raw-string, "fallback")`, attributes: { "data-x": "12" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` raw-string` + `` + `, ` + `"fallback"` + `)`, }, { message: "Modern attr() with known attribute, type() and simple fallback", propertyName: "width", propertyValue: `attr(data-x type( | ), "fallback")`, attributes: { "data-x": "10%" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` type(<length> | <percentage>)` + `` + `, ` + `"fallback"` + `)`, }, { message: "Modern attr() with unknown attribute, simple type and simple fallback", propertyValue: `attr(data-x raw-string, "fallback")`, attributes: {}, // prettier-ignore expected: `attr(` + `` + `data-x` + ` raw-string` + `` + `, ` + `"fallback"` + `)`, }, { message: "Modern attr() with unknown attribute, type() and simple fallback", propertyValue: `attr(data-x type( | ), "fallback")`, attributes: {}, // prettier-ignore expected: `attr(` + `` + `data-x` + ` type(<length> | <percentage>)` + `` + `, ` + `"fallback"` + `)`, }, { message: "Modern attr() with known attribute, simple type and nested attr() fallback", propertyValue: `attr(data-x raw-string, attr(data-y, "fallback"))`, attributes: { "data-x": "x", "data-y": "y" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` raw-string` + `` + `, ` + `` + `attr(` + `` + `data-y` + `` + `, ` + `"fallback"` + `)` + `` + `)`, }, { message: "Modern attr() with known attribute, type() and nested attr() fallback", propertyValue: `attr(data-x type( | ), attr(data-y, 300px))`, attributes: { "data-x": "20em" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` type(<length> | <percentage>)` + `` + `, ` + `` + `attr(` + `` + `data-y` + `` + `, ` + `300px` + `)` + `` + `)`, }, { message: "Modern attr() with unknown attribute, simple type and nested attr() fallback", propertyValue: `attr(data-x raw-string, attr(data-y, "fallback"))`, attributes: { "data-y": "y" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` raw-string` + `` + `, ` + `` + `attr(` + `` + `data-y` + `` + `, ` + `"fallback"` + `)` + `` + `)`, }, { message: "Modern attr() with unknown attribute, type() and nested attr() fallback", propertyValue: `attr(data-x type( | ), attr(data-y, 99%))`, attributes: {}, // prettier-ignore expected: `attr(` + `` + `data-x` + ` type(<length> | <percentage>)` + `` + `, ` + `` + `attr(` + `` + `data-y` + `` + `, ` + `99%` + `)` + `` + `)`, }, { message: "Modern attr() with `number` attr type and value matching type", propertyName: "line-height", propertyValue: `attr(data-x number, 2)`, attributes: { "data-x": "3" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` number` + `` + `, ` + `2` + `)`, }, { message: "Modern attr() with `number` attr type and value not matching type", propertyName: "line-height", propertyValue: `attr(data-x number, 2)`, attributes: { "data-x": "x" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` number` + `` + `, ` + `2` + `)`, }, { message: "Modern attr() with `deg` attr type and value matching type", propertyName: "rotate", propertyValue: `attr(data-x deg, 90deg)`, attributes: { "data-x": "1.5" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` deg` + `` + `, ` + `` + `90deg` + `` + `)`, }, { message: "Modern attr() with `deg` attr type and value not matching type", propertyName: "rotate", propertyValue: `attr(data-x deg, 90deg)`, attributes: { "data-x": "x" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` deg` + `` + `, ` + `` + `90deg` + `` + `)`, }, { message: "Modern attr() with `%` attr type and value matching type", propertyName: "height", propertyValue: `attr(data-x %, 50%)`, attributes: { "data-x": "99" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` %` + `` + `, ` + `50%` + `)`, }, { message: "Modern attr() with `%` attr type and value not matching type", propertyName: "height", propertyValue: `attr(data-x %, 50%)`, attributes: { "data-x": "20%" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` %` + `` + `, ` + `50%` + `)`, }, { message: "Modern attr() with `type()` attr type and value matching type", propertyName: "color", propertyValue: `attr(data-x type(), hotpink)`, attributes: { "data-x": "tomato" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` type(<color>)` + `` + `, ` + `` + `hotpink` + `` + `)`, }, { message: "Modern attr() with `type()` attr type and value not matching type", propertyName: "color", propertyValue: `attr(data-x type(), hotpink)`, attributes: { "data-x": "uwu" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` type(<color>)` + `` + `, ` + `` + `hotpink` + `` + `)`, }, { message: "Modern attr() with `type( | )` attr type and a length value", propertyName: "width", propertyValue: `attr(data-x type( | ), 33px)`, attributes: { "data-x": "42em" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` type(<length> | <percentage>)` + `` + `, ` + `33px` + `)`, }, { message: "Modern attr() with `type( | )` attr type and a percentage value", propertyName: "width", propertyValue: `attr(data-x type( | ), 33px)`, attributes: { "data-x": "42%" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` type(<length> | <percentage>)` + `` + `, ` + `33px` + `)`, }, { message: "Modern attr() with `type( | )` attr type and value not matching type", propertyName: "width", propertyValue: `attr(data-x type( | ), 33px)`, attributes: { "data-x": "42" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` type(<length> | <percentage>)` + `` + `, ` + `33px` + `)`, }, { message: "Modern attr() with `type()` attr type", propertyName: "position-anchor", propertyValue: `attr(data-x type(), match-parent)`, attributes: { "data-x": "--my-anchor" }, // prettier-ignore expected: `attr(` + `` + `data-x` + ` type(<custom-ident>)` + `` + `, ` + `match-parent` + `)`, }, ]; for (const test of TESTS) { const frag = parser.parseCssProperty( test.propertyName || "content", test.propertyValue, { unmatchedClass: "unmatched-class", getAttributeValue: "getAttributeValue" in test ? test.getAttributeValue : attrName => test.attributes[attrName] ?? null, } ); const target = doc.querySelector("div"); target.appendChild(frag); is(target.innerHTML, test.expected, test.message); target.innerHTML = ""; } } function testParseFunctionsForCssExplainers(doc, parser) { const TESTS = [ { message: "No custom elements and attributes when cssExplainersEnabled is false", propertyName: "width", propertyValue: "calc(10px + 1em)", cssExplainersEnabled: false, expected: `calc(10px + 1em)`, }, { message: "No custom elements and attributes when parsing a function that isn't supported", propertyName: "content", propertyValue: "counter(count, decimal)", cssExplainersEnabled: true, expected: `counter(count, decimal)`, }, { message: "Custom elements and attributes when parsing calc()", propertyName: "width", propertyValue: "calc(10px + 1em)", cssExplainersEnabled: true, // prettier-ignore expected: `` + `calc` + `(10px + 1em)` + ``, }, { message: "Custom elements and attributes when parsing nested functions", propertyName: "transform", propertyValue: "translateY(calc(10px + round(up, sin(40deg)) * 1px))", cssExplainersEnabled: true, // prettier-ignore expected: `translateY(` + `` + `calc` + `(` + `10px + ` + `` + `round` + `(` + `up, ` + `` + `sin` + `(` + `` + `40deg` + `` + `)` + `` + `)` + `` + ` * 1px` + `)` + `` + `)`, }, { message: "Custom elements and attributes when using var() and attr()", propertyName: "width", propertyValue: "calc(var(--x, attr(data-x px, 16px)))", cssExplainersEnabled: true, attributes: { "data-x": "20" }, // prettier-ignore expected: `` + `calc` + `(` + `` + `` + `var` + `(` + `--x` + `,` + `` + ` ` + `` + `attr` + `(` + `` + `data-x` + ` px` + `` + `, ` + `16px` + `)` + `` + `` + `)` + `` + `` + `)` + ``, }, { message: "Custom elements and attributes when using parenthesis block", propertyName: "opacity", propertyValue: "calc(1 - (var(--x) - 0.8rem))", cssExplainersEnabled: true, variables: { "--x": "2em" }, expected: // prettier-ignore `` + `calc` + `(` + `1 - (` + `` + `` + `var` + `(` + `` + `--x` + `` + `` + `)` + `` + `` + ` - 0.8rem` + `)` + `)` + ``, }, { message: "No data-function-expression attribute when a nested function isn't supported", propertyName: "width", propertyValue: "calc(10px + anchor-size(--my-anchor width, calc(50% + 10vw)))", cssExplainersEnabled: true, // prettier-ignore expected: `calc(` + `10px + ` + `anchor-size(` + `--my-anchor width, ` + `` + `calc` + `(50% + 10vw)` + `` + `)` + `)`, }, ]; const target = doc.querySelector("div"); for (const test of TESTS) { const frag = parser.parseCssProperty( test.propertyName, test.propertyValue, { cssExplainersEnabled: test.cssExplainersEnabled, getAttributeValue: "getAttributeValue" in test ? test.getAttributeValue : attrName => test.attributes[attrName] ?? null, getVariableData: varName => { if (typeof test.variables?.[varName] === "string") { return { value: test.variables[varName] }; } return test.variables?.[varName] || {}; }, } ); target.replaceChildren(frag); is(target.innerHTML, test.expected, test.message); } target.innerHTML = ""; }