// Terminal color reply parsing and contrast across palettes cannot be // exercised by daemon E2E: they need a terminal that answers OSC queries, and // the e2e harness has no PTY. package tui import ( "math" "testing" ) func hexColor(t *testing.T, h string) rgb { c := parseColors("\x1b]10;rgb:" + h[1:3] + "/" + h[3:5] + "/" + h[5:7] + "\x07").fg if c == nil { t.Fatalf("could not parse %s", h) } return *c } func TestParseColorReplies(t *testing.T) { c := parseColors("\x1b]10;rgb:cdcd/d6d6/f4f4\x1b\\\x1b]11;rgb:1e/1e/2e\x07\x1b]4;4;rgb:8989/b4b4/fafa\x1b\\\x1b]4;5;rgb:f5f5/c2c2/e7e7\x1b\\\x1b[?62;22c") if _, blue := c.palette[ansiBlue]; c.fg == nil || c.bg == nil || !blue || len(c.palette) != 2 { t.Fatalf("missing colors: %+v", c) } if got := c.fg.hex(); got != "#cdd6f4" { t.Fatalf("fg %s", got) } if got := c.bg.hex(); got != "#1e1e2e" { t.Fatalf("bg %s", got) } if got := parseColors("\x1b[?62;22c"); got.fg != nil || got.bg != nil { t.Fatal("a terminal that only answers device attributes reports no colors") } } func TestInkHitsItsContrastTargets(t *testing.T) { themes := map[string][2]string{ "mocha": {"#cdd6f4", "#1e1e2e"}, "latte": {"#4c4f69", "#eff1f5"}, "black": {"#ffffff", "#000000"}, } for name, theme := range themes { fg, bg := hexColor(t, theme[0]), hexColor(t, theme[1]) mixed, ok := mixInk(termColors{fg: &fg, bg: &bg}) if !ok { t.Fatal("colors were reported") } for _, step := range []struct { hex string target float64 }{{mixed.code, codeLc}, {mixed.secondary, secondaryLc}, {mixed.decor, decorLc}} { if got := apca(hexColor(t, step.hex), bg); math.Abs(got-step.target) > 1.5 { t.Errorf("%s: %s has Lc %.1f, want %v", name, step.hex, got, step.target) } } } soft, bg := hexColor(t, "#b3bbd7"), hexColor(t, "#1e1e2e") if mixed, _ := mixInk(termColors{fg: &soft, bg: &bg}); mixed.code != soft.hex() { t.Fatalf("text already under a target keeps its color, got %s", mixed.code) } black := hexColor(t, "#000000") if _, ok := mixInk(termColors{fg: &black, bg: &bg}); ok { t.Fatal("a text color barely apart from the background is not trusted") } if _, ok := mixInk(termColors{}); ok { t.Fatal("no reply leaves the palette fallbacks in place") } } func TestLanguageColorsReachTheirContrastOnEveryTheme(t *testing.T) { colors := []string{"#00ADD8", "#3572A5", "#000080", "#f1e05a", "#555555", "#ffaff3"} for _, hex := range languageHues { colors = append(colors, hex) } for _, bgHex := range []string{"#1e1e2e", "#eff1f5", "#000000", "#ffffff"} { bg := hexColor(t, bgHex) for _, hex := range colors { c := hexColor(t, hex) got := legible(c, bg, languageLc) if lc := apca(got, bg); lc < languageLc-1 { t.Errorf("%s on %s became %s with Lc %.1f, want %d", hex, bgHex, got.hex(), lc, languageLc) } if apca(c, bg) >= languageLc && got != c { t.Errorf("%s is legible on %s and should stay, got %s", hex, bgHex, got.hex()) } } } }