| 🛟 Updated. 28d5985 k33g 6h ago | 1 | package theme |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/gdamore/tcell/v2" |
| 11 | |
| 12 | "github.com/BurntSushi/toml" |
| 13 | ) |
| 14 | |
| 15 | func TestParseReadsNameAndDescription(t *testing.T) { |
| 16 | th := mustParse(t, ` |
| 17 | name = "Example" |
| 18 | description = "A theme for the tests" |
| 19 | [colors] |
| 20 | default = { fg = "white", bg = "black" } |
| 21 | `) |
| 22 | |
| 23 | if got := th.Name(); got != "Example" { |
| 24 | t.Errorf("Name() = %q, want %q", got, "Example") |
| 25 | } |
| 26 | if got := th.Description(); got != "A theme for the tests" { |
| 27 | t.Errorf("Description() = %q", got) |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | func TestStyleReadsTheColoursThatWereSet(t *testing.T) { |
| 32 | th := mustParse(t, ` |
| 33 | [colors] |
| 34 | default = { fg = "white", bg = "black" } |
| 35 | "syntax.keyword" = { fg = "lime", bg = "navy", bold = true } |
| 36 | `) |
| 37 | |
| 38 | fg, bg, attrs := th.Style(KeySyntaxKeyword).Decompose() |
| 39 | if fg != tcell.ColorLime { |
| 40 | t.Errorf("foreground = %v, want lime", fg) |
| 41 | } |
| 42 | if bg != tcell.ColorNavy { |
| 43 | t.Errorf("background = %v, want navy", bg) |
| 44 | } |
| 45 | if attrs&tcell.AttrBold == 0 { |
| 46 | t.Error("the bold attribute was not applied") |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | func TestStyleAcceptsHexColours(t *testing.T) { |
| 51 | th := mustParse(t, ` |
| 52 | [colors] |
| 53 | default = { fg = "#ff8800", bg = "#001122" } |
| 54 | `) |
| 55 | |
| 56 | fg, bg, _ := th.Style(KeyDefault).Decompose() |
| 57 | if got := fg.Hex(); got != 0xff8800 { |
| 58 | t.Errorf("foreground = %#06x, want 0xff8800", got) |
| 59 | } |
| 60 | if got := bg.Hex(); got != 0x001122 { |
| 61 | t.Errorf("background = %#06x, want 0x001122", got) |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | func TestStyleFallsBackAlongTheDots(t *testing.T) { |
| 66 | th := mustParse(t, ` |
| 67 | [colors] |
| 68 | default = { fg = "white", bg = "black" } |
| 69 | syntax = { fg = "aqua" } |
| 70 | `) |
| 71 | |
| 72 | tests := []struct { |
| 73 | key string |
| 74 | want tcell.Color |
| 75 | }{ |
| 76 | {"syntax.keyword", tcell.ColorAqua}, // falls back to "syntax" |
| 77 | {"syntax.string.raw", tcell.ColorAqua}, // two levels up |
| 78 | {"completion.item", tcell.ColorWhite}, // nothing matches, so "default" |
| 79 | {"no.such.key.at.all", tcell.ColorWhite}, // |
| 80 | } |
| 81 | |
| 82 | for _, tc := range tests { |
| 83 | fg, _, _ := th.Style(tc.key).Decompose() |
| 84 | if fg != tc.want { |
| 85 | t.Errorf("Style(%q) foreground = %v, want %v", tc.key, fg, tc.want) |
| 86 | } |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | func TestAnEntryInheritsTheHalfItLeavesOut(t *testing.T) { |
| 91 | th := mustParse(t, ` |
| 92 | [colors] |
| 93 | default = { fg = "white", bg = "navy" } |
| 94 | "syntax.comment" = { fg = "gray" } |
| 95 | `) |
| 96 | |
| 97 | fg, bg, _ := th.Style(KeySyntaxComment).Decompose() |
| 98 | if fg != tcell.ColorGray { |
| 99 | t.Errorf("foreground = %v, want gray", fg) |
| 100 | } |
| 101 | if bg != tcell.ColorNavy { |
| 102 | t.Errorf("background = %v, want the navy inherited from default", bg) |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | func TestShallowerKeysAreResolvedFirst(t *testing.T) { |
| 107 | // "syntax.keyword" must be able to inherit its background from "syntax", |
| 108 | // whatever order the entries happen to come out of the TOML map in. |
| 109 | th := mustParse(t, ` |
| 110 | [colors] |
| 111 | "syntax.keyword" = { bold = true } |
| 112 | syntax = { fg = "lime", bg = "purple" } |
| 113 | default = { fg = "white", bg = "black" } |
| 114 | `) |
| 115 | |
| 116 | fg, bg, attrs := th.Style(KeySyntaxKeyword).Decompose() |
| 117 | if fg != tcell.ColorLime || bg != tcell.ColorPurple { |
| 118 | t.Errorf("Style(syntax.keyword) = %v on %v, want lime on purple", fg, bg) |
| 119 | } |
| 120 | if attrs&tcell.AttrBold == 0 { |
| 121 | t.Error("the bold attribute of the deeper key was lost") |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | func TestAttributesAreAllSupported(t *testing.T) { |
| 126 | th := mustParse(t, ` |
| 127 | [colors] |
| 128 | default = { fg = "white", bg = "black", bold = true, underline = true, |
| 129 | italic = true, reverse = true, dim = true, blink = true } |
| 130 | `) |
| 131 | |
| 132 | _, _, attrs := th.Style(KeyDefault).Decompose() |
| 133 | for name, want := range map[string]tcell.AttrMask{ |
| 134 | "bold": tcell.AttrBold, |
| 135 | "underline": tcell.AttrUnderline, |
| 136 | "italic": tcell.AttrItalic, |
| 137 | "reverse": tcell.AttrReverse, |
| 138 | "dim": tcell.AttrDim, |
| 139 | "blink": tcell.AttrBlink, |
| 140 | } { |
| 141 | if attrs&want == 0 { |
| 142 | t.Errorf("the %s attribute was not applied", name) |
| 143 | } |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | func TestDefaultColourNamesLeaveItToTheTerminal(t *testing.T) { |
| 148 | for _, name := range []string{"default", "-", ""} { |
| 149 | th := mustParse(t, `[colors]`+"\n"+`default = { fg = "`+name+`", bg = "red" }`) |
| 150 | |
| 151 | fg, bg, _ := th.Style(KeyDefault).Decompose() |
| 152 | if fg != tcell.ColorWhite { |
| 153 | t.Errorf("fg = %q left the foreground at %v, want the inherited white", name, fg) |
| 154 | } |
| 155 | if bg != tcell.ColorRed { |
| 156 | t.Errorf("fg = %q disturbed the background: %v", name, bg) |
| 157 | } |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | func TestParseRejectsAnUnknownColour(t *testing.T) { |
| 162 | _, err := parseTheme([]byte(` |
| 163 | [colors] |
| 164 | default = { fg = "not-a-colour" } |
| 165 | `)) |
| 166 | |
| 167 | if err == nil { |
| 168 | t.Fatal("parseTheme() error = nil, want a failure naming the bad colour") |
| 169 | } |
| 170 | if !strings.Contains(err.Error(), "not-a-colour") { |
| 171 | t.Errorf("parseTheme() error = %v, want it to name the offending colour", err) |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | func TestParseRejectsInvalidTOML(t *testing.T) { |
| 176 | if _, err := parseTheme([]byte(`this is not = = toml`)); err == nil { |
| 177 | t.Fatal("parseTheme() error = nil, want a failure") |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | func TestKeysAndDefines(t *testing.T) { |
| 182 | th := mustParse(t, ` |
| 183 | [colors] |
| 184 | default = { fg = "white" } |
| 185 | syntax = { fg = "lime" } |
| 186 | `) |
| 187 | |
| 188 | if !th.Defines("syntax") { |
| 189 | t.Error(`Defines("syntax") = false, want true`) |
| 190 | } |
| 191 | if th.Defines(KeySyntaxKeyword) { |
| 192 | t.Error(`Defines("syntax.keyword") = true, want false — it is only reachable by fallback`) |
| 193 | } |
| 194 | |
| 195 | keys := th.Keys() |
| 196 | if len(keys) != 2 || keys[0] != "default" || keys[1] != "syntax" { |
| 197 | t.Errorf("Keys() = %v, want [default syntax] sorted", keys) |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | // mustParse parses a theme in a test, failing on error. |
| 202 | func mustParse(t *testing.T, content string) *Theme { |
| 203 | t.Helper() |
| 204 | th, err := parseTheme([]byte(content)) |
| 205 | if err != nil { |
| 206 | t.Fatalf("parseTheme() error = %v", err) |
| 207 | } |
| 208 | return th |
| 209 | } |
| 210 | |
| 211 | // userDir is the directory the wrappers below read the user's own themes from. |
| 212 | // It is empty unless a test called useThemeDir, so a test that does not ask for |
| 213 | // a theme directory gets the embedded themes alone — which is deterministic in |
| 214 | // a way reading the real one never was. |
| 215 | var userDir string |
| 216 | |
| 217 | // useThemeDir gives the test a theme directory of its own, and points the |
| 218 | // wrappers at it for the length of the test. |
| 219 | func useThemeDir(t *testing.T) string { |
| 220 | t.Helper() |
| 221 | dir := t.TempDir() |
| 222 | userDir = dir |
| 223 | t.Cleanup(func() { userDir = "" }) |
| 224 | return dir |
| 225 | } |
| 226 | |
| 227 | // load, available and parseTheme are the package's own functions with the test's |
| 228 | // theme directory filled in, so that the tests below read as they did when the |
| 229 | // directory came from the environment. |
| 230 | func load(name string) (*Theme, error) { return Load(name, userDir) } |
| 231 | |
| 232 | func available() []string { return Available(userDir) } |
| 233 | |
| 234 | func parseTheme(data []byte) (*Theme, error) { return Parse(data, userDir) } |
| 235 | |
| 236 | // writeTheme creates a theme file in dir. |
| 237 | func writeTheme(t *testing.T, dir, name, content string) string { |
| 238 | t.Helper() |
| 239 | path := filepath.Join(dir, name+".toml") |
| 240 | if err := os.WriteFile(path, []byte(content), 0o644); err != nil { |
| 241 | t.Fatalf("writing %s: %v", path, err) |
| 242 | } |
| 243 | return path |
| 244 | } |
| 245 | |
| 246 | func TestLoadFindsEmbeddedThemes(t *testing.T) { |
| 247 | useThemeDir(t) // an empty user directory, so only embedded themes exist |
| 248 | |
| 249 | th, err := load(DefaultName) |
| 250 | if err != nil { |
| 251 | t.Fatalf("load(%q) error = %v", DefaultName, err) |
| 252 | } |
| 253 | if th.Name() != "Turbo Classic" { |
| 254 | t.Errorf("Name() = %q, want %q", th.Name(), "Turbo Classic") |
| 255 | } |
| 256 | } |
| 257 | |
| 258 | func TestEveryEmbeddedThemeParsesAndCoversEveryKey(t *testing.T) { |
| 259 | useThemeDir(t) |
| 260 | |
| 261 | names := embeddedNames() |
| 262 | if len(names) == 0 { |
| 263 | t.Fatal("no themes are embedded in the binary") |
| 264 | } |
| 265 | |
| 266 | for _, name := range names { |
| 267 | t.Run(name, func(t *testing.T) { |
| 268 | th, err := load(name) |
| 269 | if err != nil { |
| 270 | t.Fatalf("load() error = %v", err) |
| 271 | } |
| 272 | if th.Name() == "" { |
| 273 | t.Error("the theme has no name") |
| 274 | } |
| 275 | for _, key := range allStyleKeys() { |
| 276 | if !th.Defines(key) { |
| 277 | t.Errorf("key %q is not set, so it can only be reached by fallback", key) |
| 278 | } |
| 279 | } |
| 280 | }) |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | func TestAUserThemeWinsOverAnEmbeddedOneOfTheSameName(t *testing.T) { |
| 285 | dir := useThemeDir(t) |
| 286 | writeTheme(t, dir, DefaultName, ` |
| 287 | name = "Mine" |
| 288 | [colors] |
| 289 | default = { fg = "red", bg = "black" } |
| 290 | `) |
| 291 | |
| 292 | th, err := load(DefaultName) |
| 293 | if err != nil { |
| 294 | t.Fatalf("load() error = %v", err) |
| 295 | } |
| 296 | if th.Name() != "Mine" { |
| 297 | t.Errorf("Name() = %q, want the user's theme to win", th.Name()) |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | func TestLoadOfAnUnknownThemeReportsErrNotFound(t *testing.T) { |
| 302 | useThemeDir(t) |
| 303 | |
| 304 | _, err := load("no-such-theme") |
| 305 | |
| 306 | if !errors.Is(err, ErrNotFound) { |
| 307 | t.Errorf("load() error = %v, want ErrNotFound", err) |
| 308 | } |
| 309 | } |
| 310 | |
| 311 | func TestLoadRejectsNamesThatWouldEscapeTheThemeDirectories(t *testing.T) { |
| 312 | useThemeDir(t) |
| 313 | |
| 314 | for _, name := range []string{"", "../secret", "sub/theme", `..\other`} { |
| 315 | if _, err := load(name); !errors.Is(err, ErrNotFound) { |
| 316 | t.Errorf("load(%q) error = %v, want ErrNotFound", name, err) |
| 317 | } |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | func TestInheritsFillsInTheMissingKeys(t *testing.T) { |
| 322 | dir := useThemeDir(t) |
| 323 | writeTheme(t, dir, "child", ` |
| 324 | name = "Child" |
| 325 | inherits = "turbo-classic" |
| 326 | [colors] |
| 327 | "syntax.keyword" = { fg = "red" } |
| 328 | `) |
| 329 | |
| 330 | th, err := load("child") |
| 331 | if err != nil { |
| 332 | t.Fatalf("load() error = %v", err) |
| 333 | } |
| 334 | |
| 335 | fg, _, _ := th.Style(KeySyntaxKeyword).Decompose() |
| 336 | if fg != tcell.ColorRed { |
| 337 | t.Errorf("the child's own key was lost: foreground = %v", fg) |
| 338 | } |
| 339 | if !th.Defines(KeyMenuSelected) { |
| 340 | t.Error("a key from the inherited theme is missing") |
| 341 | } |
| 342 | } |
| 343 | |
| 344 | func TestInheritsDetectsALoop(t *testing.T) { |
| 345 | dir := useThemeDir(t) |
| 346 | writeTheme(t, dir, "a", `inherits = "b"`+"\n[colors]") |
| 347 | writeTheme(t, dir, "b", `inherits = "a"`+"\n[colors]") |
| 348 | |
| 349 | _, err := load("a") |
| 350 | |
| 351 | if err == nil { |
| 352 | t.Fatal("load() error = nil, want a failure on the inheritance loop") |
| 353 | } |
| 354 | if !strings.Contains(err.Error(), "loop") { |
| 355 | t.Errorf("load() error = %v, want it to mention the loop", err) |
| 356 | } |
| 357 | } |
| 358 | |
| 359 | func TestInheritsFromAnUnknownThemeFails(t *testing.T) { |
| 360 | dir := useThemeDir(t) |
| 361 | writeTheme(t, dir, "orphan", `inherits = "nowhere"`+"\n[colors]") |
| 362 | |
| 363 | if _, err := load("orphan"); err == nil { |
| 364 | t.Fatal("load() error = nil, want a failure") |
| 365 | } |
| 366 | } |
| 367 | |
| 368 | func TestLoadFileNamesTheThemeAfterItsFile(t *testing.T) { |
| 369 | dir := t.TempDir() |
| 370 | path := writeTheme(t, dir, "unnamed", "[colors]\ndefault = { fg = \"white\" }") |
| 371 | |
| 372 | th, err := LoadFile(path, userDir) |
| 373 | if err != nil { |
| 374 | t.Fatalf("LoadFile(, userDir) error = %v", err) |
| 375 | } |
| 376 | if th.Name() != "unnamed" { |
| 377 | t.Errorf("Name() = %q, want the file's base name", th.Name()) |
| 378 | } |
| 379 | } |
| 380 | |
| 381 | func TestLoadFileOfAMissingFileFails(t *testing.T) { |
| 382 | if _, err := LoadFile(filepath.Join(t.TempDir(), "absent.toml"), userDir); err == nil { |
| 383 | t.Fatal("LoadFile() error = nil, want a failure") |
| 384 | } |
| 385 | } |
| 386 | |
| 387 | func TestAvailableMergesUserAndEmbeddedThemes(t *testing.T) { |
| 388 | dir := useThemeDir(t) |
| 389 | writeTheme(t, dir, "mine", "[colors]") |
| 390 | writeTheme(t, dir, DefaultName, "[colors]") // shadows an embedded one |
| 391 | if err := os.WriteFile(filepath.Join(dir, "notes.txt"), nil, 0o644); err != nil { |
| 392 | t.Fatalf("writing a decoy file: %v", err) |
| 393 | } |
| 394 | |
| 395 | names := available() |
| 396 | |
| 397 | if !contains(names, "mine") { |
| 398 | t.Errorf("available() = %v, want it to include the user's theme", names) |
| 399 | } |
| 400 | if !contains(names, "turbo-dark") { |
| 401 | t.Errorf("available() = %v, want it to include the embedded themes", names) |
| 402 | } |
| 403 | if countOf(names, DefaultName) != 1 { |
| 404 | t.Errorf("available() = %v, want %q listed once", names, DefaultName) |
| 405 | } |
| 406 | if contains(names, "notes") { |
| 407 | t.Errorf("available() = %v, want non-TOML files ignored", names) |
| 408 | } |
| 409 | if !isSorted(names) { |
| 410 | t.Errorf("available() = %v, want it sorted", names) |
| 411 | } |
| 412 | } |
| 413 | |
| 414 | func TestDefaultAlwaysReturnsAUsableTheme(t *testing.T) { |
| 415 | useThemeDir(t) |
| 416 | |
| 417 | th := Default(userDir) |
| 418 | |
| 419 | if th == nil { |
| 420 | t.Fatal("Default(userDir) = nil") |
| 421 | } |
| 422 | if _, bg, _ := th.Style(KeyEditorText).Decompose(); bg == tcell.ColorDefault { |
| 423 | t.Error("the default theme leaves the editor background to the terminal") |
| 424 | } |
| 425 | } |
| 426 | |
| 427 | // allStyleKeys is every key the editor asks for. An embedded theme is expected |
| 428 | // to set them all, so that adding a widget without theming it is caught here. |
| 429 | func allStyleKeys() []string { |
| 430 | return []string{ |
| 431 | KeyDefault, KeyDesktop, KeyShadow, |
| 432 | KeyMenuBar, KeyMenuItem, KeyMenuSelected, KeyMenuShortcut, KeyMenuDisabled, |
| 433 | KeyWindowFrameActive, KeyWindowFrameInactive, |
| 434 | KeyWindowTitleActive, KeyWindowTitleInactive, KeyWindowBody, |
| 435 | KeyStatusBar, KeyStatusBarKey, KeyStatusBarHint, |
| 436 | KeyScrollBar, KeyScrollBarThumb, |
| 437 | KeyDialogFrame, KeyDialogBody, KeyDialogTitle, KeyDialogLabel, |
| 438 | KeyButton, KeyButtonFocused, KeyButtonShortcut, |
| 439 | KeyInput, KeyInputFocused, KeyInputSelection, |
| 440 | KeyList, KeyListSelected, KeyListUnfocused, |
| 441 | KeyCheckbox, KeyCheckboxFocused, |
| 442 | KeyEditorText, KeyEditorSelection, KeyEditorLineNumber, KeyEditorCurrent, KeyEditorCursor, |
| 443 | KeyTerminalText, KeyTerminalCursor, |
| 444 | KeyTreeText, |
| 445 | KeyTreeDirectory, |
| 446 | KeyTreeSelected, |
| 447 | KeyTreeUnfocused, |
| 448 | KeySyntaxKeyword, KeySyntaxType, KeySyntaxBuiltin, KeySyntaxConstant, |
| 449 | KeySyntaxFunction, KeySyntaxString, KeySyntaxChar, KeySyntaxNumber, |
| 450 | KeySyntaxComment, KeySyntaxOperator, KeySyntaxPunctuation, KeySyntaxIdentifier, |
| 451 | KeySyntaxHeading, |
| 452 | KeySyntaxTag, |
| 453 | KeySyntaxAttribute, |
| 454 | KeySyntaxEmphasis, |
| 455 | KeySyntaxLink, |
| 456 | KeyCompletionFrame, KeyCompletionItem, KeyCompletionSelected, KeyCompletionDetail, |
| 457 | KeyDiagnosticError, KeyDiagnosticWarning, KeyDiagnosticInfo, |
| 458 | } |
| 459 | } |
| 460 | |
| 461 | func contains(list []string, want string) bool { |
| 462 | return countOf(list, want) > 0 |
| 463 | } |
| 464 | |
| 465 | func countOf(list []string, want string) int { |
| 466 | n := 0 |
| 467 | for _, item := range list { |
| 468 | if item == want { |
| 469 | n++ |
| 470 | } |
| 471 | } |
| 472 | return n |
| 473 | } |
| 474 | |
| 475 | func isSorted(list []string) bool { |
| 476 | for i := 1; i < len(list); i++ { |
| 477 | if list[i-1] > list[i] { |
| 478 | return false |
| 479 | } |
| 480 | } |
| 481 | return true |
| 482 | } |
| 483 | |
| 484 | func TestEveryEmbeddedThemeSetsEveryKeyItself(t *testing.T) { |
| 485 | // Defines() is satisfied by inheritance, so a theme that omits a key still |
| 486 | // passes TestEveryEmbeddedThemeParsesAndCoversEveryKey — while silently |
| 487 | // showing a colour Turbo Classic chose for its blue background. On cream |
| 488 | // or on espresso that colour can be unreadable, and nothing says so. |
| 489 | // |
| 490 | // A theme of your own may still inherit; that is what `inherits` is for. |
| 491 | // The rule is narrower: a theme shipped inside the binary is one we are |
| 492 | // answerable for, so it states its whole palette. |
| 493 | useThemeDir(t) |
| 494 | |
| 495 | for _, name := range embeddedNames() { |
| 496 | t.Run(name, func(t *testing.T) { |
| 497 | body, err := embeddedThemes.ReadFile(embeddedDir + "/" + name + ".toml") |
| 498 | if err != nil { |
| 499 | t.Fatalf("reading the embedded theme: %v", err) |
| 500 | } |
| 501 | |
| 502 | var own file |
| 503 | if _, err := toml.Decode(string(body), &own); err != nil { |
| 504 | t.Fatalf("parsing the embedded theme: %v", err) |
| 505 | } |
| 506 | for _, key := range allStyleKeys() { |
| 507 | if _, set := own.Colors[key]; !set { |
| 508 | t.Errorf("%q is left to inheritance, so this theme shows a colour chosen for another one", key) |
| 509 | } |
| 510 | } |
| 511 | }) |
| 512 | } |
| 513 | } |
| 514 | |
| 515 | // --- names a theme used to answer to ---------------------------------------- |
| 516 | |
| 517 | // A theme's name is what somebody wrote in a settings file their whole team |
| 518 | // shares. Renaming "monochrome" to "monochrome-dark" so the light one could |
| 519 | // join it as a pair would have broken every such file; the alias is what stops |
| 520 | // that, and these four tests are what stop the alias from quietly rotting. |
| 521 | |
| 522 | func TestARetiredThemeNameStillLoads(t *testing.T) { |
| 523 | old, err := Load("monochrome", "") |
| 524 | if err != nil { |
| 525 | t.Fatalf(`Load("monochrome") error = %v; a name that used to work must go on working`, err) |
| 526 | } |
| 527 | current, err := Load("monochrome-dark", "") |
| 528 | if err != nil { |
| 529 | t.Fatalf(`Load("monochrome-dark") error = %v`, err) |
| 530 | } |
| 531 | |
| 532 | if old.Name() != current.Name() { |
| 533 | t.Errorf("the retired name loads %q, want the same theme as monochrome-dark, %q", old.Name(), current.Name()) |
| 534 | } |
| 535 | if got, want := old.Style(KeyEditorText), current.Style(KeyEditorText); got != want { |
| 536 | t.Errorf("the retired name loads a different editor.text style: %v, want %v", got, want) |
| 537 | } |
| 538 | } |
| 539 | |
| 540 | func TestARetiredNameIsNotOfferedAsAThemeOfItsOwn(t *testing.T) { |
| 541 | // Otherwise the theme dialog and -list-themes would show one theme twice, |
| 542 | // under two names, and a reader would reasonably expect two themes. |
| 543 | for _, name := range Available("") { |
| 544 | if name == "monochrome" { |
| 545 | t.Error(`Available() offers "monochrome", which is a retired name rather than a theme`) |
| 546 | } |
| 547 | } |
| 548 | } |
| 549 | |
| 550 | func TestAUserThemeWinsOverARetiredName(t *testing.T) { |
| 551 | // The rule for every other name is that a file in the user's directory |
| 552 | // beats the embedded theme. A retired name must not be the exception, or |
| 553 | // somebody's own monochrome.toml would stop being read the day the alias |
| 554 | // was added. |
| 555 | dir := t.TempDir() |
| 556 | own := `name = "Mine" |
| 557 | [colors] |
| 558 | default = { fg = "#ff0000", bg = "#00ff00" } |
| 559 | ` |
| 560 | if err := os.WriteFile(filepath.Join(dir, "monochrome.toml"), []byte(own), 0o644); err != nil { |
| 561 | t.Fatal(err) |
| 562 | } |
| 563 | |
| 564 | th, err := Load("monochrome", dir) |
| 565 | if err != nil { |
| 566 | t.Fatalf("Load() error = %v", err) |
| 567 | } |
| 568 | if th.Name() != "Mine" { |
| 569 | t.Errorf("Load(%q) = %q, want the user's own file", "monochrome", th.Name()) |
| 570 | } |
| 571 | } |
| 572 | |
| 573 | func TestEveryRetiredNamePointsAtAThemeWeShip(t *testing.T) { |
| 574 | // An alias whose target is not shipped is an alias that has stopped |
| 575 | // meaning anything, and it fails at the worst moment: when a user with an |
| 576 | // old settings file starts the editor. |
| 577 | // |
| 578 | // The list is read back through Load rather than from the map, because the |
| 579 | // map is unexported — which is the point: a caller cannot add one. |
| 580 | for _, retired := range []string{"monochrome"} { |
| 581 | if _, err := Load(retired, ""); err != nil { |
| 582 | t.Errorf("the retired name %q no longer loads: %v", retired, err) |
| 583 | } |
| 584 | } |
| 585 | } |
| 586 | |
| 587 | func TestBothMonochromesAreShippedAndAreDifferentThemes(t *testing.T) { |
| 588 | dark, err := Load("monochrome-dark", "") |
| 589 | if err != nil { |
| 590 | t.Fatalf("Load(monochrome-dark) error = %v", err) |
| 591 | } |
| 592 | light, err := Load("monochrome-light", "") |
| 593 | if err != nil { |
| 594 | t.Fatalf("Load(monochrome-light) error = %v", err) |
| 595 | } |
| 596 | |
| 597 | if dark.Style(KeyEditorText) == light.Style(KeyEditorText) { |
| 598 | t.Error("the two monochromes draw the page identically; one of them is not doing its job") |
| 599 | } |
| 600 | } |