1package theme
2
3import (
4 "testing"
5
6 renderer "github.com/bnema/vev-vt"
7 "github.com/stretchr/testify/require"
8)
9
10func TestRelativeLuminance(t *testing.T) {
11 tests := []struct {
12 name string
13 color renderer.RGB
14 want float64
15 }{
16 {name: "black", color: renderer.RGB{}, want: 0},
17 {name: "white", color: renderer.RGB{R: 255, G: 255, B: 255}, want: 1},
18 {name: "sRGB linearization breakpoint", color: renderer.RGB{R: 10}, want: 0.2126 * (10.0 / 255.0 / 12.92)},
19 {name: "nonlinear sRGB", color: renderer.RGB{R: 128, G: 128, B: 128}, want: 0.21586050011389923},
20 {name: "red", color: renderer.RGB{R: 255}, want: 0.2126},
21 {name: "green", color: renderer.RGB{G: 255}, want: 0.7152},
22 {name: "blue", color: renderer.RGB{B: 255}, want: 0.0722},
23 }
24
25 for _, tt := range tests {
26 t.Run(tt.name, func(t *testing.T) {
27 got := RelativeLuminance(tt.color)
28 require.InDelta(t, tt.want, got, 1e-12)
29 require.Equal(t, got, RelativeLuminance(tt.color))
30 })
31 }
32}
33
34func TestContrastRatio(t *testing.T) {
35 black := renderer.RGB{}
36 white := renderer.RGB{R: 255, G: 255, B: 255}
37 middle := renderer.RGB{R: 123, G: 45, B: 67}
38
39 require.InDelta(t, 21, ContrastRatio(black, white), 1e-12)
40 require.InDelta(t, 1, ContrastRatio(middle, middle), 1e-12)
41 require.Equal(t, ContrastRatio(black, middle), ContrastRatio(middle, black))
42}
43
44func TestAccentContrastMinimum(t *testing.T) {
45 require.Equal(t, 3.0, accentContrastMin)
46}
47
48func TestPulseColor(t *testing.T) {
49 rgbBase := renderer.Style{
50 Italic: true,
51 Foreground: 7,
52 Background: 8,
53 HasForegroundRGB: true,
54 ForegroundRGB: renderer.RGB{R: 200, G: 100, B: 50},
55 HasBackgroundRGB: true,
56 BackgroundRGB: renderer.RGB{R: 20, G: 40, B: 60},
57 HasUnderlineColor: true,
58 UnderlineColor: 4,
59 HasUnderlineColorRGB: false,
60 }
61 indexedBase := renderer.Style{Foreground: -1, Background: -1, Inverse: true}
62 mixedBase := rgbBase
63 mixedBase.HasBackgroundRGB = false
64
65 tests := []struct {
66 name string
67 base renderer.Style
68 intensity float64
69 want renderer.Style
70 }{
71 {
72 name: "RGB starts at the background",
73 base: rgbBase,
74 intensity: 0,
75 want: func() renderer.Style {
76 style := rgbBase
77 style.Bold = true
78 style.ForegroundRGB = renderer.RGB{R: 20, G: 40, B: 60}
79 return style
80 }(),
81 },
82 {
83 name: "RGB blends halfway with rounding",
84 base: rgbBase,
85 intensity: 0.5,
86 want: func() renderer.Style {
87 style := rgbBase
88 style.Bold = true
89 style.ForegroundRGB = renderer.RGB{R: 110, G: 70, B: 55}
90 return style
91 }(),
92 },
93 {
94 name: "RGB reaches the foreground",
95 base: rgbBase,
96 intensity: 1,
97 want: func() renderer.Style {
98 style := rgbBase
99 style.Bold = true
100 return style
101 }(),
102 },
103 {
104 name: "RGB foreground without RGB background uses indexed grayscale",
105 base: mixedBase,
106 intensity: 0.5,
107 want: func() renderer.Style {
108 style := mixedBase
109 style.Bold = true
110 style.HasForegroundRGB = false
111 style.Foreground = 249
112 return style
113 }(),
114 },
115 {
116 name: "indexed color uses xterm grayscale start",
117 base: indexedBase,
118 intensity: 0,
119 want: renderer.Style{Bold: true, Inverse: true, Foreground: 244, Background: -1},
120 },
121 {
122 name: "indexed color truncates the grayscale ramp",
123 base: indexedBase,
124 intensity: 0.5,
125 want: renderer.Style{Bold: true, Inverse: true, Foreground: 249, Background: -1},
126 },
127 {
128 name: "indexed color reaches xterm grayscale end",
129 base: indexedBase,
130 intensity: 1,
131 want: renderer.Style{Bold: true, Inverse: true, Foreground: 255, Background: -1},
132 },
133 }
134
135 for _, tt := range tests {
136 t.Run(tt.name, func(t *testing.T) {
137 require.Equal(t, tt.want, PulseColor(tt.base, tt.intensity))
138 })
139 }
140}
141
142func TestIndexedForegroundClearsInverseForNonTrueColorFallback(t *testing.T) {
143 fallback := indexedForeground(renderer.Style{
144 Bold: true,
145 Inverse: true,
146 Foreground: -1,
147 HasForegroundRGB: true,
148 ForegroundRGB: renderer.RGB{R: 1, G: 2, B: 3},
149 }, 2)
150
151 require.False(t, fallback.Inverse, "an indexed foreground must not become an effective background")
152 require.False(t, fallback.HasForegroundRGB)
153 require.Equal(t, 2, fallback.Foreground)
154 require.True(t, fallback.Bold)
155}
156
157func TestMRUFade(t *testing.T) {
158 base := renderer.Style{
159 Bold: true,
160 Foreground: 7,
161 Background: 8,
162 HasForegroundRGB: true,
163 ForegroundRGB: renderer.RGB{R: 200, G: 100, B: 50},
164 HasBackgroundRGB: true,
165 BackgroundRGB: renderer.RGB{R: 40, G: 60, B: 80},
166 HasUnderlineColorRGB: true,
167 UnderlineColorRGB: renderer.RGB{R: 1, G: 2, B: 3},
168 }
169 terminalTheme := Theme{HasBG: true, Background: renderer.RGB{R: 10, G: 20, B: 30}}
170
171 tests := []struct {
172 name string
173 base renderer.Style
174 theme Theme
175 i int
176 count int
177 want renderer.Style
178 }{
179 {name: "single entry is unchanged", base: base, theme: terminalTheme, count: 1, want: base},
180 {name: "missing theme background is unchanged", base: base, theme: Theme{}, i: 1, count: 3, want: base},
181 {name: "indexed base is unchanged", base: renderer.DefaultStyle(), theme: terminalTheme, i: 1, count: 3, want: renderer.DefaultStyle()},
182 {name: "newest entry starts unfaded", base: base, theme: terminalTheme, count: 3, want: base},
183 {
184 name: "middle entry fades thirty percent toward terminal background",
185 base: base,
186 theme: terminalTheme,
187 i: 1,
188 count: 3,
189 want: func() renderer.Style {
190 style := base
191 style.ForegroundRGB = renderer.RGB{R: 143, G: 76, B: 44}
192 style.BackgroundRGB = renderer.RGB{R: 31, G: 48, B: 65}
193 return style
194 }(),
195 },
196 {
197 name: "oldest entry fades sixty percent toward terminal background",
198 base: base,
199 theme: terminalTheme,
200 i: 2,
201 count: 3,
202 want: func() renderer.Style {
203 style := base
204 style.ForegroundRGB = renderer.RGB{R: 86, G: 52, B: 38}
205 style.BackgroundRGB = renderer.RGB{R: 22, G: 36, B: 50}
206 return style
207 }(),
208 },
209 }
210
211 for _, tt := range tests {
212 t.Run(tt.name, func(t *testing.T) {
213 require.Equal(t, tt.want, MRUFade(tt.base, tt.theme, tt.i, tt.count))
214 })
215 }
216}
217
218func TestThemePaletteColor(t *testing.T) {
219 palette := [16]renderer.RGB{
220 3: {R: 0x11, G: 0x22, B: 0x33},
221 7: {R: 0xaa, G: 0xbb, B: 0xcc},
222 }
223 theme := Theme{
224 UsePalette: true,
225 Palette: palette,
226 PaletteKnown: 1<<3 | 1<<7,
227 }
228
229 tests := []struct {
230 name string
231 slot int
232 want renderer.RGB
233 ok bool
234 }{
235 {name: "known slot", slot: 3, want: palette[3], ok: true},
236 {name: "unknown slot", slot: 4},
237 {name: "negative slot", slot: -1},
238 {name: "slot above palette", slot: 16},
239 }
240
241 for _, tt := range tests {
242 t.Run(tt.name, func(t *testing.T) {
243 got, ok := theme.PaletteColor(tt.slot)
244 require.Equal(t, tt.ok, ok)
245 require.Equal(t, tt.want, got)
246 })
247 }
248
249 theme.UsePalette = false
250 got, ok := theme.PaletteColor(3)
251 require.False(t, ok)
252 require.Equal(t, renderer.RGB{}, got)
253}
254
255func TestBuiltinThemesDoNotUsePalette(t *testing.T) {
256 for name, theme := range map[string]Theme{
257 "dark": BuiltinDark,
258 "light": BuiltinLight,
259 } {
260 t.Run(name, func(t *testing.T) {
261 require.False(t, theme.UsePalette)
262 require.Zero(t, theme.PaletteKnown)
263 require.Equal(t, [16]renderer.RGB{}, theme.Palette)
264 })
265 }
266}
267
268func TestThemeIsComparable(t *testing.T) {
269 themes := map[Theme]bool{BuiltinDark: true}
270 require.True(t, themes[BuiltinDark])
271}