1package css
2
3import (
4 "encoding/json"
5 "os"
6 "reflect"
7 "testing"
8)
9
10func TestFixture(t *testing.T) {
11 src, e := os.ReadFile("testdata/basic.css")
12 if e != nil {
13 t.Fatal(e)
14 }
15 want, e := os.ReadFile("testdata/basic.json")
16 if e != nil {
17 t.Fatal(e)
18 }
19 root := Element{Type: "app"}
20 child := Element{Type: "row"}
21 sheet := Parse(string(src))
22 engine := Compile(UA(), sheet)
23 p := engine.Compute(&root, nil)
24 got := engine.Compute(&child, p).Style
25 v := map[string]any{"display": keywordNames[got.Display], "padding-top": got.Padding.Top, "padding-right": got.Padding.Right, "background-color": got.BackgroundColor, "font-size": got.FontSize}
26 b, _ := json.Marshal(v)
27 var a, c any
28 _ = json.Unmarshal(b, &a)
29 _ = json.Unmarshal(want, &c)
30 if !reflect.DeepEqual(a, c) {
31 t.Fatalf("got %s want %s", b, want)
32 }
33}
34func TestColors(t *testing.T) {
35 tests := []struct {
36 s string
37 c Color
38 }{{"#f00", Color{1, 0, 0, 1}}, {"#f008", Color{1, 0, 0, 136.0 / 255}}, {"#ff0000", Color{1, 0, 0, 1}}, {"#ff000080", Color{1, 0, 0, 128.0 / 255}}, {"rgb(255,0,0)", Color{1, 0, 0, 1}}, {"rgba(255 0 0 / 50%)", Color{1, 0, 0, .5}}, {"hsl(0 100% 50%)", Color{1, 0, 0, 1}}, {"hsla(0,100%,50%,.5)", Color{1, 0, 0, .5}}, {"red", Color{1, 0, 0, 1}}, {"transparent", Color{}}}
39 for _, tt := range tests {
40 t.Run(tt.s, func(t *testing.T) {
41 v, ok := parseColor(tt.s, Color{})
42 if !ok || v != tt.c {
43 t.Fatalf("%v %v", v, ok)
44 }
45 })
46 }
47}
48func TestCascade(t *testing.T) {
49 tests := []struct {
50 css, typ, id string
51 hover bool
52 want string
53 }{{"row{display:block}", "row", "", false, "block"}, {"row{display:none} row{display:flex}", "row", "", false, "flex"}, {"#x {display:none} row {display:flex}", "row", "x", false, "none"}, {"row:hover{display:none} row{display:flex}", "row", "", true, "none"}, {"row{display:none!important} #x{display:flex}", "row", "x", false, "none"}}
54 for _, tt := range tests {
55 t.Run(tt.css, func(t *testing.T) {
56 e := Element{Type: tt.typ, ID: tt.id}
57 if tt.hover {
58 e.State = Hover
59 }
60 s := Compile(UA(), Parse(tt.css)).Compute(&e, nil).Style
61 if keywordNames[s.Display] != tt.want {
62 t.Fatalf("got %s want %s", keywordNames[s.Display], tt.want)
63 }
64 })
65 }
66}
67func TestInvalidAndInheritance(t *testing.T) {
68 p := Element{Type: "app"}
69 e := Element{Type: "button"}
70 sheet := Parse("app {color: blue; font-size: 20px; --a: var(--b); --b: var(--a)} button { color: var(--a); padding: 1em 2px; opacity: 1e999; unknown: 1; background: var(--a, red)}")
71 engine := Compile(UA(), sheet)
72 ps := engine.Compute(&p, nil)
73 s := engine.Compute(&e, ps).Style
74 if s.Color != ps.Style.Color || s.Padding.Top.Value != 20 || s.Opacity != 1 || s.BackgroundColor.R != 1 {
75 t.Fatalf("style %#v", s)
76 }
77}
78func TestRecovery(t *testing.T) {
79 s := Parse("button { wat; color:red } bad:hover::foo {color:blue} button { color: green }")
80 if len(s.Rules) != 2 || len(s.Diagnostics) < 2 {
81 t.Fatalf("%+v", s)
82 }
83 e := Element{Type: "button"}
84 v := Compile(UA(), s).Compute(&e, nil).Style
85 if v.Color.G != namedColors["green"].G {
86 t.Fatal(v.Color)
87 }
88}
89func TestSelector(t *testing.T) {
90 p := Element{Type: "app"}
91 e := Element{Type: "button", Classes: []string{"ok"}, State: FocusVisible | Checked}
92 for _, tt := range []struct {
93 s string
94 match bool
95 }{{"app > button.ok:focus-visible", true}, {"app button", true}, {"row > button", false}, {"app > button:disabled", false}, {"button:checked", true}} {
96 s := Parse(tt.s + " {color:red}")
97 en := Compile(Sheet{}, s)
98 pv := en.Compute(&p, nil)
99 got := en.Compute(&e, pv).Style.Color == namedColors["red"]
100 if len(s.Rules) != 1 || got != tt.match {
101 t.Fatalf("%s %+v", tt.s, s)
102 }
103 }
104}
105func FuzzTokenizerParser(f *testing.F) {
106 for _, s := range []string{"a { color:red }", "\xff{bad:'\n; background: rgb(1,2,3)}", "/*", "a{--x:var(--x); color:var(--x)}"} {
107 f.Add(s)
108 }
109 f.Fuzz(func(t *testing.T, s string) {
110 if len(s) > 1<<16 {
111 t.Skip()
112 }
113 _ = Tokenize(s)
114 sheet := Parse(s)
115 e := Element{Type: "a"}
116 _ = Compile(UA(), sheet).Compute(&e, nil)
117 })
118}
119func TestHugeNumbers(t *testing.T) {
120 for _, v := range []string{"1e999", "NaN", "Inf", "1e308em"} {
121 e := Element{Type: "app"}
122 s := Compile(UA(), Parse("app{width:"+v+";font-size:"+v+"; opacity:"+v+"}")).Compute(&e, nil).Style
123 if s.Width.Unit != "auto" || s.FontSize.Value != 16 || s.Opacity != 1 {
124 t.Fatalf("%s: %+v", v, s)
125 }
126 }
127}
128func TestTokenRecovery(t *testing.T) {
129 ts := Tokenize("a {color:'broken\n; background:url(foo); width:2px}")
130 found := false
131 for _, tok := range ts {
132 if tok.Kind == "bad-string" {
133 found = true
134 }
135 }
136 if !found {
137 t.Fatal(ts)
138 }
139}
140func TestEscapesAndBadURL(t *testing.T) {
141 for _, tt := range []struct{ src, kind, value string }{{`b\75 tton`, "ident", "button"}, {`#\78 `, "hash", "x"}, {`url(foo)`, "url", "foo"}, {`url(foo"bar);a`, "bad-url", "foo\"bar"}} {
142 ts := Tokenize(tt.src)
143 if len(ts) == 0 || ts[0].Kind != tt.kind || ts[0].Value != tt.value {
144 t.Errorf("%q: %+v", tt.src, ts)
145 }
146 }
147 e := Element{Type: "button"}
148 s := Parse(`b\75 tton {color:red}`)
149 if len(s.Rules) != 1 || Compile(Sheet{}, s).Compute(&e, nil).Style.Color != namedColors["red"] {
150 t.Fatal(s)
151 }
152}
153func TestRecoveryNested(t *testing.T) {
154 tests := []struct {
155 src string
156 rules, decls int
157 }{{`button {broken: fn(a;b); color: red;}`, 1, 2}, {`button {bad { x:y; }; color:red} row{display:flex}`, 2, 1}, {`button:unknown { color:red } button {color:blue}`, 1, 1}, {`button { color:red`, 0, 0}}
158 for _, tt := range tests {
159 s := Parse(tt.src)
160 if len(s.Rules) != tt.rules {
161 t.Errorf("%q rules=%d diagnostics=%v", tt.src, len(s.Rules), s.Diagnostics)
162 continue
163 }
164 if tt.rules > 0 && len(s.Rules[0].Declarations) != tt.decls {
165 t.Errorf("%q decls=%d diagnostics=%v", tt.src, len(s.Rules[0].Declarations), s.Diagnostics)
166 }
167 }
168}