cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / ui / panes_test.go
panes_test.go 6.7K
1package ui23import (4 "fmt"5 "strings"6 "testing"7 "time"89 tea "charm.land/bubbletea/v2"1011 "src.rest/cat/012/internal/sheet"12)1314// table fills n rows below a header "Item, Qty": Row 1..n and the row15// number mod 4.16func table(t *testing.T, m *Model, n int) {17 t.Helper()18 s := m.sheet19 s.Set(addr("A1"), "Item")20 s.Set(addr("B1"), "Qty")21 for i := range n {22 s.Set(sheet.Addr{Row: i + 1}, fmt.Sprintf("Row %d", i+1))23 s.Set(sheet.Addr{Col: 1, Row: i + 1}, fmt.Sprint((i+1)%4))24 }25 s.ClearHistory()26}2728func TestFrozenPanesStayWhileScrolling(t *testing.T) {29 m := newModel() // 80x20: 15 grid lines30 table(t, m, 40)31 m.runCommand("view.freeze_rows1")32 m.runCommand("view.freeze_cols1")33 if m.note != "Froze 1 column" || !m.changed {34 t.Errorf("note %q changed %v", m.note, m.changed)35 }36 press(t, m, "<ctrl+end>")37 if m.cur != addr("B41") {38 t.Fatalf("cur %v", m.cur)39 }40 // Row 1 and column A stay; a divider line and column separate them.41 if l := line(m, gridTop); !strings.HasPrefix(l, " 1 Item │ Qty") {42 t.Errorf("frozen row %q", l)43 }44 if l := line(m, gridTop+1); !strings.HasPrefix(l, "────────────────┼───") {45 t.Errorf("divider %q", l)46 }47 if l := line(m, gridTop+14); !strings.HasPrefix(l, " 41 Row 40 │ 0") {48 t.Errorf("last row %q", l)49 }50 if m.top != 28 {51 t.Errorf("top %d, want 28 (13 scrolling rows ending at 41)", m.top)52 }53 // Moving up into the frozen row scrolls back to the top, as in Sheets.54 press(t, m, "<ctrl+up>")55 if m.cur != addr("B1") || m.top != 1 {56 t.Errorf("ctrl+up: cur %v top %d", m.cur, m.top)57 }58 press(t, m, "<down>")59 if m.cur != addr("B2") || m.top != 1 {60 t.Errorf("down from the frozen row: cur %v top %d", m.cur, m.top)61 }62 // Horizontal scrolling keeps column A.63 for range 10 {64 press(t, m, "<right>")65 }66 if l := line(m, headerLine); !strings.HasPrefix(l, " A │") || strings.Contains(l, " B ") {67 t.Errorf("header %q", l)68 }69 // Undo unfreezes.70 press(t, m, "<ctrl+z>")71 if _, c := m.sheet.Frozen(); c != 0 || m.note != "Undid: freeze 1 column" {72 t.Errorf("undo: %d frozen columns, note %q", c, m.note)73 }74}7576func TestFrozenPanesShrinkToFit(t *testing.T) {77 m := newModel()78 m.cur = addr("A30")79 m.runCommand("view.freeze_rows_cur")80 if r, _ := m.frozen(); r != 13 || !strings.Contains(m.note, "13 fit in this window") {81 t.Errorf("frozen %d, note %q", r, m.note)82 }83 if got := len(m.screenRows()); got != m.visibleRows() {84 t.Errorf("%d screen rows", got)85 }86}8788func TestMouseWithFrozenPanes(t *testing.T) {89 m := newModel()90 table(t, m, 40)91 m.sheet.SetFrozen(2, 1)92 m.top, m.left = 20, 393 divX := minRowHdrW + sheet.DefaultWidth // the column divider94 tests := []struct {95 x, y int96 want hit97 }{98 {minRowHdrW + 1, gridTop, hit{kind: hitCell, addr: addr("A1")}},99 {minRowHdrW + 1, gridTop + 1, hit{kind: hitCell, addr: addr("A2")}},100 {minRowHdrW + 1, gridTop + 2, hit{}}, // the divider line101 {minRowHdrW + 1, gridTop + 3, hit{kind: hitCell, addr: addr("A21")}},102 {divX, gridTop + 3, hit{}},103 {divX + 1, gridTop + 3, hit{kind: hitCell, addr: addr("D21")}},104 {divX + 1, gridTop, hit{kind: hitCell, addr: addr("D1")}},105 {2, gridTop + 3, hit{kind: hitRowHeader, addr: sheet.Addr{Col: 3, Row: 20}}},106 {divX + sheet.DefaultWidth, headerLine, hit{kind: hitColBorder, addr: sheet.Addr{Col: 3, Row: 20}}},107 {minRowHdrW + sheet.DefaultWidth - 1, headerLine, hit{kind: hitColBorder, addr: sheet.Addr{Col: 0, Row: 20}}},108 }109 for _, tt := range tests {110 if got := m.hitTest(tt.x, tt.y); got != tt.want {111 t.Errorf("hitTest(%d, %d) = %+v, want %+v", tt.x, tt.y, got, tt.want)112 }113 }114 // Resizing a scrolling column measures from where it's drawn.115 send(m, tea.MouseClickMsg(leftAt(divX+sheet.DefaultWidth, headerLine)))116 send(m, tea.MouseMotionMsg(leftAt(divX+14, headerLine)))117 send(m, tea.MouseReleaseMsg(leftAt(divX+14, headerLine)))118 if w := m.sheet.ColWidth(3); w != 14 {119 t.Errorf("D width %d", w)120 }121 // The context menu opens under the clicked cell.122 x, y := m.cellPos(addr("D22"))123 if x != divX+1 || y != gridTop+4 {124 t.Errorf("cellPos D22 = %d, %d", x, y)125 }126}127128func TestDragIntoFrozenRowsScrollsBack(t *testing.T) {129 m := newModel()130 table(t, m, 40)131 m.sheet.SetFrozen(1, 0)132 m.top = 6133 send(m, tea.MouseClickMsg(leftAt(cellX(1), gridTop+4)))134 if m.cur != addr("B9") {135 t.Fatalf("cur %v", m.cur)136 }137 // Dragging onto the frozen header row scrolls up instead of jumping138 // to row 1.139 send(m, tea.MouseMotionMsg(leftAt(cellX(1), gridTop)))140 if m.ext != addr("B7") || !m.mouse.autoscrolling {141 t.Fatalf("ext %v autoscrolling %v", m.ext, m.mouse.autoscrolling)142 }143 for i := 0; i < 10 && m.mouse.autoscrolling; i++ {144 send(m, autoscrollMsg{})145 }146 if m.top != 1 || m.ext.Row > 1 {147 t.Errorf("after autoscroll: top %d ext %v", m.top, m.ext)148 }149 send(m, tea.MouseReleaseMsg(leftAt(cellX(1), gridTop)))150 if r := m.selection(); r.From.Row > 1 || r.To != addr("B9") {151 t.Errorf("selection %v", r)152 }153}154155func TestNavigationSkipsFilteredRows(t *testing.T) {156 m := newModel()157 table(t, m, 40)158 m.sheet.CreateFilter(sheet.Rect{To: addr("B41")})159 m.sheet.FilterColumn(1, sheet.Criteria{Hidden: []string{"1", "2", "3"}}) // rows 5, 9, ..., 41 show160 press(t, m, "<ctrl+home>", "<down>")161 if m.cur != addr("A5") {162 t.Errorf("down: %v", m.cur)163 }164 press(t, m, "<down>", "<up>", "<up>")165 if m.cur != addr("A1") {166 t.Errorf("down, up, up: %v", m.cur)167 }168 press(t, m, "<ctrl+down>")169 if m.cur != addr("A41") {170 t.Errorf("ctrl+down: %v", m.cur)171 }172 // Row numbers show the gaps; the header marks the filtered column.173 press(t, m, "<ctrl+home>")174 var nums []string175 for i := range 4 {176 nums = append(nums, strings.Fields(line(m, gridTop+i))[0])177 }178 if strings.Join(nums, " ") != "1 5 9 13" {179 t.Errorf("row numbers %v", nums)180 }181 if l := line(m, headerLine); !strings.Contains(l, "A ▾") || !strings.Contains(l, "B ▼") {182 t.Errorf("header %q", l)183 }184 if !strings.Contains(line(m, m.height-1), "Filter hides 30 rows") {185 t.Errorf("status %q", line(m, m.height-1))186 }187 // Page down moves a screenful (15) of visible rows: ten in the188 // filter, then five below it.189 press(t, m, "<pgdown>")190 if m.cur != addr("A46") {191 t.Errorf("pgdown: %v", m.cur)192 }193 // Clicking a row maps through the gaps.194 press(t, m, "<ctrl+home>")195 click(m, cellX(0), gridTop+2, 0)196 if m.cur != addr("A9") {197 t.Errorf("click: %v", m.cur)198 }199}200201func TestFilteredNavigationIsFast(t *testing.T) {202 m := newModel()203 table(t, m, 8190)204 m.sheet.SetFrozen(1, 1)205 m.sheet.CreateFilter(sheet.Rect{To: sheet.Addr{Col: 1, Row: sheet.MaxRows - 1}}) // whole columns206 m.sheet.FilterColumn(1, sheet.Criteria{Hidden: []string{"1", "2", "3"}})207 m.View()208 start := time.Now()209 for _, k := range []string{"<down>", "<pgdown>", "<ctrl+down>", "<ctrl+up>", "<pgup>", "<up>"} {210 press(t, m, k)211 m.View()212 }213 if d := time.Since(start) / 6; d > 20*time.Millisecond {214 t.Errorf("a keystroke took %v", d)215 }216}