Files
webstudio/packages/css-data/src/parse-css.test.ts
T
Oleg Isonen da7e9cdce7 experimental: support nested and attribute selectors (#4213)
## Description

Needed to support all wf presets for fixing this
https://github.com/webstudio-is/webstudio/pull/4155

## Steps for reproduction

1. click button
2. expect xyz

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
5de6)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file
2024-10-04 17:23:09 +02:00

738 lines
17 KiB
TypeScript

import { describe, expect, test } from "@jest/globals";
import { parseCss, parseMediaQuery } from "./parse-css";
describe("Parse CSS", () => {
test("longhand property name with keyword value", () => {
expect(parseCss(`.test { background-color: red }`)).toEqual([
{
selector: ".test",
property: "backgroundColor",
value: { type: "keyword", value: "red" },
},
]);
});
test("one class selector rules", () => {
expect(parseCss(`.test { color: #ff0000 }`)).toEqual([
{
selector: ".test",
property: "color",
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
},
]);
});
test("parse supported shorthand values", () => {
const css = `
.test {
background: #ff0000 linear-gradient(180deg, #11181C 0%, rgba(17, 24, 28, 0) 36.09%), #EBFFFC;
}
`;
expect(parseCss(css)).toEqual([
{
selector: ".test",
property: "backgroundImage",
value: {
type: "layers",
value: [
{
type: "unparsed",
value:
"linear-gradient(180deg,#11181C 0%,rgba(17,24,28,0) 36.09%)",
},
{ type: "keyword", value: "none" },
],
},
},
{
selector: ".test",
property: "backgroundPositionX",
value: {
type: "layers",
value: [
{ type: "unit", unit: "%", value: 0 },
{ type: "unit", unit: "%", value: 0 },
],
},
},
{
selector: ".test",
property: "backgroundPositionY",
value: {
type: "layers",
value: [
{ type: "unit", unit: "%", value: 0 },
{ type: "unit", unit: "%", value: 0 },
],
},
},
{
selector: ".test",
property: "backgroundSize",
value: {
type: "layers",
value: [
{
type: "tuple",
value: [
{ type: "keyword", value: "auto" },
{ type: "keyword", value: "auto" },
],
},
{
type: "tuple",
value: [
{ type: "keyword", value: "auto" },
{ type: "keyword", value: "auto" },
],
},
],
},
},
{
selector: ".test",
property: "backgroundRepeat",
value: {
type: "layers",
value: [
{ type: "keyword", value: "repeat" },
{ type: "keyword", value: "repeat" },
],
},
},
{
selector: ".test",
property: "backgroundAttachment",
value: {
type: "layers",
value: [
{ type: "keyword", value: "scroll" },
{ type: "keyword", value: "scroll" },
],
},
},
{
selector: ".test",
property: "backgroundOrigin",
value: {
type: "layers",
value: [
{ type: "keyword", value: "padding-box" },
{ type: "keyword", value: "padding-box" },
],
},
},
{
selector: ".test",
property: "backgroundClip",
value: {
type: "layers",
value: [
{ type: "keyword", value: "border-box" },
{ type: "keyword", value: "border-box" },
],
},
},
{
selector: ".test",
property: "backgroundColor",
value: { alpha: 1, b: 252, g: 255, r: 235, type: "rgb" },
},
]);
});
test("parses single layer", () => {
const css = `
.test {
background-image: none; background-position: 0px 0px; background-size: auto;
}
`;
expect(parseCss(css)).toEqual([
{
selector: ".test",
property: "backgroundImage",
value: {
type: "layers",
value: [{ type: "keyword", value: "none" }],
},
},
{
selector: ".test",
property: "backgroundPositionX",
value: {
type: "layers",
value: [{ type: "unit", unit: "px", value: 0 }],
},
},
{
selector: ".test",
property: "backgroundPositionY",
value: {
type: "layers",
value: [{ type: "unit", unit: "px", value: 0 }],
},
},
{
selector: ".test",
property: "backgroundSize",
value: {
type: "layers",
value: [{ type: "keyword", value: "auto" }],
},
},
]);
});
test("parse state", () => {
expect(parseCss(`a:hover { color: #ff0000 }`)).toEqual([
{
selector: "a",
state: ":hover",
property: "color",
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
},
]);
});
test("attribute selector", () => {
expect(parseCss(`[class^="a"] { color: #ff0000 }`)).toEqual([
{
selector: '[class^="a"]',
property: "color",
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
},
]);
});
test("parse first pseudo class as selector", () => {
// E.g. :root
expect(parseCss(`:first-pseudo:my-state { color: #ff0000 }`)).toEqual([
{
selector: ":first-pseudo",
state: ":my-state",
property: "color",
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
},
]);
});
test("parse pseudo element", () => {
expect(parseCss(`input::placeholder { color: #ff0000 }`)).toEqual([
{
selector: "input",
state: "::placeholder",
property: "color",
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
},
]);
});
test("parse multiple selectors, one with state", () => {
expect(parseCss(`a, a:hover { color: #ff0000 }`)).toEqual([
{
selector: "a",
property: "color",
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
},
{
selector: "a",
state: ":hover",
property: "color",
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
},
]);
});
test("parse multiple selectors, both with state", () => {
expect(parseCss(`a:active, a:hover { color: #ff0000 }`)).toEqual([
{
selector: "a",
state: ":active",
property: "color",
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
},
{
selector: "a",
state: ":hover",
property: "color",
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
},
]);
});
test("parse multiple rules", () => {
expect(parseCss(`a { color: red} a:hover { color: #ff0000 }`)).toEqual([
{
selector: "a",
property: "color",
value: {
type: "keyword",
value: "red",
},
},
{
selector: "a",
state: ":hover",
property: "color",
value: {
alpha: 1,
b: 0,
g: 0,
r: 255,
type: "rgb",
},
},
]);
});
test("parse multiple rules, remove overwritten properties", () => {
const css = `
h1 {
margin-bottom: 5px;
font-size: 2em;
}
h1 {
margin-bottom: 10px;
font-weight: bold;
}
h1 {
margin-top: 20px;
font-size: 38px;
line-height: 44px;
}
`;
expect(parseCss(css)).toEqual([
{
selector: "h1",
property: "marginBottom",
value: { type: "unit", unit: "px", value: 10 },
},
{
selector: "h1",
property: "fontSize",
value: { type: "unit", unit: "px", value: 38 },
},
{
selector: "h1",
property: "fontWeight",
value: { type: "keyword", value: "bold" },
},
{
selector: "h1",
property: "marginTop",
value: { type: "unit", unit: "px", value: 20 },
},
{
selector: "h1",
property: "lineHeight",
value: { type: "unit", unit: "px", value: 44 },
},
]);
});
test("parse shorthand", () => {
expect(parseCss(`a { border: 1px solid red }`)).toEqual([
{
selector: "a",
property: "borderTopWidth",
value: { type: "unit", unit: "px", value: 1 },
},
{
selector: "a",
property: "borderRightWidth",
value: { type: "unit", unit: "px", value: 1 },
},
{
selector: "a",
property: "borderBottomWidth",
value: { type: "unit", unit: "px", value: 1 },
},
{
selector: "a",
property: "borderLeftWidth",
value: { type: "unit", unit: "px", value: 1 },
},
{
selector: "a",
property: "borderTopStyle",
value: { type: "keyword", value: "solid" },
},
{
selector: "a",
property: "borderRightStyle",
value: { type: "keyword", value: "solid" },
},
{
selector: "a",
property: "borderBottomStyle",
value: { type: "keyword", value: "solid" },
},
{
selector: "a",
property: "borderLeftStyle",
value: { type: "keyword", value: "solid" },
},
{
selector: "a",
property: "borderTopColor",
value: { type: "keyword", value: "red" },
},
{
selector: "a",
property: "borderRightColor",
value: { type: "keyword", value: "red" },
},
{
selector: "a",
property: "borderBottomColor",
value: { type: "keyword", value: "red" },
},
{
selector: "a",
property: "borderLeftColor",
value: { type: "keyword", value: "red" },
},
]);
});
test("parse custom property", () => {
expect(parseCss(`a { --my-property: red; }`)).toEqual([
{
selector: "a",
property: "--my-property",
value: { type: "unparsed", value: "red" },
},
]);
});
// @todo https://github.com/webstudio-is/webstudio/issues/3399
test("parse variable as unset default", () => {
expect(parseCss(`a { color: var(--color) }`)).toEqual([
{
selector: "a",
property: "color",
value: { type: "keyword", value: "unset" },
},
]);
});
test("optionally parse variable as var value", () => {
expect(
parseCss(
`
a {
color: var(--color);
background-color: var(--color, red);
}
`,
{ customProperties: true }
)
).toEqual([
{
selector: "a",
property: "color",
value: { type: "var", value: "color" },
},
{
selector: "a",
property: "backgroundColor",
value: {
type: "var",
value: "color",
fallback: { type: "unparsed", value: "red" },
},
},
]);
});
test("parse empty value as unset", () => {
expect(parseCss(`a { color: ; background-color: red }`)).toEqual([
{
selector: "a",
property: "color",
value: { type: "keyword", value: "unset" },
},
{
selector: "a",
property: "backgroundColor",
value: { type: "keyword", value: "red" },
},
]);
});
test("unprefix property that doesn't need a prefix", () => {
expect(parseCss(`a { -webkit-color: red; }`)).toEqual([
{
selector: "a",
property: "color",
value: { type: "keyword", value: "red" },
},
]);
});
test("keep prefix for property that needs one", () => {
expect(parseCss(`a { -webkit-box-orient: horizontal; }`)).toEqual([
{
selector: "a",
property: "-webkit-box-orient",
value: { type: "keyword", value: "horizontal" },
},
]);
});
test("parse child combinator", () => {
expect(parseCss(`a > b { color: #ff0000 }`)).toEqual([
{
selector: "a > b",
property: "color",
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
},
]);
});
test("parse space combinator", () => {
expect(parseCss(`.a b { color: #ff0000 }`)).toEqual([
{
selector: ".a b",
property: "color",
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
},
]);
});
test("parse nested selectors as one token", () => {
expect(parseCss(`a b c.d { color: #ff0000 }`)).toEqual([
{
selector: "a b c.d",
property: "color",
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
},
]);
});
});
test("parse font-smooth properties", () => {
expect(
parseCss(`
a {
font-smoothing: auto;
}
b {
-webkit-font-smoothing: auto;
}
c {
-moz-osx-font-smoothing: auto;
}
`)
).toEqual([
{
selector: "a",
property: "WebkitFontSmoothing",
value: { type: "keyword", value: "auto" },
},
{
selector: "b",
property: "WebkitFontSmoothing",
value: { type: "keyword", value: "auto" },
},
{
selector: "c",
property: "MozOsxFontSmoothing",
value: { type: "keyword", value: "auto" },
},
]);
});
test("parse incorrectly unprefixed tap-highlight-color", () => {
expect(
parseCss(`
a {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
b {
tap-highlight-color: transparent;
}
`)
).toEqual([
{
selector: "a",
property: "-webkit-tap-highlight-color",
value: { alpha: 0, b: 0, g: 0, r: 0, type: "rgb" },
},
{
selector: "b",
property: "-webkit-tap-highlight-color",
value: { type: "keyword", value: "transparent" },
},
]);
});
test("parse top level rules and media all as base query", () => {
expect(
parseCss(`
a {
color: red;
}
@media all {
b {
width: auto;
}
}
`)
).toEqual([
{
selector: "a",
property: "color",
value: { type: "keyword", value: "red" },
},
{
selector: "b",
property: "width",
value: { type: "keyword", value: "auto" },
},
]);
});
test("parse media queries", () => {
expect(
parseCss(`
@media ( max-width: 768px ) {
a {
color: red;
}
}
@media (min-width: 768px) {
a {
color: green;
}
}
`)
).toEqual([
{
breakpoint: `(max-width:768px)`,
selector: "a",
property: "color",
value: { type: "keyword", value: "red" },
},
{
breakpoint: `(min-width:768px)`,
selector: "a",
property: "color",
value: { type: "keyword", value: "green" },
},
]);
});
test("support only screen media type", () => {
expect(
parseCss(`
@media all {
a {
color: yellow;
}
}
@media all and ( min-width: 768px ) {
a {
color: red;
}
}
@media screen and (min-width: 1024px) {
a {
color: green;
}
}
@media print and (min-width: 1280px) {
a {
color: blue;
}
}
`)
).toEqual([
{
selector: "a",
property: "color",
value: { type: "keyword", value: "yellow" },
},
{
breakpoint: `(min-width:768px)`,
selector: "a",
property: "color",
value: { type: "keyword", value: "red" },
},
{
breakpoint: `(min-width:1024px)`,
selector: "a",
property: "color",
value: { type: "keyword", value: "green" },
},
]);
});
test("ignore unsupported media queries", () => {
expect(
parseCss(`
a {
color: red;
}
@media (min-width: 768px) and (max-width: 1024px) {
b {
color: green;
}
}
@media (min-width: 768px) and (max-width: 1024px) {
c {
color: blue;
}
}
@media (hover: hover) {
d {
color: yellow;
}
}
@media (min-width: 40rem) {
e {
color: orange;
}
}
`)
).toEqual([
{
selector: "a",
property: "color",
value: { type: "keyword", value: "red" },
},
]);
});
test("ignore unsupported at rules", () => {
expect(
parseCss(`
a {
color: red;
}
@supports (display: grid) {
b {
color: green;
}
}
`)
).toEqual([
{
selector: "a",
property: "color",
value: { type: "keyword", value: "red" },
},
]);
});
test("parse media query", () => {
expect(parseMediaQuery(`(min-width: 768px)`)).toEqual({
minWidth: 768,
});
expect(parseMediaQuery(`(max-width: 768px)`)).toEqual({
maxWidth: 768,
});
expect(parseMediaQuery(`(hover: hover)`)).toEqual(undefined);
});