cat / 012
clone

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

cat/module-path
012 / e2e / app_test.go
app_test.go 7.4K
1package e2e23import (4 "fmt"5 "os"6 "path/filepath"7 "strings"8 "testing"910 ghostty "go.mitchellh.com/libghostty"11)1213// Screen layout: menu bar 0, formula bar 1 (an 11-wide name box and a14// space, then the contents), context line 2, column header 3, grid row 115// on line 4.16const (17 barLine = 118 barX = 1219 contextLine = 220 gridRow1 = 421)2223// waitForBar waits for the formula bar to show name in the name box and24// text after it.25func (s *session) waitForBar(name, text string) {26 s.t.Helper()27 s.eventually(fmt.Sprintf("formula bar %s %q", name, text), func() bool {28 l := s.line(barLine) + strings.Repeat(" ", barX)29 return strings.TrimSpace(l[:barX]) == name && strings.TrimRight(l[barX:], " ") == text30 })31}3233// waitForName waits for the name box to show name.34func (s *session) waitForName(name string) {35 s.t.Helper()36 s.eventually(fmt.Sprintf("name box %s", name), func() bool {37 l := s.line(barLine)38 return strings.TrimSpace(l[:min(barX, len(l))]) == name39 })40}4142// waitForEntry waits for the formula bar to show text, whatever the name43// box says; used while typing.44func (s *session) waitForEntry(text string) {45 s.t.Helper()46 s.eventually(fmt.Sprintf("formula bar %q", text), func() bool {47 l := s.line(barLine)48 return len(l) >= barX && l[barX:] == text49 })50}5152// numRow is how a grid row of numbers in default-width (10) columns looks:53// a 6-wide row header, then each number right-aligned with one column of54// padding.55func numRow(row int, nums ...string) string {56 line := fmt.Sprintf("%5d ", row)57 for _, n := range nums {58 line += fmt.Sprintf("%9s ", n)59 }60 return strings.TrimRight(line, " ")61}6263func TestFormulaRecalc(t *testing.T) {64 t.Parallel()65 s := start(t, "")66 s.keys("10", "<enter>", "20", "<enter>", "=SUM(A1:A2)", "<enter>")67 s.keys("<up>")68 s.waitForBar("A3", "=SUM(A1:A2)")69 s.waitForLine(gridRow1+2, numRow(3, "30"))7071 // Changing a precedent recalculates the total on screen.72 s.keys("<ctrl+home>", "5", "<enter>")73 s.waitForLine(gridRow1+2, numRow(3, "25"))74}7576func TestPointModeBuildsFormula(t *testing.T) {77 t.Parallel()78 s := start(t, "")79 s.keys("4", "<tab>", "6", "<tab>", "=", "<left>", "<left>")80 s.waitFor("POINT")81 s.waitForBar("C1", "=A1")82 s.keys("*", "<left>", "<enter>")83 // Enter after a run of Tabs returns to the starting column, as in Sheets.84 s.keys("<up>", "<right>", "<right>")85 s.waitForBar("C1", "=A1*B1")86 s.waitForLine(gridRow1, numRow(1, "4", "6", "24"))87}8889func TestShiftSelectionShowsStats(t *testing.T) {90 t.Parallel()91 s := start(t, "")92 s.keys("1", "<enter>", "2", "<enter>", "3", "<enter>", "<ctrl+home>")93 s.keys("<shift+down>", "<shift+down>")94 s.eventually("selection stats", func() bool {95 l := s.line(29)96 return strings.Contains(l, "A1:A3") && strings.Contains(l, "Sum 6") && strings.Contains(l, "Count 3")97 })98 s.keys("<delete>")99 s.waitForLine(gridRow1, " 1")100}101102func TestTextOverflow(t *testing.T) {103 t.Parallel()104 s := start(t, "")105 s.keys("Quarterly revenue", "<enter>", "$1,200", "<enter>", "TRUE", "<enter>")106 s.waitForLine(gridRow1, " 1 Quarterly revenue")107 s.waitForLine(gridRow1+1, numRow(2, "$1,200"))108 s.waitForLine(gridRow1+2, " 3 TRUE")109}110111func TestInvalidFormulaCursor(t *testing.T) {112 t.Parallel()113 s := start(t, "")114 s.keys("=SUM(A1", "<enter>")115 s.waitFor("EDIT")116 s.waitFor("Expected , or ) in SUM")117 // The real terminal cursor sits on the edit line at the error.118 s.eventually("cursor at end of edit line", func() bool {119 x, y := s.cursor()120 return x == barX+len("=SUM(A1") && y == barLine121 })122}123124func TestSaveQuitReopen(t *testing.T) {125 t.Parallel()126 dir := t.TempDir()127 s := start(t, dir)128 s.keys("Budget", "<enter>", "1200", "<enter>", "=A2*12", "<enter>")129 s.keys("<ctrl+s>", "budget", "<enter>")130 s.eventually("title update", func() bool { return s.title() == "012 - budget.012" })131 if _, err := os.Stat(filepath.Join(dir, "budget.012")); err != nil {132 t.Fatal(err)133 }134135 s.keys("<ctrl+q>")136 s.waitExit()137 // Leaving the alternate screen restores the user's shell screen.138 if scr := s.activeScreen(); scr != ghostty.ScreenPrimary {139 t.Errorf("active screen after exit = %v, want primary", scr)140 }141142 r := start(t, dir, "budget.012")143 r.waitForLine(gridRow1+2, numRow(3, "14400"))144 if !strings.Contains(r.screen(), "budget.012") {145 t.Errorf("status line missing file name:\n%s", r.screen())146 }147}148149func TestResizeShowsMoreColumns(t *testing.T) {150 t.Parallel()151 s := start(t, "")152 s.waitFor(" I")153 if strings.Contains(s.line(3), " N") {154 t.Fatalf("column N visible at 100 columns: %q", s.line(3))155 }156 s.resize(160, 30)157 s.eventually("column N after resize", func() bool { return strings.Contains(s.line(3), " N") })158}159160func TestCopyPasteAdjustsReferences(t *testing.T) {161 t.Parallel()162 s := start(t, "")163 clip := s.watchClipboard()164 s.keys("2", "<tab>", "=A1*10", "<enter>", "3", "<enter>", "<ctrl+home>", "<right>", "<ctrl+c>")165 s.waitFor("Copied B1")166 s.eventually("system clipboard", func() bool { return clip.get() == "20" })167 s.keys("<down>", "<ctrl+v>")168 s.waitFor("Pasted 1 cell at B2")169 s.waitForBar("B2", "=A2*10")170 s.waitForLine(gridRow1+1, numRow(2, "3", "30"))171172 // Esc clears the marker; the paste stays.173 s.keys("<esc>")174 s.eventually("marker cleared", func() bool { return !strings.Contains(s.line(2), "Copied") })175176 // Cut and paste moves the cell and the formula that uses it follows.177 s.keys("<ctrl+home>", "<ctrl+x>", "<down>", "<down>", "<down>", "<ctrl+v>")178 s.waitFor("Moved A1 to A4")179 s.keys("<ctrl+home>", "<right>")180 s.waitForBar("B1", "=A4*10")181 s.waitForLine(gridRow1+3, numRow(4, "2"))182}183184func TestUndoRedo(t *testing.T) {185 t.Parallel()186 s := start(t, "")187 s.keys("10", "<enter>", "=A1+1", "<enter>", "<up>", "<up>", "<shift+down>", "<delete>")188 s.waitForLine(gridRow1, " 1")189 s.keys("<ctrl+z>")190 s.waitFor("Undid: clear A1:A2")191 s.waitForLine(gridRow1+1, numRow(2, "11"))192 s.keys("<ctrl+y>")193 s.waitFor("Redid: clear A1:A2")194 s.waitForLine(gridRow1+1, " 2")195 s.keys("<ctrl+shift+z>")196 s.waitFor("Nothing to redo")197}198199func TestInsertRowRewritesFormulas(t *testing.T) {200 t.Parallel()201 s := start(t, "")202 s.keys("1", "<enter>", "2", "<enter>", "=SUM(A1:A2)", "<enter>")203 s.keys("<up>", "<up>", "<shift+space>", "<ctrl+alt+=>")204 s.waitForLine(gridRow1+3, numRow(4, "3"))205 s.keys("<down>", "<down>")206 s.waitForBar("A4", "=SUM(A1:A3)")207 s.keys("<up>", "<up>", "5", "<enter>")208 s.waitForLine(gridRow1+3, numRow(4, "8"))209 s.keys("<ctrl+z>", "<ctrl+z>")210 s.waitFor("Undid: insert 1 row")211 s.waitForLine(gridRow1+2, numRow(3, "3"))212}213214func TestPasteTSVFillsCells(t *testing.T) {215 t.Parallel()216 s := start(t, "")217 s.paste("Item\tCost\nRent\t1450\nFood\t=B2/2\n")218 s.waitFor("Pasted 6 cells at A1:B3")219 s.waitForLine(gridRow1+2, " 3 Food 725")220}221222func TestFillAndAbsoluteReferences(t *testing.T) {223 t.Parallel()224 s := start(t, "")225 s.keys("2", "<enter>", "3", "<enter>", "4", "<enter>", "<ctrl+home>", "<right>")226 s.keys("=A1*A1", "<left>", "<left>", "<f4>")227 s.waitForEntry("=A1*$A$1")228 s.keys("<enter>", "<up>", "<shift+down>", "<shift+down>", "<ctrl+d>")229 s.waitForLine(gridRow1+2, numRow(3, "4", "8"))230 s.keys("<right>", "<shift+down>", "=B1+1", "<ctrl+enter>")231 s.waitForLine(gridRow1+1, numRow(2, "3", "6", "7"))232}233234// Click handling is covered by the unit tests; this checks the program235// actually turned on mouse reporting in the terminal.236func TestMouseTrackingEnabled(t *testing.T) {237 t.Parallel()238 s := start(t, "")239 s.eventually("mouse tracking enabled", func() bool {240 s.mu.Lock()241 defer s.mu.Unlock()242 on, _ := s.vt.MouseTracking()243 return on244 })245}