1package sortbar23import (4 "strings"5 "testing"67 tea "charm.land/bubbletea/v2"8 "github.com/charmbracelet/x/ansi"910 "src.rest/cat/012/internal/sheet"11 "src.rest/cat/012/internal/ui/overlay"12 "src.rest/cat/012/internal/ui/theme"13)1415// fakeHost is a sheet with a header row, recording the sort asked for.16type fakeHost struct {17 th theme.Theme18 s *sheet.Sheet19 w int20 closed int21 data sheet.Rect22 keys []sheet.SortKey23 answer string24}2526func newHost() *fakeHost {27 s := sheet.New()28 for i, row := range [][]string{{"Item", "Amount", "Qty"}, {"Rent", "1450", "1"}, {"Food", "300", "4"}} {29 for j, v := range row {30 s.Set(sheet.Addr{Col: j, Row: i}, v)31 }32 }33 return &fakeHost{th: theme.New(true), s: s, w: 120}34}3536func (h *fakeHost) Theme() *theme.Theme { return &h.th }37func (h *fakeHost) Size() (int, int) { return h.w, 30 }38func (h *fakeHost) Sheet() *sheet.Sheet { return h.s }39func (h *fakeHost) Close() { h.closed++ }40func (h *fakeHost) Sort(data sheet.Rect, keys []sheet.SortKey, answer string) tea.Cmd {41 h.data, h.keys, h.answer = data, keys, answer42 return nil43}4445var rng = sheet.Rect{To: sheet.Addr{Col: 2, Row: 2}}4647func press(b *Bar, keys ...tea.KeyPressMsg) {48 for _, k := range keys {49 b.Key(k)50 }51}5253var (54 right = tea.KeyPressMsg{Code: tea.KeyRight}55 space = tea.KeyPressMsg{Code: tea.KeySpace}56 enter = tea.KeyPressMsg{Code: tea.KeyEnter}57 altA = tea.KeyPressMsg{Code: 'a', Mod: tea.ModAlt}58 altH = tea.KeyPressMsg{Code: 'h', Mod: tea.ModAlt}59 letter = func(r rune) tea.KeyPressMsg { return tea.KeyPressMsg{Code: r, Text: string(r)} }60)6162func TestKeysPickColumnsAndOrder(t *testing.T) {63 h := newHost()64 b := New(h, rng, 1, 0)65 if b.Data().String() != "A2:C3" || b.Active() != (sheet.Addr{Row: 1}) {66 t.Fatalf("data %s, active %s", b.Data(), b.Active())67 }68 press(b, right, space, altA, letter('c'))69 line, _ := b.ContextLine()70 if plain := ansi.Strip(line); !strings.Contains(plain, "Sort A2:C3 by B Amount Z→A then C Qty A→Z") {71 t.Fatalf("bar %q", plain)72 }73 press(b, enter)74 if h.closed != 1 || h.data.String() != "A2:C3" || len(h.keys) != 2 || h.keys[0] != (sheet.SortKey{Col: 1, Desc: true}) || h.keys[1].Col != 2 {75 t.Fatalf("sorted %s by %v, closed %d", h.data, h.keys, h.closed)76 }77 if want := `{"by":[{"column":"B","order":"desc"},{"column":"C"}],"header":true}`; h.answer != want {78 t.Fatalf("answer %s\nwant %s", h.answer, want)79 }80}8182func TestHeaderToggle(t *testing.T) {83 h := newHost()84 b := New(h, rng, 1, 0)85 press(b, altH)86 if b.Data().String() != "A1:C3" {87 t.Fatalf("without the header row the data is %s", b.Data())88 }89 b.Mouse(overlay.MouseEvent{Kind: overlay.MousePress, X: 0, Y: overlay.ContextLine + 1})90 if h.closed != 1 {91 t.Fatalf("a click off the bar should close it")92 }93}9495func TestAnswerSortsAsPicked(t *testing.T) {96 h := newHost()97 b := New(h, rng, 1, 0)98 if _, err := b.Answer(`{"by":[{"column":"c","order":"desc"}],"header":false}`); err != nil {99 t.Fatal(err)100 }101 if h.data.String() != "A1:C3" || len(h.keys) != 1 || h.keys[0] != (sheet.SortKey{Col: 2, Desc: true}) {102 t.Fatalf("sorted %s by %v", h.data, h.keys)103 }104 for text, want := range map[string]string{105 `{"by":[]}`: "names no columns",106 `{"by":[{"column":"F"}]}`: `column "F" isn't in A1:C3`,107 `{"by":[{"column":"A","order":"up"}]}`: `the order of A is "up"`,108 `["A"]`: "the sort's answer",109 } {110 if _, err := New(h, rng, 1, 0).Answer(text); err == nil || !strings.Contains(err.Error(), want) {111 t.Errorf("Answer(%s) = %v, want %q", text, err, want)112 }113 }114}115116func TestStatusFitsNarrowScreens(t *testing.T) {117 h := newHost()118 b := New(h, rng, 1, 0)119 desc, keys := b.Status()120 if !strings.Contains(ansi.Strip(desc), "Alt+A add") || !strings.Contains(ansi.Strip(keys), "Esc") {121 t.Fatalf("wide status %q %q", desc, keys)122 }123 h.w = 30124 desc, keys = b.Status()125 if ansi.Strip(desc) != "" || ansi.StringWidth(keys) > 30 {126 t.Fatalf("narrow status %q %q", desc, keys)127 }128}