1package themepicker23import (4 "slices"5 "strings"6 "testing"78 tea "charm.land/bubbletea/v2"910 "src.rest/cat/012/internal/ui/lineedit"11 "src.rest/cat/012/internal/ui/overlay"12 "src.rest/cat/012/internal/ui/theme"13)1415// fakeHost records what the picker drew and kept.16type fakeHost struct {17 th theme.Theme18 line lineedit.Line19 current string20 previews []string21 kept string22 closed int23 releases bool24}2526func (f *fakeHost) Theme() *theme.Theme { return &f.th }27func (f *fakeHost) Size() (width, height int) { return 100, 30 }28func (f *fakeHost) Line() *lineedit.Line { return &f.line }29func (f *fakeHost) Close() { f.closed++ }30func (f *fakeHost) RecordAnswer(string, bool) {}31func (f *fakeHost) Current() string { return f.current }32func (f *fakeHost) ThemesDir() string { return "" }33func (f *fakeHost) Preview(name string) { f.previews = append(f.previews, name) }34func (f *fakeHost) Keep(name string) { f.kept = name }35func (f *fakeHost) KeyReleases() bool { return f.releases }36func (f *fakeHost) last() string { return f.previews[len(f.previews)-1] }37func newHost(current string) *fakeHost { return &fakeHost{th: theme.New(true), current: current} }38func down() tea.KeyPressMsg { return tea.KeyPressMsg{Code: tea.KeyDown} }39func key(code rune) tea.KeyPressMsg { return tea.KeyPressMsg{Code: code} }40func typed(s string) tea.KeyPressMsg { return tea.KeyPressMsg{Code: []rune(s)[0], Text: s} }41func press(p *Picker, keys ...tea.KeyPressMsg) {42 for _, k := range keys {43 p.Key(k)44 }45}4647func selected(p *Picker) string { return p.Selected().Title }48func outside() overlay.MouseEvent { return overlay.MouseEvent{Kind: overlay.MousePress} }49func hover(row int) overlay.MouseEvent {50 return overlay.MouseEvent{Kind: overlay.MouseMotion, Box: "picker", Row: row}51}5253func entries() []theme.Entry { return theme.List("") }54func name(i int) string { return entries()[i].Name }55func index(n string) int {56 for i, e := range entries() {57 if e.Name == n {58 return i59 }60 }61 return -162}6364func TestOpensOnTheCurrentTheme(t *testing.T) {65 want := name(3)66 h := newHost(want)67 p := New(h)68 if selected(p) != want || h.last() != want {69 t.Errorf("opened on %q, drew %v", selected(p), h.previews)70 }71}7273func TestPreviewsAsItMovesAndRestoresOnEsc(t *testing.T) {74 h := newHost(name(0))75 p := New(h)76 press(p, down())77 if h.last() != name(1) {78 t.Errorf("Down drew %v", h.previews)79 }80 press(p, key(tea.KeyEscape))81 if h.last() != "" || h.closed != 1 {82 t.Errorf("Esc: drew %v, closed %d", h.previews, h.closed)83 }84 n := len(h.previews)85 p.Mouse(hover(4))86 if len(h.previews) != n {87 t.Error("a closed picker drew a theme")88 }89}9091func TestEnterKeeps(t *testing.T) {92 h := newHost(name(0))93 p := New(h)94 press(p, down(), down(), key(tea.KeyEnter))95 if h.kept != name(2) {96 t.Errorf("kept %q", h.kept)97 }98}99100func TestClickOutsideRestores(t *testing.T) {101 h := newHost(name(0))102 p := New(h)103 press(p, down())104 p.Mouse(outside())105 if h.last() != "" || h.closed != 1 {106 t.Errorf("click outside: drew %v, closed %d", h.previews, h.closed)107 }108}109110func TestSearchPreviewsTheBestMatch(t *testing.T) {111 h := newHost(name(0))112 p := New(h)113 target := name(len(entries()) - 1)114 for _, r := range target {115 press(p, typed(string(r)))116 }117 if index(selected(p)) < 0 || h.last() != selected(p) {118 t.Errorf("searching %q selected %q, drew %v", target, selected(p), h.previews)119 }120}121122// search types s into the picker.123func search(p *Picker, s string) {124 for _, r := range s {125 press(p, typed(string(r)))126 }127}128129// shownNames are the titles the picker lists.130func shownNames(p *Picker) []string {131 var out []string132 for _, m := range p.Shown() {133 out = append(out, m.Item.Title)134 }135 return out136}137138func TestDarkOrLightFiltersByKind(t *testing.T) {139 dark := map[string]bool{}140 for _, e := range entries() {141 dark[e.Name] = e.Dark142 }143 for _, q := range []string{"light", "Dark", "light sol", "dark cat"} {144 p := New(newHost(name(0)))145 search(p, q)146 want := strings.EqualFold(q[:4], "dark")147 got := shownNames(p)148 if len(got) == 0 {149 t.Errorf("%q lists nothing", q)150 }151 for _, n := range got {152 if n == theme.Terminal || dark[n] != want {153 t.Errorf("%q lists %q (dark %v)", q, n, dark[n])154 }155 }156 }157 p := New(newHost(name(0)))158 search(p, "light")159 if slices.Contains(shownNames(p), "Bright Lights") {160 t.Error(`"light" lists the dark "Bright Lights"`)161 }162 if n := len(shownNames(p)); n >= len(entries())/2 {163 t.Errorf(`"light" lists %d of %d themes`, n, len(entries()))164 }165 p = New(newHost(name(0)))166 search(p, "light sol")167 if !strings.Contains(selected(p), "Solarized Light") {168 t.Errorf(`"light sol" selected %q`, selected(p))169 }170 // Other searches still match names fuzzily, whatever their kind.171 p = New(newHost(name(0)))172 search(p, "brightli")173 if selected(p) != "Bright Lights" {174 t.Errorf(`"brightli" selected %q`, selected(p))175 }176}