1package ui23import (4 "strings"5 "testing"67 tea "charm.land/bubbletea/v2"89 "src.rest/cat/012/internal/sheet"10 "src.rest/cat/012/internal/ui/choicebar"11)1213// rect parses a range for tests.14func rect(s string) sheet.Rect {15 r, ok := sheet.ParseRange(s)16 if !ok {17 panic(s)18 }19 return r20}2122// gridLines returns the grid's screen lines, right-trimmed.23func gridLines(m *Model) []string {24 lines := strings.Split(screen(m), "\n")25 out := lines[gridTop : gridTop+m.visibleRows()]26 for i := range out {27 out[i] = strings.TrimRight(out[i], " ")28 }29 return out30}3132func TestWrapGrowsTheRow(t *testing.T) {33 m := newModel()34 press(t, m, "one two three four five", "<enter>", "<up>")35 if got := gridLines(m)[0]; !strings.Contains(got, "one two three four five") {36 t.Fatalf("overflow: %q", got)37 }38 run(m, m.runCommand("format.wrap"))39 g := gridLines(m)40 want := []string{"one two", "three", "four", "five"}41 for i, w := range want {42 if !strings.Contains(g[i], w) {43 t.Errorf("line %d = %q, want %q", i, g[i], w)44 }45 }46 // The row number sits on the row's last line, beside its values.47 if strings.Contains(g[0], " 1 ") || !strings.HasPrefix(strings.TrimSpace(g[3]), "1 ") {48 t.Errorf("row number: %q", g[:4])49 }50 if !strings.HasPrefix(strings.TrimSpace(g[4]), "2") {51 t.Errorf("row 2 after: %q", g[4])52 }53 if !commands["format.wrap"].checked(m) || commands["format.wrap_clip"].checked(m) {54 t.Error("Wrap not checked")55 }56 // Clip keeps the text in its column.57 run(m, m.runCommand("format.wrap_clip"))58 if got := gridLines(m)[0]; !strings.Contains(got, "one two t") || strings.Contains(got, "three") {59 t.Errorf("clip: %q", got)60 }61 press(t, m, "<ctrl+z>")62 if gridLines(m)[1] == "" {63 t.Error("undo clip: not wrapped")64 }65}6667func TestRowHeightPromptAndFit(t *testing.T) {68 m := newModel()69 press(t, m, "x", "<enter>", "<up>")70 run(m, m.runCommand("row.height"))71 if m.mode != modePrompt || m.prompt.indicator != "HEIGHT" {72 t.Fatalf("mode %v", m.mode)73 }74 press(t, m, "<right>", "<right>")75 if h, _ := m.sheet.RowHeight(0); h != 3 {76 t.Errorf("preview height %d", h)77 }78 press(t, m, "<enter>")79 if m.shape(0).Lines != 3 || !strings.Contains(gridLines(m)[2], "x") {80 t.Errorf("rows: %q", gridLines(m)[:4])81 }82 press(t, m, "<ctrl+z>")83 if _, ok := m.sheet.RowHeight(0); ok {84 t.Error("undo kept the height")85 }86 press(t, m, "<ctrl+y>")87 run(m, m.runCommand("row.fit"))88 if _, ok := m.sheet.RowHeight(0); ok || m.shape(0).Lines != 1 {89 t.Error("fit to data")90 }91 run(m, m.runCommand("row.height"))92 press(t, m, "99", "<enter>")93 if m.mode != modeError {94 t.Errorf("height 99 accepted: %v", m.mode)95 }96}9798func TestResizeRowByDragging(t *testing.T) {99 m := newModel()100 handle := m.hdrW() - 1101 send(m, tea.MouseMotionMsg{X: handle, Y: gridTop + 1})102 if m.mouse.hover.kind != hitRowBorder || !strings.Contains(line(m, gridTop+1), "▄") {103 t.Fatalf("hover %v line %q", m.mouse.hover.kind, line(m, gridTop+1))104 }105 send(m, tea.MouseClickMsg(leftAt(handle, gridTop+1)))106 send(m, tea.MouseMotionMsg(leftAt(handle, gridTop+3)))107 send(m, tea.MouseReleaseMsg(leftAt(handle, gridTop+3)))108 if h, _ := m.sheet.RowHeight(1); h != 3 {109 t.Errorf("dragged height %d", h)110 }111 // Row 2's handle is now on its third line; a double click fits it.112 click(m, handle, gridTop+3, 0)113 click(m, handle, gridTop+3, 0)114 if _, ok := m.sheet.RowHeight(1); ok {115 t.Error("double-click didn't fit the row")116 }117}118119func TestBordersDraw(t *testing.T) {120 m := newModel()121 press(t, m, "a", "<tab>", "b", "<enter>", "c", "<tab>", "d", "<enter>")122 m.cur = addr("A1")123 m.selectRect(rect("A1:B2"))124 run(m, m.runCommand("format.borders_all"))125 g := gridLines(m)126 for i, want := range []string{"┌─────────┬─────────┐", "│a │b │", "├─────────┼─────────┤", "│c │d │", "└─────────┴─────────┘"} {127 if !strings.Contains(g[i], want) {128 t.Errorf("line %d = %q, want %q", i, g[i], want)129 }130 }131 run(m, m.runCommand("format.border_double"))132 run(m, m.runCommand("format.borders_outer"))133 if g := gridLines(m); !strings.Contains(g[0], "╔═════════╤═════════╗") || !strings.Contains(g[2], "╟─────────┼─────────╢") {134 t.Errorf("double outer: %q", g[:3])135 }136 run(m, m.runCommand("format.borders_clear"))137 if g := gridLines(m); strings.ContainsAny(strings.Join(g, ""), "─│═║") {138 t.Errorf("cleared: %q", g[:3])139 }140 // Text doesn't run on across a border.141 m.clearSelection()142 m.cur = addr("D5")143 press(t, m, "a long label", "<enter>")144 m.cur = addr("E5")145 run(m, m.runCommand("format.border_left"))146 if got := gridLines(m)[4]; strings.Contains(got, "a long label") || !strings.Contains(got, "a long la║") {147 t.Errorf("overflow over a border: %q", got)148 }149}150151func TestMergeCells(t *testing.T) {152 m := newModel()153 press(t, m, "Title", "<tab>", "lost", "<enter>")154 m.cur = addr("A3")155 press(t, m, "b", "<enter>", "a", "<enter>")156 m.cur = addr("A1")157 m.selectRect(rect("A1:C1"))158 run(m, m.runCommand("format.merge_all"))159 if _, ok := m.overlay.(*choicebar.Bar); !ok || !strings.Contains(line(m, contextLine), "keeps only the top-left value") ||160 !strings.Contains(line(m, m.height-1), "clears B1") {161 t.Fatalf("no warning: %q", line(m, contextLine))162 }163 press(t, m, "<enter>")164 if mg, ok := m.sheet.MergeAt(addr("C1")); !ok || mg != rect("A1:C1") || !m.sheet.Cell(addr("B1")).Blank() {165 t.Fatalf("merge %v %v", mg, ok)166 }167 // Centered across the merge.168 if got := gridLines(m)[0]; !strings.Contains(got, strings.Repeat(" ", 12)+"Title") {169 t.Errorf("centered: %q", got)170 }171 // One cell for moving, selecting and clicking.172 if m.cur != addr("A1") || m.selection() != rect("A1:C1") || m.hasRange() {173 t.Errorf("cur %v selection %v", m.cur, m.selection())174 }175 press(t, m, "<right>")176 if m.cur != addr("D1") {177 t.Errorf("right from the merge: %v", m.cur)178 }179 press(t, m, "<left>")180 if m.cur != addr("A1") {181 t.Errorf("left into the merge: %v", m.cur)182 }183 click(m, cellX(2), gridTop, 0)184 if m.cur != addr("A1") {185 t.Errorf("click in the merge: %v", m.cur)186 }187 press(t, m, "New", "<enter>")188 if m.sheet.Value(addr("A1")).Str != "New" || m.cur != addr("A2") {189 t.Errorf("edit: %v at %v", m.sheet.Value(addr("A1")), m.cur)190 }191 m.cur = addr("B4")192 press(t, m, "<shift+up>", "<shift+up>", "<shift+up>")193 if m.selection() != rect("A1:C4") {194 t.Errorf("selection grows over the merge: %v", m.selection())195 }196 // Sorting over merged cells is refused.197 run(m, m.runCommand("data.sort_range_az"))198 if m.mode != modeError || !strings.Contains(m.errMsg, "merged") {199 t.Errorf("sort: %v %q", m.mode, m.errMsg)200 }201 press(t, m, "<esc>")202 m.cur = addr("B1")203 m.clearSelection()204 m.cur = m.snap(m.cur)205 run(m, m.runCommand("format.unmerge"))206 if len(m.sheet.Merges()) != 0 {207 t.Errorf("unmerge left %v", m.sheet.Merges())208 }209 press(t, m, "<ctrl+z>")210 if len(m.sheet.Merges()) != 1 {211 t.Error("undo unmerge")212 }213}214215func TestMergeVertically(t *testing.T) {216 m := newModel()217 m.selectRect(rect("A1:B3"))218 run(m, m.runCommand("format.merge_vertical"))219 m.selectRect(rect("D2:E2"))220 run(m, m.runCommand("format.merge_all"))221 if len(m.sheet.Merges()) != 3 {222 t.Fatalf("merges %v", m.sheet.Merges())223 }224 m.cur = addr("A1")225 m.clearSelection()226 press(t, m, "<down>")227 if m.cur != addr("A4") {228 t.Errorf("down out of a vertical merge: %v", m.cur)229 }230 press(t, m, "<up>")231 if m.cur != addr("A1") {232 t.Errorf("up into a vertical merge: %v", m.cur)233 }234 // A spill can't write over merged cells, and cells an array spills235 // into can't be merged.236 m.cur = addr("D1")237 press(t, m, "=SEQUENCE(3)", "<enter>")238 if m.sheet.Value(addr("D1")).Kind != sheet.Error {239 t.Errorf("spill over merged cells: %v", m.sheet.Value(addr("D1")))240 }241 m.cur = addr("F1")242 press(t, m, "=SEQUENCE(3)", "<enter>")243 m.cur = addr("F2")244 m.selectRect(rect("F2:G2"))245 run(m, m.runCommand("format.merge_all"))246 if _, merged := m.sheet.MergeAt(addr("F2")); merged || !strings.Contains(m.note, "array") {247 t.Errorf("merged over a spill: %q", m.note)248 }249}250251func TestTallRowsScroll(t *testing.T) {252 m := newModel()253 for r := range 30 {254 m.sheet.SetRowHeight(r, r, 3)255 }256 m.sheet.Set(addr("A30"), "end")257 // Going down keeps the active cell on screen, a whole row at a time.258 for range 29 {259 press(t, m, "<down>")260 }261 b, ok := m.bandOf(29)262 if !ok || b.shown != 3 {263 t.Fatalf("row 30 shown %v %+v", ok, b)264 }265 if m.scrollRows() != m.visibleRows()/3 {266 t.Errorf("a page is %d rows", m.scrollRows())267 }268 top := m.top269 press(t, m, "<pgup>")270 if m.top != top-m.scrollRows() {271 t.Errorf("page up from %d to %d", top, m.top)272 }273 // Frozen rows keep their height.274 m.sheet.SetFrozen(1, 0)275 if m.frozenLines(1) != 3 || m.screenRows()[1] != divider {276 t.Errorf("frozen lines %d rows %v", m.frozenLines(1), m.screenRows())277 }278 // A click on any line of a tall row picks it.279 m.top = 5280 y, _ := m.rowY(5)281 click(m, cellX(0), y+2, 0)282 if m.cur.Row != 5 {283 t.Errorf("clicked row %d", m.cur.Row+1)284 }285}286287func TestRecordRowHeightDrag(t *testing.T) {288 m := newModel()289 run(m, m.runCommand("macro.record"))290 handle := m.hdrW() - 1291 send(m, tea.MouseClickMsg(leftAt(handle, gridTop)))292 send(m, tea.MouseMotionMsg(leftAt(handle, gridTop+1)))293 send(m, tea.MouseReleaseMsg(leftAt(handle, gridTop+1)))294 run(m, m.runCommand("macro.stop"))295 press(t, m, "<enter>", "<enter>")296 if got := body(mustMacro(t, m, "Macro 1").Source); got != `set_height("1", 2)` {297 t.Errorf("recorded %q", got)298 }299 r := withMacro(t, m, "Macro 1")300 run(r, r.runMacro(mustMacro(t, r, "Macro 1")))301 if h, _ := r.sheet.RowHeight(0); h != 2 {302 t.Errorf("replayed height %d (%q)", h, r.warn)303 }304}305306// A chart floats over the rows it's anchored to, however tall they are,307// and dragging it snaps its corner to the row under the mouse.308func TestChartOverTallRows(t *testing.T) {309 m := newModel()310 m.sheet.AddChart(sheet.Chart{Type: sheet.ChartColumn, Data: rect("A1:B3"), At: addr("D5"), W: 20, H: 8})311 m.sheet.SetRowHeight(1, 1, 4) // row 2 is four lines tall312 c := m.sheet.Charts()[0]313 _, y := m.chartScreen(c)314 if want, _ := m.rowY(4); y != want {315 t.Errorf("chart at line %d, row 5 at %d", y, want)316 }317 if got := m.chartCellAt(0, y); got.Row != 4 {318 t.Errorf("line %d is row %d", y, got.Row+1)319 }320 if got := m.chartCellAt(0, gridTop+2); got.Row != 1 {321 t.Errorf("the third line is row %d, want 2", got.Row+1)322 }323 m.top = 3 // scrolled past the tall row: the chart's lines follow324 if _, y := m.chartScreen(c); y != gridTop+1 {325 t.Errorf("scrolled: chart at line %d", y)326 }327 if _, y := m.chartScreen(sheet.Chart{At: addr("D2"), H: 8}); y != gridTop-5 {328 t.Errorf("above the top: chart at line %d", y)329 }330}