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 / nbgrid_test.go
nbgrid_test.go 6.6K
1package ui23import (4 "fmt"5 "strings"6 "testing"78 tea "charm.land/bubbletea/v2"910 "src.rest/cat/012/internal/notebook"11 "src.rest/cat/012/internal/sheet"12)1314// bigTable is an output of n rows of four columns, as nu prints it.15func bigTable(n int) string {16 var b strings.Builder17 b.WriteString("[[name, size, when, ok]; ")18 for i := range n {19 fmt.Fprintf(&b, "[file%d.txt, %dkb, 2026-09-%02dT10:00:00Z, true], ", i, i%900, i%28+1)20 }21 b.WriteString("[last, 1b, 2026-09-01T00:00:00Z, false]]")22 return b.String()23}2425// outputModel is a notebook of one cell named files whose output is26// out, w by h, the output selected.27func outputModel(out string, w, h int) *Model {28 m := sized(sheet.New(), w, h)29 m.OpenNotebook()30 id := m.book().NewCellID()31 m.sheet.SetNotebookCells("cells", []notebook.Cell{{ID: id, Source: "files = ls"}})32 m.book().SetOutput(id, &notebook.Output{NUON: []byte(out), Count: 1})33 m.nbView().Select(0, true)34 return m35}3637// entered is a notebook with lsOut's output, its grid entered.38func entered(t *testing.T) (*Model, *outGrid) {39 t.Helper()40 m := outputModel(lsOut, 80, 24)41 press(t, m, "<enter>")42 g := m.gridIn()43 if g == nil {44 t.Fatalf("Enter didn't enter the output:\n%s", screen(m))45 }46 return m, g47}4849func TestOutputGridLooks(t *testing.T) {50 m := outputModel(lsOut, 80, 24)51 s := screen(m)52 for _, want := range []string{"Out[1]: name size", "1 a.txt 2.0 kB", "3 c.txt 5.0 kB"} {53 if !strings.Contains(s, want) {54 t.Errorf("lacks %q:\n%s", want, s)55 }56 }57 if strings.Contains(s, " A ") || strings.Contains(s, " C ") {58 t.Errorf("the columns are lettered, or drawn past the table:\n%s", s)59 }60 rec := outputModel("{name: a.txt, size: 2kb}", 80, 24)61 if s := screen(rec); !strings.Contains(s, "field value") || !strings.Contains(s, "2 size 2.0 kB") {62 t.Errorf("a record's grid:\n%s", s)63 }64 if strings.Contains(line(m, menuLine), "OUTPUT") {65 t.Error("selecting the output entered it")66 }67}6869func TestOutputGridSelectsAndCopies(t *testing.T) {70 m, g := entered(t)71 if !strings.Contains(line(m, menuLine), "OUTPUT") {72 t.Errorf("mode: %q", line(m, menuLine))73 }74 press(t, m, "<down>", "<shift+right>", "<shift+down>")75 if got := g.child.selection(); got.String() != "A3:B4" {76 t.Errorf("selected %s", got)77 }78 if st := line(m, m.height-1); !strings.Contains(st, "name to size, rows 2 to 3") || !strings.Contains(st, "Count 4") {79 t.Errorf("status: %q", st)80 }81 _, cmd := m.Update(tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl})82 if got := fmt.Sprint(cmd()); !strings.Contains(got, "b.txt\t10\nc.txt\t5000") {83 t.Errorf("the system clipboard: %q", got)84 }85 // Ctrl+V on a sheet pastes what was copied, formats and all.86 press(t, m, "<esc>", "<esc>")87 run(m, m.runCommand("sheet.new"))88 press(t, m, "<ctrl+v>")89 if got := m.sheet.ShownText(addr("B2")); got != "5.0 kB" {90 t.Errorf("pasted %q", got)91 }92}9394func TestOutputGridSortsFiltersFinds(t *testing.T) {95 m, g := entered(t)96 press(t, m, "<right>")97 run(m, m.runCommand("data.sort_sheet_za"))98 if got := line(m, 7); !strings.Contains(got, "1 c.txt") {99 t.Errorf("sorted by size, Z to A:\n%s", screen(m))100 }101 if !strings.Contains(string(m.book().Output(m.sheet.NotebookCells()[0].ID).NUON), "a.txt, 2kb") {102 t.Error("sorting changed the output")103 }104 press(t, m, "<ctrl+z>")105 if got := line(m, 7); !strings.Contains(got, "1 a.txt") {106 t.Errorf("undo takes back the sort:\n%s", screen(m))107 }108 run(m, m.runCommand("data.filter"))109 var hide []string110 for _, v := range g.child.sheet.FilterValues(1) {111 if v.Label != "10 B" {112 hide = append(hide, v.Text)113 }114 }115 g.child.sheet.FilterColumn(1, sheet.Criteria{Hidden: hide})116 if s := screen(m); !strings.Contains(s, "2 b.txt") || strings.Contains(s, "a.txt") {117 t.Errorf("filtered to 10 B:\n%s", s)118 }119 run(m, m.runCommand("data.filter_remove"))120 press(t, m, "<ctrl+f>", "c.t")121 if !strings.Contains(line(m, menuLine), "FIND") || g.child.cur != addr("A4") {122 t.Errorf("find: %s at %s\n%s", line(m, menuLine), g.child.cur, screen(m))123 }124}125126func TestOutputGridRefusesEdits(t *testing.T) {127 m, g := entered(t)128 press(t, m, "x")129 if !strings.Contains(line(m, contextLine), "can't be changed") || g.child.mode != modeReady {130 t.Errorf("typing: %q", line(m, contextLine))131 }132 press(t, m, "<delete>")133 run(m, m.runCommand("format.bold"))134 if got := g.child.sheet.ShownText(addr("A2")); got != "a.txt" || g.child.sheet.CellStyle(addr("A2")).Bold {135 t.Errorf("the output changed: %q", got)136 }137 if commands["clear"].available(m) || !commands["data.sort_sheet_az"].available(m) {138 t.Error("the menus offer what the grid refuses")139 }140}141142func TestOutputGridFullScreenAndBack(t *testing.T) {143 m, g := entered(t)144 press(t, m, "<enter>")145 if !m.nbView().FullOpen() || !strings.HasPrefix(strings.TrimSpace(line(m, nbBody)), "name") {146 t.Fatalf("Enter didn't show the grid full-screen:\n%s", screen(m))147 }148 press(t, m, "<esc>")149 if m.nbView().FullOpen() || m.gridIn() != g {150 t.Error("Esc from full-screen left the grid")151 }152 press(t, m, "<esc>")153 if m.gridIn() != nil || !strings.Contains(line(m, menuLine), "NOTEBOOK") {154 t.Error("Esc didn't leave the grid")155 }156}157158func TestOutputGridMouse(t *testing.T) {159 m := outputModel(lsOut, 80, 24)160 y := strings.Count(screen(m)[:strings.Index(screen(m), "b.txt")], "\n")161 x := screenX(t, m, y, "b.txt")162 send(m, tea.MouseClickMsg{X: x, Y: y, Button: tea.MouseLeft})163 send(m, tea.MouseReleaseMsg{X: x, Y: y, Button: tea.MouseLeft})164 g := m.gridIn()165 if g == nil || g.child.cur != addr("A3") {166 t.Fatalf("a click didn't enter the grid at b.txt:\n%s", screen(m))167 }168 // Drag the border right of name to widen it.169 w := g.child.sheet.ColWidth(0)170 bx := x + w - 2171 send(m, tea.MouseClickMsg{X: bx, Y: y - 2, Button: tea.MouseLeft})172 send(m, tea.MouseMotionMsg{X: bx + 4, Y: y - 2, Button: tea.MouseLeft})173 send(m, tea.MouseReleaseMsg{X: bx + 4, Y: y - 2, Button: tea.MouseLeft})174 if got := g.child.sheet.ColWidth(0); got != w+4 {175 t.Errorf("the column is %d wide, was %d", got, w)176 }177}178179func TestOutputGridChartsOnASheet(t *testing.T) {180 m, _ := entered(t)181 run(m, m.runCommand("insert.chart"))182 if m.sheet.IsNotebook() || m.sheet.Name() != "files" || !strings.Contains(line(m, menuLine), "CHART") {183 t.Fatalf("the chart isn't on the output's sheet:\n%s", screen(m))184 }185 if _, r, ok := m.book().Region("files"); !ok || !r.Output {186 t.Error("the output wasn't sent to the sheet")187 }188}189190// A long output scrolls in its window and jumps to its end.191func TestOutputGridLong(t *testing.T) {192 if testing.Short() {193 t.Skip("reads 100,000 rows")194 }195 m := outputModel(bigTable(100000), 120, 40)196 press(t, m, "<enter>", "<ctrl+down>")197 if s := screen(m); !strings.Contains(s, "100001 last") || !strings.Contains(s, "rows 99,992 to 100,001 of 100,001") {198 t.Errorf("the end:\n%s", s)199 }200}