cat / 012
clone

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

E2E: sessions run in parallel, golden screens wait to match

Dami committed bfd4178
Every session is its own process, pty, temp directory, config home and
fake services, so the tests and the golden screens' subtests call
t.Parallel and run up to GOMAXPROCS at a time. TestScreens stays serial
at the top, so TestStills reads the goldens it has just written under
-update, and TestStressKeyLatency stays serial as it times keys.

A screen is captured once it holds still and shows its golden, or after
five seconds of not showing it: nu's highlighting can reach a busy
screen after two captures matched, which parallel runs exposed.
commit
bfd41781fe92182fff11732aaabf34bc54e7f1cb
parent
55928ef00d21eba0ed272fbcadf24a1ff6dbf6f1
author
Dami <dami.etoile@gmail.com>
kind
test risk score 1.3 of 3, Jev's estimate
39 files changed, +128 -5
Me2e/app_test.go13+++++++++++++
Me2e/arrays_test.go1+
Me2e/charts_test.go5+++++
Me2e/config_test.go5+++++
Me2e/crash_test.go1+
Me2e/data_test.go2++
Me2e/explain_test.go1+
Me2e/find_test.go1+
Me2e/follow_test.go1+
Me2e/format_test.go3+++
Me2e/formula_help_test.go3+++
Me2e/import_test.go6++++++
Me2e/jev_test.go2++
Me2e/jupyter_test.go2++
Me2e/kitty_test.go3+++
Me2e/layout_test.go3+++
Me2e/live_test.go2++
Me2e/locale_test.go3+++
Me2e/macros_test.go3+++
Me2e/mouse_test.go4++++
Me2e/notebook_test.go7+++++++
Me2e/notes_test.go2++
Me2e/numodule_test.go4++++
Me2e/pipe_test.go6++++++
Me2e/pivot_test.go3+++
Me2e/rules_test.go2++
Me2e/screens_test.go26+++++++++++++++++++++-----
Me2e/settings_test.go1+
Me2e/share_test.go1+
Me2e/sheets_test.go3+++
Me2e/shell_test.go5+++++
Me2e/sources_test.go1+
Me2e/ssh_test.go2++
Me2e/stream_test.go1+
Me2e/tables_test.go1+
Me2e/trace_test.go1+
Me2e/vast_test.go1+
Me2e/vim_test.go1+
Me2e/vimmore_test.go1+
M e2e/app_test.go +13 -0
@@ -61,6 +61,7 @@ func numRow(row int, nums ...string) string {
6161 }6262 6363 func TestFormulaRecalc(t *testing.T) {64+ t.Parallel()6465 s := start(t, "")6566 s.keys("10", "<enter>", "20", "<enter>", "=SUM(A1:A2)", "<enter>")6667 s.keys("<up>")
@@ -73,6 +74,7 @@ func TestFormulaRecalc(t *testing.T) {
7374 }7475 7576 func TestPointModeBuildsFormula(t *testing.T) {77+ t.Parallel()7678 s := start(t, "")7779 s.keys("4", "<tab>", "6", "<tab>", "=", "<left>", "<left>")7880 s.waitFor("POINT")
@@ -85,6 +87,7 @@ func TestPointModeBuildsFormula(t *testing.T) {
8587 }8688 8789 func TestShiftSelectionShowsStats(t *testing.T) {90+ t.Parallel()8891 s := start(t, "")8992 s.keys("1", "<enter>", "2", "<enter>", "3", "<enter>", "<ctrl+home>")9093 s.keys("<shift+down>", "<shift+down>")
@@ -97,6 +100,7 @@ func TestShiftSelectionShowsStats(t *testing.T) {
97100 }98101 99102 func TestTextOverflow(t *testing.T) {103+ t.Parallel()100104 s := start(t, "")101105 s.keys("Quarterly revenue", "<enter>", "$1,200", "<enter>", "TRUE", "<enter>")102106 s.waitForLine(gridRow1, " 1 Quarterly revenue")
@@ -105,6 +109,7 @@ func TestTextOverflow(t *testing.T) {
105109 }106110 107111 func TestInvalidFormulaCursor(t *testing.T) {112+ t.Parallel()108113 s := start(t, "")109114 s.keys("=SUM(A1", "<enter>")110115 s.waitFor("EDIT")
@@ -117,6 +122,7 @@ func TestInvalidFormulaCursor(t *testing.T) {
117122 }118123 119124 func TestSaveQuitReopen(t *testing.T) {125+ t.Parallel()120126 dir := t.TempDir()121127 s := start(t, dir)122128 s.keys("Budget", "<enter>", "1200", "<enter>", "=A2*12", "<enter>")
@@ -141,6 +147,7 @@ func TestSaveQuitReopen(t *testing.T) {
141147 }142148 143149 func TestResizeShowsMoreColumns(t *testing.T) {150+ t.Parallel()144151 s := start(t, "")145152 s.waitFor(" I")146153 if strings.Contains(s.line(3), " N") {
@@ -151,6 +158,7 @@ func TestResizeShowsMoreColumns(t *testing.T) {
151158 }152159 153160 func TestCopyPasteAdjustsReferences(t *testing.T) {161+ t.Parallel()154162 s := start(t, "")155163 clip := s.watchClipboard()156164 s.keys("2", "<tab>", "=A1*10", "<enter>", "3", "<enter>", "<ctrl+home>", "<right>", "<ctrl+c>")
@@ -174,6 +182,7 @@ func TestCopyPasteAdjustsReferences(t *testing.T) {
174182 }175183 176184 func TestUndoRedo(t *testing.T) {185+ t.Parallel()177186 s := start(t, "")178187 s.keys("10", "<enter>", "=A1+1", "<enter>", "<up>", "<up>", "<shift+down>", "<delete>")179188 s.waitForLine(gridRow1, " 1")
@@ -188,6 +197,7 @@ func TestUndoRedo(t *testing.T) {
188197 }189198 190199 func TestInsertRowRewritesFormulas(t *testing.T) {200+ t.Parallel()191201 s := start(t, "")192202 s.keys("1", "<enter>", "2", "<enter>", "=SUM(A1:A2)", "<enter>")193203 s.keys("<up>", "<up>", "<shift+space>", "<ctrl+alt+=>")
@@ -202,6 +212,7 @@ func TestInsertRowRewritesFormulas(t *testing.T) {
202212 }203213 204214 func TestPasteTSVFillsCells(t *testing.T) {215+ t.Parallel()205216 s := start(t, "")206217 s.paste("Item\tCost\nRent\t1450\nFood\t=B2/2\n")207218 s.waitFor("Pasted 6 cells at A1:B3")
@@ -209,6 +220,7 @@ func TestPasteTSVFillsCells(t *testing.T) {
209220 }210221 211222 func TestFillAndAbsoluteReferences(t *testing.T) {223+ t.Parallel()212224 s := start(t, "")213225 s.keys("2", "<enter>", "3", "<enter>", "4", "<enter>", "<ctrl+home>", "<right>")214226 s.keys("=A1*A1", "<left>", "<left>", "<f4>")
@@ -222,6 +234,7 @@ func TestFillAndAbsoluteReferences(t *testing.T) {
222234 // Click handling is covered by the unit tests; this checks the program223235 // actually turned on mouse reporting in the terminal.224236 func TestMouseTrackingEnabled(t *testing.T) {237+ t.Parallel()225238 s := start(t, "")226239 s.eventually("mouse tracking enabled", func() bool {227240 s.mu.Lock()
M e2e/arrays_test.go +1 -0
M e2e/charts_test.go +5 -0
M e2e/config_test.go +5 -0
@@ -11,6 +11,7 @@ import (
1111 // Problems in the config file show on the context line; 012 starts1212 // anyway, with defaults for what's wrong.1313 func TestConfigWarnings(t *testing.T) {14+ t.Parallel()1415 s := startWith(t, options{config: "theme = Dracula\nbogus = 1\nchart-images = maybe\n"})1516 s.waitFor(`Config: config:2: unknown key "bogus" (and 1 more; run 012 config)`)1617 if !strings.Contains(s.html(), "background:#1e1f29") {
@@ -29,6 +30,7 @@ func openTheme(s *session) {
2930 // The theme picker previews the highlighted theme, Esc restores the one3031 // there was, and Enter keeps and saves it, so the next start has it.3132 func TestThemePicker(t *testing.T) {33+ t.Parallel()3234 cfg := t.TempDir()3335 s := startWith(t, options{configDir: cfg})3436 openTheme(s)
@@ -55,6 +57,7 @@ func TestThemePicker(t *testing.T) {
5557 // A .env with the key next to the sheet is never read: JEV stays off,5658 // and the context line says how to store the key instead.5759 func TestDotEnvNotRead(t *testing.T) {60+ t.Parallel()5861 srv, requests := fakeTypeSafe(t)5962 dir := t.TempDir()6063 os.WriteFile(filepath.Join(dir, ".env"), []byte("TYPESAFE_API_KEY=test-key\nTYPESAFE_BASE_URL="+srv.URL+"\n"), 0o600)
@@ -70,6 +73,7 @@ func TestDotEnvNotRead(t *testing.T) {
7073 // 012 config set-key stores the key (here in the tests' stand-in for the7174 // credential store); the next start finds it there and JEV works.7275 func TestSetKeyThenJEV(t *testing.T) {76+ t.Parallel()7377 srv, requests := fakeTypeSafe(t)7478 cfg := t.TempDir()7579 cmd := exec.Command(binPath, "config", "set-key")
@@ -90,6 +94,7 @@ func TestSetKeyThenJEV(t *testing.T) {
9094 // File > Settings > JEV API key takes the key masked, stores it and turns9195 // JEV on at once, after one test call with it.9296 func TestAPIKeyPrompt(t *testing.T) {97+ t.Parallel()9398 srv, _ := fakeTypeSafe(t)9499 cfg := t.TempDir()95100 s := startWith(t, options{configDir: cfg, env: []string{"TYPESAFE_BASE_URL=" + srv.URL}})
M e2e/crash_test.go +1 -0
M e2e/data_test.go +2 -0
M e2e/explain_test.go +1 -0
M e2e/find_test.go +1 -0
M e2e/follow_test.go +1 -0
M e2e/format_test.go +3 -0
M e2e/formula_help_test.go +3 -0
M e2e/import_test.go +6 -0
@@ -55,6 +55,7 @@ func writeText(t *testing.T, name, text string) {
5555 }5656 5757 func TestOpenXLSXFromCommandLine(t *testing.T) {58+ t.Parallel()5859 dir := t.TempDir()5960 writeXLSX(t, filepath.Join(dir, "q3.xlsx"))6061 s := start(t, dir, "q3.xlsx")
@@ -87,6 +88,7 @@ func TestOpenXLSXFromCommandLine(t *testing.T) {
8788 }8889 8990 func TestOpenCSVFromCommandLine(t *testing.T) {91+ t.Parallel()9092 dir := t.TempDir()9193 writeText(t, filepath.Join(dir, "prices.csv"), "\xEF\xBB\xBFItem;Price;Date\nTea;$3.50;9/26/2026\nCake;4;2026-09-27\n")9294 s := start(t, dir, "prices.csv")
@@ -108,6 +110,7 @@ func TestOpenCSVFromCommandLine(t *testing.T) {
108110 }109111 110112 func TestOpenMissingImport(t *testing.T) {113+ t.Parallel()111114 dir := t.TempDir()112115 out, err := runBinary(dir, "nope.xlsx")113116 if err == nil || !strings.Contains(out, "nope.xlsx: no such file") {
@@ -129,6 +132,7 @@ func importDir(t *testing.T, dir string) {
129132 }130133 131134 func TestImportPickerLists(t *testing.T) {135+ t.Parallel()132136 dir := t.TempDir()133137 importDir(t, dir)134138 s := start(t, dir)
@@ -181,6 +185,7 @@ func runBinary(dir string, args ...string) (string, error) {
181185 }182186 183187 func TestImportProgressAndCancel(t *testing.T) {188+ t.Parallel()184189 s := start(t, "")185190 slowCSV(s)186191 s.waitFor("WAIT")
@@ -194,6 +199,7 @@ func TestImportProgressAndCancel(t *testing.T) {
194199 // file goes: new sheets after the one shown (every sheet of an .xlsx),195200 // undone as one step, or in place of the sheet shown.196201 func TestImportLocation(t *testing.T) {202+ t.Parallel()197203 dir := t.TempDir()198204 importDir(t, dir)199205 s := start(t, dir)
M e2e/jev_test.go +2 -0
M e2e/jupyter_test.go +2 -0
M e2e/kitty_test.go +3 -0
M e2e/layout_test.go +3 -0
M e2e/live_test.go +2 -0
M e2e/locale_test.go +3 -0
M e2e/macros_test.go +3 -0
M e2e/mouse_test.go +4 -0
M e2e/notebook_test.go +7 -0
@@ -24,6 +24,7 @@ func needNu(t *testing.T) {
2424 }2525 2626 func TestNotebookWithNu(t *testing.T) {27+ t.Parallel()2728 needNu(t)2829 dir := t.TempDir()2930 s := startWith(t, options{dir: dir, startsOn: "EDIT"}, "nu", "book.012")
@@ -53,6 +54,7 @@ func TestNotebookWithNu(t *testing.T) {
5354 // assigns, runs with the real nu: its output is the second's, and a5455 // later cell reads the variable.5556 func TestNotebookStatementsWithNu(t *testing.T) {57+ t.Parallel()5658 needNu(t)5759 dir := t.TempDir()5860 os.WriteFile(filepath.Join(dir, "big.txt"), []byte(strings.Repeat("x", 3000)), 0o644)
@@ -79,6 +81,7 @@ func TestNotebookStatementsWithNu(t *testing.T) {
7981 }8082 8183 func TestNotebookSendsToSheetWithNu(t *testing.T) {84+ t.Parallel()8285 needNu(t)8386 s := startWith(t, options{startsOn: "EDIT"}, "nu")8487 s.keys("n = [[k]; [1] [2] [3]]", "<ctrl+enter>")
@@ -96,6 +99,7 @@ func TestNotebookSendsToSheetWithNu(t *testing.T) {
9699 // With the real nu, a cell being written is highlighted by nu's shapes97100 // once typing pauses, and Tab completes what only nu knows: a flag.98101 func TestNotebookHighlightsAndCompletesWithNu(t *testing.T) {102+ t.Parallel()99103 needNu(t)100104 s := startWith(t, options{startsOn: "EDIT"}, "nu")101105 s.keys("ls | where size > 1kb | sort-by size")
@@ -109,6 +113,7 @@ func TestNotebookHighlightsAndCompletesWithNu(t *testing.T) {
109113 // command's help, with its page in nushell's docs as a link. On a110114 // cell's $name, the line says the cell and its output's shape.111115 func TestNotebookHoverWithNu(t *testing.T) {116+ t.Parallel()112117 needNu(t)113118 s := startWith(t, options{cols: 120, rows: 30, startsOn: "EDIT"}, "nu")114119 s.keys("n = [[k v]; [1 a] [2 b] [3 c]]", "<shift+enter>")
@@ -133,6 +138,7 @@ func TestNotebookHoverWithNu(t *testing.T) {
133138 // Data > Shell from a workbook makes a Notebook tab; $selection is134139 // the range selected on the sheet shown before it.135140 func TestNotebookReadsSelection(t *testing.T) {141+ t.Parallel()136142 needNu(t)137143 s := start(t, "")138144 s.keys("n", "<enter>", "3", "<enter>", "4", "<enter>", "<up>", "<up>", "<up>", "<shift+down>", "<shift+down>", "<shift+down>")
@@ -150,6 +156,7 @@ func TestNotebookReadsSelection(t *testing.T) {
150156 // A 200-character pipeline at 80 columns is on the screen whole, wrapped151157 // before its pipes, while it's edited and once it isn't.152158 func TestNotebookLongPipeline(t *testing.T) {159+ t.Parallel()153160 s := startWith(t, options{cols: 80, rows: 24, startsOn: "EDIT", env: fakeNu}, "nu")154161 var b strings.Builder155162 b.WriteString("ls")
M e2e/notes_test.go +2 -0
M e2e/numodule_test.go +4 -0
@@ -41,6 +41,7 @@ const sizesNu = "[[name size]; [a 2kb] [b 10b] [c 5mb]]"
4141 // A table goes through sheet: a selection is sent back, and nu gets it4242 // with its types, file sizes as file sizes.4343 func TestSheetSendsSelection(t *testing.T) {44+ t.Parallel()4445 s := startSheet(t, "5.0 MB", "let t = "+sizesNu+" | sheet; print ($t | get size | describe); $t | to nuon")4546 s.keys("<shift+down>", "<shift+down>", "<shift+right>")4647 s.waitFor("send A1:B3 as NUON")
@@ -56,6 +57,7 @@ func TestSheetSendsSelection(t *testing.T) {
5657 // sheet --send sheet gives back all of ls's table on Ctrl+Q, whatever5758 // is selected.5859 func TestSheetSendSheet(t *testing.T) {60+ t.Parallel()5961 s := startSheet(t, "b.txt", "cd (mktemp -d); 'x' | save a.txt; 'yyy' | save b.txt; "+6062 "let t = ls | sheet --send sheet; print ($t | columns | to nuon); "+6163 "print (($t | select name type size) == (ls | select name type size)); print ($t | length)")
@@ -70,6 +72,7 @@ func TestSheetSendSheet(t *testing.T) {
7072 7173 // Quitting without sending raises sheet's error, with 012's reason.7274 func TestSheetNotSent(t *testing.T) {75+ t.Parallel()7376 s := startSheet(t, "5.0 MB", "try { "+sizesNu+" | sheet } catch {|e| print $'caught: ($e.msg)' }")7477 s.keys("<ctrl+q>", "d")7578 out, code := s.finish()
@@ -80,6 +83,7 @@ func TestSheetNotSent(t *testing.T) {
8083 8184 // sheet view shows the table and returns nothing.8285 func TestSheetView(t *testing.T) {86+ t.Parallel()8387 s := startSheet(t, "5.0 MB", sizesNu+" | sheet view | describe")8488 s.keys("<ctrl+q>")8589 s.waitFor("You have unsaved changes.")
M e2e/pipe_test.go +6 -0
@@ -72,6 +72,7 @@ const lsNUON = "[[name, size, modified]; [CLAUDE.md, 1646b, 2026-09-27T11:27:31-
7272 // an edit is made, and quitting sends it on standard output, which gets7373 // nothing else.7474 func TestPipeEditAndSend(t *testing.T) {75+ t.Parallel()7576 s := startWith(t, options{piped: true, stdin: lsNUON, env: []string{"TZ=America/Denver"}}, "--pipe")7677 s.waitFor("Read 4 rows of NUON from standard input")7778 s.waitFor("4.2 kB")
@@ -92,6 +93,7 @@ func TestPipeEditAndSend(t *testing.T) {
9293 // TestPipeSendSelectionAsCSV sends a selection, in the format --to9394 // asks for.9495 func TestPipeSendSelectionAsCSV(t *testing.T) {96+ t.Parallel()9597 s := startWith(t, options{piped: true, stdin: lsNUON}, "--pipe", "--to", "csv")9698 s.waitFor("4.2 kB")9799 s.keys("<shift+down>", "<shift+down>", "<shift+right>")
@@ -108,6 +110,7 @@ func TestPipeSendSelectionAsCSV(t *testing.T) {
108110 // TestPipeSendSelectionWithoutAsking: --send selection makes Ctrl+Q109111 // send the selection at once.110112 func TestPipeSendSelectionWithoutAsking(t *testing.T) {113+ t.Parallel()111114 s := startWith(t, options{piped: true, stdin: lsNUON}, "--pipe", "--send", "selection", "--to", "csv")112115 s.waitFor("Ctrl+Q send the sheet as CSV")113116 s.keys("<shift+down>", "<shift+right>")
@@ -123,6 +126,7 @@ func TestPipeSendSelectionWithoutAsking(t *testing.T) {
123126 // whole sheet, whatever is selected; File > Quit without sending still124127 // sends nothing.125128 func TestPipeSendSheetWithoutAsking(t *testing.T) {129+ t.Parallel()126130 s := startWith(t, options{piped: true, stdin: "a,b\n1,2\n3,4\n"}, "--pipe", "--send", "sheet")127131 s.waitFor("Read 3 rows of CSV from standard input")128132 s.keys("<shift+down>")
@@ -146,6 +150,7 @@ func TestPipeSendSheetWithoutAsking(t *testing.T) {
146150 147151 // TestPipeQuitWithoutSending exits with status 1 and writes nothing.148152 func TestPipeQuitWithoutSending(t *testing.T) {153+ t.Parallel()149154 s := startWith(t, options{piped: true, stdin: "a,b\n1,2\n"}, "--pipe")150155 s.waitFor("Read 2 rows of CSV from standard input")151156 s.keys("<ctrl+q>", "d")
@@ -158,6 +163,7 @@ func TestPipeQuitWithoutSending(t *testing.T) {
158163 // TestStdinOnly: 012 - reads standard input and writes nothing to159164 // standard output.160165 func TestStdinOnly(t *testing.T) {166+ t.Parallel()161167 s := startWith(t, options{piped: true, stdin: `[{"a": 1, "b": "x"}]`}, "-")162168 s.waitFor("Read 2 rows of JSON from standard input")163169 s.keys("<ctrl+q>")
M e2e/pivot_test.go +3 -0
M e2e/rules_test.go +2 -0
M e2e/screens_test.go +21 -5
@@ -45,20 +45,21 @@ func TestScreens(t *testing.T) {
4545 }4646 for _, sc := range screens {4747 t.Run(sc.name, func(t *testing.T) {48+ t.Parallel()4849 s := startScreen(t, sc)4950 sc.setup(s)50- got := s.stableHTML()5151 path := filepath.Join(dir, sc.name+".html")52+ want, err := os.ReadFile(path)53+ if err != nil && !*update {54+ t.Fatalf("missing golden (run make screens): %v", err)55+ }56+ got := s.settledHTML(string(want))5257 if *update {5358 if err := os.WriteFile(path, []byte(got), 0o644); err != nil {5459 t.Fatal(err)5560 }5661 return5762 }58- want, err := os.ReadFile(path)59- if err != nil {60- t.Fatalf("missing golden (run make screens): %v", err)61- }6263 if got != string(want) {6364 actual := filepath.Join(t.TempDir(), sc.name+".html")6465 os.WriteFile(actual, []byte(got), 0o644)
@@ -153,6 +154,21 @@ func (s *session) stableHTML() string {
153154 return ""154155 }155156 157+// settledHTML is the screen once it has settled: stable, and showing158+// want (the golden) if it comes to within waitTimeout. Some of a screen159+// arrives after it first holds still, such as nu's highlighting, which160+// a busy machine delays past a capture or two; a screen that differs161+// from its golden for good fails, or is rewritten under -update, once162+// the wait is over.163+func (s *session) settledHTML(want string) string {164+ s.t.Helper()165+ got := s.stableHTML()166+ for deadline := time.Now().Add(waitTimeout); got != want && time.Now().Before(deadline); {167+ got = s.stableHTML()168+ }169+ return got170+}171+156172 // writeGallery renders all goldens into gallery.html (git-ignored), once157173 // with each reference palette.158174 func writeGallery(dir string) error {
M e2e/settings_test.go +1 -0
M e2e/share_test.go +1 -0
M e2e/sheets_test.go +3 -0
M e2e/shell_test.go +5 -0
M e2e/sources_test.go +1 -0
M e2e/ssh_test.go +2 -0
M e2e/stream_test.go +1 -0
M e2e/tables_test.go +1 -0
M e2e/trace_test.go +1 -0
M e2e/vast_test.go +1 -0
M e2e/vim_test.go +1 -0
M e2e/vimmore_test.go +1 -0