1package client
2
3import (
4 "regexp"
5 "testing"
6
7 "github.com/stretchr/testify/require"
8
9 ansirenderer "github.com/bnema/vev-vt/ansi"
10 "github.com/bnema/vev/internal/domain"
11 "github.com/bnema/vev/internal/protocol"
12 "github.com/bnema/vev/internal/usecase/picker"
13)
14
15func TestPickerStaysLegibleInReducedColor(t *testing.T) {
16 loop := pickerLoopFixture(t)
17 size := domain.Size{Cols: 100, Rows: 30}
18
19 ansi16 := string(newPickerRenderer(ansirenderer.ColorProfileANSI16).render(loop, size, emptyPickerPreview()))
20 require.NotRegexp(t, `(?:38|48|58);[25];`, ansi16, "ansi16 picker emitted extended colors")
21 require.Regexp(t, `\x1b\[0;[0-9;]*7`, ansi16, "the selected row must stay reverse video")
22
23 mono := string(newPickerRenderer(ansirenderer.ColorProfileMonochrome).render(loop, size, emptyPickerPreview()))
24 require.NotRegexp(t, `\x1b\[[0-9;:]*(?:38|48|58|3[0-7]|4[0-7]|9[0-7]|10[0-7])[;:m]`, mono, "monochrome picker emitted a color")
25 require.Regexp(t, `\x1b\[0;[0-9;]*7`, mono, "the selected row must stay reverse video")
26 require.Contains(t, mono, "work")
27}
28
29func TestToastBorderFollowsColorProfile(t *testing.T) {
30 tests := []struct {
31 name string
32 profile ansirenderer.ColorProfile
33 severity domain.NoticeSeverity
34 want string
35 }{
36 {name: "truecolor error", profile: ansirenderer.ColorProfileTrueColor, severity: domain.NoticeError, want: "\x1b[38;5;167m"},
37 {name: "256 warn", profile: ansirenderer.ColorProfileANSI256, severity: domain.NoticeWarn, want: "\x1b[38;5;179m"},
38 {name: "256 info", profile: ansirenderer.ColorProfileANSI256, severity: domain.NoticeInfo},
39 {name: "16 error", profile: ansirenderer.ColorProfileANSI16, severity: domain.NoticeError, want: "\x1b[31m"},
40 {name: "16 warn", profile: ansirenderer.ColorProfileANSI16, severity: domain.NoticeWarn, want: "\x1b[33m"},
41 {name: "16 info", profile: ansirenderer.ColorProfileANSI16, severity: domain.NoticeInfo},
42 {name: "mono error", profile: ansirenderer.ColorProfileMonochrome, severity: domain.NoticeError, want: "\x1b[1m"},
43 {name: "mono warn", profile: ansirenderer.ColorProfileMonochrome, severity: domain.NoticeWarn, want: "\x1b[1m"},
44 {name: "mono info", profile: ansirenderer.ColorProfileMonochrome, severity: domain.NoticeInfo},
45 }
46 for _, tt := range tests {
47 t.Run(tt.name, func(t *testing.T) {
48 require.Equal(t, tt.want, toastBorderSGRFor(tt.severity, tt.profile))
49 })
50 }
51 // Monochrome borders are closed with normal intensity, not default color.
52 bounds := domain.Rect{Width: 12, Height: 3}
53 lines := clientToastLines(bounds, "hi", toastBorderSGRFor(domain.NoticeError, ansirenderer.ColorProfileMonochrome))
54 require.Contains(t, lines[0], "\x1b[22m")
55 require.NotContains(t, lines[0], "\x1b[39m")
56}
57
58// TestPickerProblemDotsKeepMeaningPerProfile checks how each profile encodes
59// the problem dots: truecolor and 256 keep the fixed indexed colors, ANSI-16 is
60// quantized by the renderer, and monochrome marks error and warning dots bold
61// since no color survives. Informational statuses never gain weight.
62func TestPickerProblemDotsKeepMeaningPerProfile(t *testing.T) {
63 snapshot := pickerSnapshotFixture()
64 snapshot.Lines = []protocol.PickerLine{
65 {Key: "aa/err", Kind: protocol.PickerLineSession, Label: "err", Focusable: true, Actions: protocol.PickerCanNavigate, Status: protocol.PickerLineStatusError},
66 {Key: "bb/warn", Kind: protocol.PickerLineSession, Label: "warn", Focusable: true, Actions: protocol.PickerCanNavigate, Status: protocol.PickerLineStatusStale},
67 {Key: "cc/ver", Kind: protocol.PickerLineSession, Label: "ver", Focusable: true, Actions: protocol.PickerCanNavigate, Status: protocol.PickerLineStatusVersion},
68 }
69 snapshot.Cursor = protocol.PickerCursor{Key: "aa/err"}
70 // dot returns the SGR group in effect for the dot on the row labelled label.
71 dot := func(out, label string) string {
72 row := regexp.MustCompile(`(?s)` + label + `[^●]*(\x1b\[[0-9;]*m)[^●\x1b]*●`)
73 m := row.FindStringSubmatch(out)
74 require.NotNil(t, m, "no problem dot after %q in %q", label, out)
75 return m[1]
76 }
77 tests := []struct {
78 name string
79 profile ansirenderer.ColorProfile
80 want map[string]string // label -> exact SGR before the dot
81 }{
82 {name: "truecolor", profile: ansirenderer.ColorProfileTrueColor, want: map[string]string{"err": "\x1b[0;38;5;167m", "warn": "\x1b[0;38;5;179m", "ver": "\x1b[0;38;5;170m"}},
83 {name: "ansi256", profile: ansirenderer.ColorProfileANSI256, want: map[string]string{"err": "\x1b[0;38;5;167m", "warn": "\x1b[0;38;5;179m", "ver": "\x1b[0;38;5;170m"}},
84 {name: "ansi16", profile: ansirenderer.ColorProfileANSI16, want: map[string]string{"err": "", "warn": "", "ver": ""}},
85 {name: "monochrome", profile: ansirenderer.ColorProfileMonochrome, want: map[string]string{"err": "\x1b[0;1m", "warn": "\x1b[0;1m", "ver": "\x1b[0m"}},
86 }
87 for _, tt := range tests {
88 t.Run(tt.name, func(t *testing.T) {
89 loop := pickerLoopFromSnapshot(snapshot, protocol.PickerIntentNavigation, picker.SortRecent)
90 out := string(newPickerRenderer(tt.profile).render(loop, domain.Size{Cols: 100, Rows: 30}, emptyPickerPreview()))
91 for label, want := range tt.want {
92 got := dot(out, label)
93 if want == "" {
94 // ANSI-16 colors are chosen by the renderer's quantizer: only
95 // require a basic or bright palette slot, never extended color.
96 require.Regexp(t, `^\x1b\[0;(3[0-7]|9[0-7])m$`, got, label)
97 continue
98 }
99 require.Equal(t, want, got, label)
100 }
101 })
102 }
103}