v0.7.2 71ebe61

engine_test.go RawHistoryFolder

1package css
2
3import (
4	"fmt"
5	"reflect"
6	"strings"
7	"testing"
8)
9
10func TestMemoIdentityAndEviction(t *testing.T) {
11	en := Compile(UA(), Parse(`.on button {color:red} .on:hover > button {background: blue} #x {opacity:.5} button {color:green}`))
12	root := Element{Type: "app", Classes: []string{"on"}}
13	child := Element{Type: "button"}
14	a := en.Compute(&root, nil)
15	first := en.Compute(&child, a)
16	en.EndFrame()
17	if en.Compute(&root, nil) != a || en.Compute(&child, a) != first {
18		t.Fatal("memo miss")
19	}
20	root.State = Hover
21	b := en.Compute(&root, nil)
22	second := en.Compute(&child, b)
23	if second == first || second.Style.BackgroundColor != namedColors["blue"] {
24		t.Fatal("state invalidation", second.Style)
25	}
26	root.State = 0
27	root.Classes = []string{"off"}
28	c := en.Compute(&root, nil)
29	third := en.Compute(&child, c)
30	if third == first || third.Style.Color != namedColors["green"] {
31		t.Fatal("class invalidation")
32	}
33	inline1, ds := ParseInline("color: red; opacity: .2")
34	if len(ds) > 0 {
35		t.Fatal(ds)
36	}
37	inline2, _ := ParseInline("color: blue; opacity: .2")
38	child.Inline = inline1
39	fourth := en.Compute(&child, a)
40	child.Inline = inline2
41	fifth := en.Compute(&child, a)
42	if fourth == fifth || fourth.Style.Color != namedColors["red"] || fifth.Style.Color != namedColors["blue"] {
43		t.Fatal("inline pointer invalidation")
44	}
45	for i := 0; i < 4; i++ {
46		en.EndFrame()
47	}
48	if len(en.memo) != 0 {
49		t.Fatalf("retained %d stale nodes", len(en.memo))
50	}
51}
52func TestCascadeVariablesAndSpecificity(t *testing.T) {
53	tests := []struct {
54		css, inline string
55		want        Color
56		padding     float64
57	}{
58		{`.x {color:red} .x {color:blue}`, "", namedColors["blue"], 0},
59		{`.x {color:red!important} #id {color:blue}`, "", namedColors["red"], 0},
60		{`.x {color:red!important}`, "color: blue", namedColors["red"], 0},
61		{`.x {color:red!important}`, "color: blue!important", namedColors["blue"], 0},
62		{`.x {--a: 3px; padding: var(--a); color:var(--missing,red)}`, "", namedColors["red"], 3},
63		{`.x {color:green;color:var(--missing)}`, "", Color{A: 1}, 0},
64	}
65	for _, tt := range tests {
66		en := Compile(UA(), Parse(tt.css))
67		d, _ := ParseInline(tt.inline)
68		s := en.Compute(&Element{Type: "box", ID: "id", Classes: []string{"x"}, Inline: d}, nil).Style
69		if s.Color != tt.want || s.Padding.Top.Value != tt.padding {
70			t.Fatalf("%s %s: %v padding %v", tt.css, tt.inline, s.Color, s.Padding.Top)
71		}
72	}
73}
74func TestClassSetOrder(t *testing.T) {
75	en := Compile(UA(), Parse(`.a.b {color:red}`))
76	a := en.Compute(&Element{Classes: []string{"a", "b"}}, nil)
77	b := en.Compute(&Element{Classes: []string{"b", "a"}}, nil)
78	if a != b || a.Style.Color != namedColors["red"] {
79		t.Fatal("class sets must be order independent")
80	}
81}
82
83func benchmarkTree() ([]Element, []int, Sheet) {
84	var src strings.Builder
85	types := []string{"app", "row", "column", "button", "text", "box"}
86	for i := 0; i < 200; i++ {
87		switch i % 5 {
88		case 0:
89			fmt.Fprintf(&src, "%s {padding:2px; color:#123}\n", types[i%len(types)])
90		case 1:
91			fmt.Fprintf(&src, ".c%d > button:hover {color:red}\n", i%12)
92		case 2:
93			fmt.Fprintf(&src, "#id%d {opacity:.5}\n", i%24)
94		case 3:
95			fmt.Fprintf(&src, ".c%d text {font-size:18px}\n", i%12)
96		case 4:
97			fmt.Fprintf(&src, ".c%d {margin:1px}\n", i%12)
98		}
99	}
100	nodes := make([]Element, 1000)
101	parents := make([]int, len(nodes))
102	for i := range nodes {
103		nodes[i].Type = types[i%len(types)]
104		nodes[i].Classes = []string{fmt.Sprintf("c%d", i%12)}
105		if i%7 == 0 {
106			nodes[i].ID = fmt.Sprintf("id%d", i%24)
107		}
108		if i%11 == 0 {
109			nodes[i].State = Hover
110		}
111		parents[i] = (i - 1) / 3
112	}
113	return nodes, parents, Parse(src.String())
114}
115func computeTree(en *Engine, nodes []Element, parents []int, results []*Computed) {
116	for i := range nodes {
117		var parent *Computed
118		if i > 0 {
119			parent = results[parents[i]]
120		}
121		results[i] = en.Compute(&nodes[i], parent)
122	}
123}
124func BenchmarkEngine(b *testing.B) {
125	nodes, parents, sheet := benchmarkTree()
126	for _, mode := range []string{"cold", "warm", "state-change"} {
127		b.Run(mode, func(b *testing.B) {
128			b.ReportAllocs()
129			en := Compile(UA(), sheet)
130			results := make([]*Computed, len(nodes))
131			computeTree(en, nodes, parents, results)
132			en.EndFrame()
133			b.ResetTimer()
134			for i := 0; i < b.N; i++ {
135				if mode == "cold" {
136					en = Compile(UA(), sheet)
137				}
138				if mode == "state-change" {
139					nodes[10].State ^= Hover
140				}
141				computeTree(en, nodes, parents, results)
142				en.EndFrame()
143			}
144		})
145	}
146}
147
148func TestTypedPropertiesAndVariables(t *testing.T) {
149	en := Compile(UA(), Parse(`app {color:#123;font-size:20px; --pair: 1em 2px; --ink: rebeccapurple} app > button {padding:var(--pair);border:2px solid var(--ink);font-family:"A B", sans-serif;box-shadow:1px 2px 3px red;line-height:1.5} button:focus-visible {opacity:.4}`))
150	root := en.Compute(&Element{Type: "app"}, nil)
151	c := en.Compute(&Element{Type: "button", State: FocusVisible}, root).Style
152	if c.Padding.Top.Value != 20 || c.Padding.Right.Value != 2 || c.BorderWidth.Left.Value != 2 || c.BorderColor.Left != namedColors["rebeccapurple"] || c.BorderStyle.Left != KeywordSolid || c.Opacity != .4 || len(c.FontFamily) != 2 || c.FontFamily[0] != "A B" || len(c.BoxShadow) != 1 || c.LineHeightKind != KeywordNumber || c.LineHeightNumber != 1.5 {
153		t.Fatalf("typed style: %+v", c)
154	}
155}
156func TestDuplicateSelectorListUsesHighestMatchingSpecificity(t *testing.T) {
157	en := Compile(Sheet{}, Parse(`button, #x {color:red} button {color:blue}`))
158	if got := en.Compute(&Element{Type: "button", ID: "x"}, nil).Style.Color; got != namedColors["red"] {
159		t.Fatal(got)
160	}
161}
162func TestEndFrameKeepsAncestorsOfLiveDescendant(t *testing.T) {
163	en := Compile(UA(), Parse(`.a button {color:red}`))
164	p := en.Compute(&Element{Type: "app", Classes: []string{"a"}}, nil)
165	c := en.Compute(&Element{Type: "button"}, p)
166	for i := 0; i < 5; i++ {
167		en.Compute(&Element{Type: "button"}, p)
168		en.EndFrame()
169	}
170	if en.memo[p.node.key] != p || en.Compute(&Element{Type: "button"}, p) != c {
171		t.Fatal("lost live ancestor")
172	}
173	for i := 0; i < 3; i++ {
174		en.EndFrame()
175	}
176	if len(en.memo) != 0 {
177		t.Fatal("eviction leak", len(en.memo))
178	}
179}
180
181func TestEvictionBoundAcrossChangingStates(t *testing.T) {
182	en := Compile(UA(), Parse(`.a:hover button{color:red}`))
183	root := Element{Type: "app", Classes: []string{"a"}}
184	child := Element{Type: "button"}
185	for frame := 0; frame < 100; frame++ {
186		root.State = StateFlags(frame%2) * Hover
187		p := en.Compute(&root, nil)
188		en.Compute(&child, p)
189		en.EndFrame()
190		if len(en.memo) > 4 {
191			t.Fatalf("frame %d: %d nodes", frame, len(en.memo))
192		}
193	}
194}
195
196func TestVariableCSSWideAndInvalidShorthand(t *testing.T) {
197	en := Compile(Sheet{}, Parse(`app{color:blue;font-size:22px} button{--s:inherit;color:var(--s);--x:bad; margin:7px;margin:var(--x); border:1px solid red;border:var(--x)}`))
198	p := en.Compute(&Element{Type: "app"}, nil)
199	s := en.Compute(&Element{Type: "button"}, p).Style
200	if s.Color != p.Style.Color || s.Margin.Top.Value != 0 || s.BorderWidth.Top.Value != 0 {
201		t.Fatalf("variable cascade: %+v", s)
202	}
203}
204
205func TestInlineInvalidAndSheetSnapshot(t *testing.T) {
206	sheet := Parse(`button{color:red}`)
207	en := Compile(Sheet{}, sheet)
208	sheet.Rules[0].Declarations[0].Value = "blue"
209	d, _ := ParseInline("color: nonsense; opacity:.2")
210	got := en.Compute(&Element{Type: "button", Inline: d}, nil).Style
211	if got.Color != namedColors["red"] || got.Opacity != .2 {
212		t.Fatalf("invalid inline or sheet mutation: %+v", got)
213	}
214}
215
216func TestStylePropertyCoverage(t *testing.T) {
217	if len(propertyNames) != int(propertyCount) {
218		t.Fatal("property enum and table diverged")
219	}
220	for id, name := range propertyNames {
221		if got, ok := propertyIndex(name); !ok || int(got) != id {
222			t.Fatalf("property %q: %d %v", name, got, ok)
223		}
224		v, ok := parseValue(name, initial(name), Color{A: 1}, 16, 16)
225		if !ok {
226			t.Fatalf("invalid default for %q", name)
227		}
228		s := &Style{}
229		s.set(propertyID(id), v)
230		want := s.value(propertyID(id))
231		if name == "line-height" && want.Keyword != "normal" {
232			t.Fatalf("line-height default: %+v", want)
233		}
234	}
235}
236
237func TestKeywordAndCurrentColorInheritance(t *testing.T) {
238	en := Compile(Sheet{}, Parse(`app{color:blue; font-style:italic; font-size:20px} button{color:currentColor; border-color:currentColor; border-style:solid; font-style:oblique; font-size:1em; line-height:2; letter-spacing:normal}`))
239	p := en.Compute(&Element{Type: "app"}, nil)
240	s := en.Compute(&Element{Type: "button"}, p).Style
241	if s.Color != p.Style.Color || s.BorderColor.Top != s.Color || s.BorderStyle.Top != KeywordSolid || s.FontStyle != KeywordOblique || s.FontSize.Value != 20 || s.LineHeightKind != KeywordNumber || s.LineHeightNumber != 2 || !s.LetterSpacingNormal {
242		t.Fatalf("keyword/color style %+v", s)
243	}
244}
245
246func TestVariableInvalidatesWholeWinningShorthand(t *testing.T) {
247	en := Compile(Sheet{}, Parse(`button{--p:5px nonsense;padding:3px;padding:var(--p)}`))
248	s := en.Compute(&Element{Type: "button"}, nil).Style
249	if s.Padding.Top.Value != 0 || s.Padding.Right.Value != 0 {
250		t.Fatal("invalid shorthand did not reset", s.Padding)
251	}
252}
253
254func TestInvalidStaticShorthandIgnored(t *testing.T) {
255	en := Compile(Sheet{}, Parse(`button{padding:3px;padding:5px nonsense;border:2px solid red;border:garbage green}`))
256	s := en.Compute(&Element{Type: "button"}, nil).Style
257	if s.Padding.Top.Value != 3 || s.BorderWidth.Top.Value != 2 || s.BorderColor.Top != namedColors["red"] {
258		t.Fatalf("partial invalid shorthand: %+v", s)
259	}
260}
261
262func TestCSSWideShorthand(t *testing.T) {
263	en := Compile(Sheet{}, Parse(`app{color:blue} button{padding:5px; padding:initial; border:2px solid red; border:unset; color:red; color:inherit}`))
264	p := en.Compute(&Element{Type: "app"}, nil)
265	s := en.Compute(&Element{Type: "button"}, p).Style
266	if s.Padding.Top.Value != 0 || s.BorderWidth.Top.Value != 0 || s.Color != p.Style.Color {
267		t.Fatalf("wide shorthand: %+v", s)
268	}
269}
270
271func TestCSSWideVariableShorthand(t *testing.T) {
272	en := Compile(Sheet{}, Parse(`button{--x:initial; padding:9px; padding:var(--x); border:2px solid red;border:var(--x)}`))
273	s := en.Compute(&Element{Type: "button"}, nil).Style
274	if s.Padding.Top.Value != 0 || s.BorderWidth.Top.Value != 0 || s.BorderStyle.Top != KeywordNone {
275		t.Fatalf("wide var shorthand: %+v", s)
276	}
277}
278
279func TestUnusedMemoEvictedAfterTwoFrames(t *testing.T) {
280	en := Compile(Sheet{}, Sheet{})
281	en.Compute(&Element{Type: "app"}, nil)
282	en.EndFrame()
283	if len(en.memo) != 1 {
284		t.Fatal("evicted too early")
285	}
286	en.EndFrame()
287	if len(en.memo) != 0 {
288		t.Fatal("evicted too late")
289	}
290}
291
292func TestStyleInitialMatchesPropertyDefaults(t *testing.T) {
293	s := Compile(Sheet{}, Sheet{}).Compute(&Element{Type: "app"}, nil).Style
294	for id, name := range propertyNames {
295		want, ok := parseValue(name, initial(name), s.Color, 16, 16)
296		if !ok {
297			t.Fatalf("invalid initial %s", name)
298		}
299		got := s.value(propertyID(id))
300		// Slices are shared immutable values and not directly comparable.
301		switch name {
302		case "font-family":
303			if !reflect.DeepEqual(got.Families, want.Families) {
304				t.Fatalf("%s: %+v vs %+v", name, got, want)
305			}
306		case "box-shadow":
307			if len(got.Shadows) != len(want.Shadows) {
308				t.Fatalf("%s: %+v vs %+v", name, got, want)
309			}
310		default:
311			if got.Length != want.Length || got.Color != want.Color || got.Number != want.Number || got.Keyword != want.Keyword {
312				t.Fatalf("%s: %+v vs %+v", name, got, want)
313			}
314		}
315	}
316}
317
318func TestInheritedFieldsAndCurrentColorDefaults(t *testing.T) {
319	en := Compile(Sheet{}, Parse(`app{color:red; font-family:serif; font-size:22px; font-style:italic; cursor:pointer; text-align:center;line-height:2;letter-spacing:1px;accent-color:green} button{background-color:blue}`))
320	p := en.Compute(&Element{Type: "app"}, nil)
321	c := en.Compute(&Element{Type: "button"}, p).Style
322	if c.Color != p.Style.Color || c.AccentColor != namedColors["green"] || c.BorderColor.Top != c.Color || c.OutlineColor != c.Color || c.FontSize != p.Style.FontSize || c.FontStyle != KeywordItalic || c.FontFamily[0] != "serif" || c.Cursor != KeywordPointer || c.TextAlign != KeywordCenter || c.LineHeightNumber != 2 || c.LetterSpacing.Value != 1 || c.BackgroundColor != namedColors["blue"] {
323		t.Fatalf("inheritance: %+v", c)
324	}
325}
326
327func TestComputedInitialInheritsColorButResetsBackground(t *testing.T) {
328	en := Compile(Sheet{}, Parse(`app{color:red;background:blue}button{background:initial}`))
329	p := en.Compute(&Element{Type: "app"}, nil)
330	c := en.Compute(&Element{Type: "button"}, p).Style
331	if c.Color != namedColors["red"] || c.BackgroundColor != (Color{}) || c.BorderColor.Top != c.Color {
332		t.Fatalf("computed initial: %+v", c)
333	}
334}
335
336func TestChangedStateMissesOnlySubtree(t *testing.T) {
337	nodes, parents, sheet := benchmarkTree()
338	en := Compile(UA(), sheet)
339	results := make([]*Computed, len(nodes))
340	computeTree(en, nodes, parents, results)
341	before := append([]*Computed(nil), results...)
342	en.EndFrame()
343	const changed = 10
344	nodes[changed].State ^= Hover
345	computeTree(en, nodes, parents, results)
346	descendants := make([]bool, len(nodes))
347	descendants[changed] = true
348	expected, misses := 0, 0
349	for i := range nodes {
350		if i > changed && descendants[parents[i]] {
351			descendants[i] = true
352		}
353		if descendants[i] {
354			expected++
355		}
356		if results[i] != before[i] {
357			misses++
358			if !descendants[i] {
359				t.Fatalf("unrelated node %d recomputed", i)
360			}
361		} else if descendants[i] {
362			t.Fatalf("descendant %d reused stale result", i)
363		}
364	}
365	if misses != expected {
366		t.Fatalf("Compute misses %d; affected subtree size %d", misses, expected)
367	}
368}
369
370func TestPendingVariableShorthandsAtomic(t *testing.T) {
371	cases := []struct {
372		property, base, valid, invalid string
373		targets                        []string
374	}{
375		{"background", "blue", "red", "no-color", []string{"background-color"}},
376		{"margin", "9px", "1px 2px 3px", "1px nope", shorthandTargets("margin")},
377		{"padding", "9px", "1px 2px 3px", "1px nope", shorthandTargets("padding")},
378		{"border-width", "9px", "1px 2px 3px", "1px nope", shorthandTargets("border-width")},
379		{"border-style", "none", "solid", "solid dotted", shorthandTargets("border-style")},
380		{"border-color", "blue", "red green", "red nope", shorthandTargets("border-color")},
381		{"border-radius", "9px", "1px 2px 3px", "1px nope", shorthandTargets("border-radius")},
382		{"border-top", "2px solid blue", "3px solid red", "3px dotted red", shorthandTargets("border-top")},
383		{"border-right", "2px solid blue", "3px solid red", "3px dotted red", shorthandTargets("border-right")},
384		{"border-bottom", "2px solid blue", "3px solid red", "3px dotted red", shorthandTargets("border-bottom")},
385		{"border-left", "2px solid blue", "3px solid red", "3px dotted red", shorthandTargets("border-left")},
386		{"border", "2px solid blue", "3px solid red", "3px dotted red", shorthandTargets("border")},
387		{"outline", "2px solid blue", "3px solid red", "3px dotted red", shorthandTargets("outline")},
388		{"flex", "none", "2 3 10px", "2 garbage", shorthandTargets("flex")},
389		{"gap", "9px", "1px 2px", "1px nope", shorthandTargets("gap")},
390		{"overflow", "hidden", "scroll auto", "scroll garbage", shorthandTargets("overflow")},
391	}
392	baseline := Compile(Sheet{}, Sheet{}).Compute(&Element{Type: "button"}, nil).Style
393	for _, tt := range cases {
394		t.Run(tt.property, func(t *testing.T) {
395			css := func(v string) *Style {
396				src := "button{--x:" + v + ";" + tt.property + ":" + tt.base + ";" + tt.property + ":var(--x)}"
397				return Compile(Sheet{}, Parse(src)).Compute(&Element{Type: "button"}, nil).Style
398			}
399			good, bad := css(tt.valid), css(tt.invalid)
400			for _, name := range tt.targets {
401				id, ok := propertyIndex(name)
402				if !ok {
403					t.Fatalf("unknown target %s", name)
404				}
405				if reflect.DeepEqual(good.value(id), baseline.value(id)) {
406					t.Errorf("valid %s did not override %s", tt.valid, name)
407				}
408				if !reflect.DeepEqual(bad.value(id), baseline.value(id)) {
409					t.Errorf("invalid %s did not reset %s: %+v", tt.invalid, name, bad.value(id))
410				}
411			}
412		})
413	}
414}
415
416func TestPendingShorthandWinnerPerLonghand(t *testing.T) {
417	en := Compile(Sheet{}, Parse(`button{--pair:1px 2px; padding:var(--pair); padding-left:7px; padding-right:9px!important}`))
418	s := en.Compute(&Element{Type: "button"}, nil).Style
419	if s.Padding.Top.Value != 1 || s.Padding.Bottom.Value != 1 || s.Padding.Left.Value != 7 || s.Padding.Right.Value != 9 {
420		t.Fatal(s.Padding)
421	}
422}
423
424func TestPendingShorthandInheritedCustomAndInline(t *testing.T) {
425	en := Compile(Sheet{}, Parse(`app{--space:1em 2px;color:red}button{padding:var(--space);margin:9px;margin:var(--missing);border:1px solid blue;border:var(--missing)}`))
426	p := en.Compute(&Element{Type: "app"}, nil)
427	in, _ := ParseInline(`padding:var(--space);border:var(--missing, 2px solid currentColor)`)
428	c := en.Compute(&Element{Type: "button", Inline: in}, p).Style
429	if c.Padding.Top.Value != 16 || c.Padding.Right.Value != 2 || c.Margin.Top.Value != 0 || c.BorderWidth.Left.Value != 2 || c.BorderColor.Left != namedColors["red"] {
430		t.Fatalf("pending group inheritance/inline %+v", c)
431	}
432}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close