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 / screenlist_test.go
screenlist_test.go 16.7K
1package e2e23import ghostty "go.mitchellh.com/libghostty"45// The golden screens: every UI state recorded, how to reach it, and which6// are recorded on a light terminal too. The fixtures they share are in7// screensetup_test.go; TestScreens in screens_test.go records them.89var screens = []screen{10 {name: "ready-empty", setup: func(s *session) {}},11 {name: "budget", setup: budget},12 {name: "entry-formula", setup: func(s *session) {13 budget(s)14 s.keys("<down>", "=B7*12")15 s.waitFor("ENTER")16 }},17 {name: "point-range", setup: func(s *session) {18 budget(s)19 s.keys("<right>", "=AVERAGE(", "<left>", "<up>", "<up>", "<up>", "<up>", "<shift+down>", "<shift+down>")20 s.waitFor("=AVERAGE(B3:B5")21 }},22 {name: "selection-stats", setup: func(s *session) {23 budget(s)24 s.keys("<up>", "<up>", "<up>", "<up>", "<shift+down>", "<shift+down>")25 s.waitFor("Sum 2158.4")26 }},27 {name: "column-select", setup: func(s *session) {28 budget(s)29 s.keys("<ctrl+space>")30 s.waitFor(" B:B ")31 }},32 {name: "edit-error", setup: func(s *session) {33 s.keys("=SUM(A1", "<enter>")34 s.waitFor("Expected , or ) in SUM")35 }},36 {name: "menu", setup: func(s *session) {37 budget(s)38 s.keys("<alt+f>", "<down>", "<down>", "<down>")39 s.waitFor("Save the sheet")40 }},41 {name: "palette", setup: func(s *session) {42 budget(s)43 s.keys("<ctrl+k>")44 s.waitFor("Search the menus")45 }},46 {name: "palette-search", setup: func(s *session) {47 budget(s)48 s.keys("<alt+/>", "col")49 s.waitFor("│ › col")50 }},51 {name: "palette-narrow", opts: options{cols: 60, rows: 16}, setup: func(s *session) {52 budget(s)53 s.keys("<ctrl+k>", "sa")54 s.waitFor("│ › sa")55 }},56 {name: "context-menu", setup: func(s *session) {57 budget(s)58 s.click(ghostty.MouseButtonRight, 6+10+4, 4+3)59 s.waitFor("│ Clear")60 }},61 {name: "context-menu-column", setup: func(s *session) {62 budget(s)63 s.click(ghostty.MouseButtonRight, 6+10+4, 3)64 s.waitFor("│ Resize column")65 }},66 {name: "menu-narrow", opts: options{cols: 60, rows: 16}, setup: func(s *session) {67 budget(s)68 s.keys("<alt+h>")69 s.waitFor("│ About 012")70 }},71 {name: "functions", setup: func(s *session) {72 budget(s)73 s.keys("<alt+h>", "f", "if")74 s.waitFor("│ › if")75 }},76 {name: "about", setup: func(s *session) {77 s.keys("<alt+h>", "a")78 s.waitFor("Google Sheets keys")79 }},80 {name: "help-narrow", opts: options{cols: 60, rows: 16}, setup: func(s *session) {81 s.keys("<f1>")82 s.waitFor("Keyboard shortcuts")83 }},84 {name: "palette-wide", opts: options{cols: 200, rows: 30}, setup: func(s *session) {85 budget(s)86 s.keys("<ctrl+k>", "sel")87 s.waitFor("│ › sel")88 }},89 {name: "help-wide", opts: options{cols: 200, rows: 45}, setup: func(s *session) {90 s.keys("<f1>")91 s.waitFor("Keyboard shortcuts")92 }},93 {name: "quit-confirm", setup: func(s *session) {94 budget(s)95 s.keys("<ctrl+q>")96 s.waitFor("unsaved changes")97 }},98 {name: "prompt-width", setup: func(s *session) {99 budget(s)100 s.keys("<ctrl+home>", "<alt+o>", "c", "<enter>", "<right>", "<right>", "<right>")101 s.waitFor("Column width (1-240): 13")102 }},103 {name: "error-goto", setup: func(s *session) {104 s.keys("<f5>", "nope", "<enter>")105 s.waitFor("Not a cell, range, named range, table or region")106 }},107 {name: "help", setup: func(s *session) {108 s.keys("<f1>")109 s.waitFor("HELP")110 }},111 {name: "narrow", opts: options{cols: 60, rows: 16}, setup: budget},112 {name: "hover-resize-handle", setup: func(s *session) {113 budget(s)114 s.mouse(ghostty.MouseActionMotion, ghostty.MouseButtonUnknown, 6+10-1, 3, 0)115 s.waitFor("▐")116 }},117 {name: "resizing-column", setup: func(s *session) {118 budget(s)119 s.mouse(ghostty.MouseActionPress, ghostty.MouseButtonLeft, 6+10-1, 3, 0)120 s.mouse(ghostty.MouseActionMotion, ghostty.MouseButtonLeft, 6+10+5, 3, 0)121 s.waitFor("Column A width 16")122 }},123 {name: "copy-marker", setup: func(s *session) {124 budget(s)125 s.keys("<up>", "<up>", "<up>", "<up>", "<shift+down>", "<shift+down>", "<ctrl+c>", "<right>", "<up>")126 s.waitFor("Copied B3:B5")127 }},128 {name: "copy-marker-selected", setup: func(s *session) {129 budget(s)130 s.keys("<left>", "<up>", "<up>", "<up>", "<up>", "<shift+down>", "<shift+down>", "<shift+right>", "<ctrl+x>")131 s.waitFor("Cut A3:B5")132 }},133 {name: "undo-note", setup: func(s *session) {134 budget(s)135 s.keys("<up>", "<shift+up>", "<delete>", "<ctrl+z>")136 s.waitFor("Undid: clear B5:B6")137 }},138 {name: "narrow-copy", opts: options{cols: 60, rows: 16}, setup: func(s *session) {139 budget(s)140 s.keys("<ctrl+c>", "<down>")141 s.waitFor("Copied B7")142 }},143 {name: "formats", setup: formatted},144 {name: "jev", opts: options{jev: true}, setup: reviews},145 {name: "find", setup: func(s *session) {146 budget(s)147 s.keys("<ctrl+f>", "r")148 s.waitFor("1 of 4")149 }},150 {name: "replace", setup: func(s *session) {151 budget(s)152 s.keys("<ctrl+home>", "<shift+down>", "<shift+down>", "<shift+down>", "<shift+down>", "<shift+down>", "<shift+down>")153 s.keys("<ctrl+h>", "Rent", "<tab>", "Lease", "<alt+c>")154 s.waitFor("in A1:A7")155 }},156 {name: "find-narrow", opts: options{cols: 60, rows: 16}, setup: func(s *session) {157 budget(s)158 s.keys("<ctrl+h>", "tr", "<tab>", "x")159 s.waitFor("Replace")160 }},161 {name: "menu-format-number", setup: func(s *session) {162 formatted(s)163 s.keys("<alt+o>", "<right>")164 s.waitFor("Currency rounded")165 }},166 {name: "formats-narrow", opts: options{cols: 60, rows: 16}, setup: formatted},167 {name: "autocomplete", setup: func(s *session) {168 budget(s)169 s.keys("<down>", "=B7/su")170 s.waitFor("│ SUM ")171 }},172 {name: "autocomplete-narrow", opts: options{cols: 60, rows: 16}, setup: func(s *session) {173 budget(s)174 s.keys("<down>", "=MAX(B7,co")175 s.waitFor("│ COUNT ")176 }},177 {name: "signature", setup: func(s *session) {178 budget(s)179 s.keys("<down>", `=SUMIF(A3:A5, "R*", `)180 s.waitFor("criterion, [sum_range])")181 }},182 {name: "named-ranges", setup: func(s *session) {183 named(s)184 s.keys("<alt+d>", "n")185 s.waitFor("│ › Type a name")186 }},187 {name: "trace-precedents", setup: func(s *session) {188 named(s)189 s.keys("<alt+,>")190 s.waitFor("precedents of B8")191 }},192 {name: "chart", setup: func(s *session) {193 spending(s)194 insertChart(s)195 s.keys("<enter>", "<esc>")196 s.waitFor("READY")197 }},198 {name: "chart-editor", setup: func(s *session) {199 spending(s)200 insertChart(s)201 }},202 {name: "chart-selected", setup: func(s *session) {203 spending(s)204 insertChart(s)205 s.keys("<enter>")206 s.waitFor("Column chart of A1:C5")207 }},208 {name: "chart-bar", setup: func(s *session) { chartOfType(s, 1, "Bar chart") }},209 {name: "chart-line", setup: func(s *session) { chartOfType(s, 2, "Line chart") }},210 {name: "chart-pie", setup: func(s *session) { chartOfType(s, 3, "Pie chart") }},211 {name: "chart-narrow", opts: options{cols: 60, rows: 16}, setup: func(s *session) {212 spending(s)213 insertChart(s)214 }},215 {name: "chart-wide", opts: options{cols: 200, rows: 30}, setup: func(s *session) {216 spending(s)217 insertChart(s)218 s.keys("<enter>", "<esc>")219 s.waitFor("READY")220 }},221 {name: "links-errors", setup: func(s *session) {222 s.keys("Docs", "<tab>", "https://example.com/docs", "<enter>")223 s.keys("Help", "<tab>", `=HYPERLINK("example.org/help", "Help center")`, "<enter>")224 s.keys("Ratio", "<tab>", "=B5/0", "<enter>")225 s.keys("Total", "<tab>", "=B3*2", "<enter>")226 s.keys("<up>", "<right>")227 s.waitFor("From B3: division by zero in B5/0")228 }},229 {name: "decimal", setup: func(s *session) {230 money(s)231 s.keys("<ctrl+k>", "decimal", "<enter>")232 s.waitFor("Decimal arithmetic on")233 }},234 {name: "menu-settings", setup: func(s *session) {235 money(s)236 s.keys("<ctrl+k>", "decimal", "<enter>")237 s.waitFor("Decimal arithmetic on")238 s.keys("<alt+f>", "<up>", "<up>", "<right>")239 s.waitFor("Compute money exactly")240 }},241 // Themes: color schemes draw bars and the screen in their own colors.242 {name: "theme-classic", opts: options{config: "theme = 1-2-3 Classic\n"}, setup: func(s *session) {243 budget(s)244 s.keys("<up>", "<up>", "<up>", "<up>", "<shift+down>", "<shift+down>")245 s.waitFor("Sum 2158.4")246 }},247 {name: "theme-classic-narrow-menu", opts: options{cols: 61, rows: 16, config: "theme = 1-2-3 Classic\n"}, setup: func(s *session) {248 budget(s)249 s.keys("<alt+f>", "<down>", "<down>", "<down>")250 s.waitFor("Save the sheet")251 }},252 {name: "theme-dark", opts: options{config: "theme = TokyoNight\n"}, setup: formatted},253 {name: "theme-light-menu", opts: options{light: true, config: "theme = light:Catppuccin Latte,dark:Catppuccin Mocha\n"}, setup: func(s *session) {254 budget(s)255 s.keys("<alt+f>", "<down>", "<down>", "<down>")256 s.waitFor("Save the sheet")257 }},258 {name: "theme-picker", setup: func(s *session) {259 budget(s)260 openTheme(s)261 s.keys("gruvbox")262 s.waitFor("│ › gruvbox")263 }},264 {name: "api-key-prompt", setup: func(s *session) {265 s.keys("<alt+f>", "<up>", "<up>", "<right>", "<down>", "<down>", "<down>", "<down>")266 s.waitFor("Store the TypeSafe API key")267 s.keys("<enter>", "ts-demo-0123456789")268 s.waitFor("TypeSafe API key: ••••••••••••••••••")269 }},270 {name: "config-warning", opts: options{config: "theme = Nord\nbogus = 1\n"}, setup: func(s *session) {271 s.waitFor(`unknown key "bogus"`)272 }},273 {name: "menu-freeze", setup: func(s *session) {274 inventory(s)275 s.keys("<alt+v>", "<right>", "<down>")276 s.waitFor("Keep the first row on screen")277 }},278 {name: "frozen", setup: func(s *session) {279 inventory(s)280 s.keys("<alt+v>", "<enter>", "1", "<enter>")281 s.waitFor("Froze 1 row")282 s.keys("<alt+v>", "<enter>", "1", "1", "<enter>")283 s.waitFor("Froze 1 column")284 s.keys("<ctrl+end>", "<up>", "<up>", "<shift+up>", "<shift+up>")285 s.waitForName("F37:F39")286 }},287 {name: "frozen-narrow", opts: options{cols: 60, rows: 16}, setup: func(s *session) {288 inventory(s)289 s.keys("<right>", "<down>", "<alt+v>", "<enter>", "u", "<enter>")290 s.waitFor("Froze 2 rows")291 s.keys("<alt+v>", "<enter>", "u", "u", "<enter>")292 s.waitFor("Froze 2 columns")293 s.keys("<pgdown>", "<right>", "<right>", "<right>")294 s.waitForName("E10")295 }},296 {name: "filter-picker", setup: func(s *session) {297 inventory(s)298 s.keys("<right>", "<alt+d>", "c")299 s.waitFor("Created a filter")300 s.keys("<alt+down>", "<down>", "<down>", "<space>", "<down>")301 s.waitFor("[ ] Hardware")302 }},303 {name: "filter-condition", setup: func(s *session) {304 inventory(s)305 s.keys("<alt+d>", "c")306 s.waitFor("Created a filter")307 s.keys("<right>", "<right>", "<right>", "<alt+down>", "<tab>")308 for range 8 {309 s.keys("<down>")310 }311 s.keys("50")312 s.waitFor("‹ Greater than › 50")313 }},314 {name: "filtered", setup: func(s *session) {315 inventory(s)316 s.keys("<alt+d>", "c")317 s.waitFor("Created a filter")318 s.keys("<right>", "<alt+down>", "<down>", "<down>", "<space>", "<enter>")319 s.keys("<right>", "<right>", "<alt+down>", "<tab>")320 for range 8 {321 s.keys("<down>")322 }323 s.keys("50", "<enter>")324 s.waitFor("Filtered column D")325 s.keys("<down>", "<down>")326 }},327 {name: "sort-bar", setup: func(s *session) {328 inventory(s)329 s.keys("<right>", "<alt+d>", "<down>", "<enter>", "a", "<enter>")330 s.waitFor("Sort A2:F41 by")331 s.keys("<alt+a>", "<right>", "<right>", "<right>", "<space>")332 s.waitFor("D Qty Z→A")333 }},334 {name: "fill-handle", setup: func(s *session) {335 s.keys("Week", "<tab>", "Day", "<tab>", "Batch", "<enter>")336 s.keys("1", "<tab>", "Mon", "<tab>", "Item 1", "<enter>")337 s.keys("2", "<tab>", "Tue", "<tab>", "Item 2", "<enter>")338 s.keys("<up>", "<up>", "<shift+down>", "<shift+right>", "<shift+right>")339 s.waitForName("A2:C3")340 s.mouse(ghostty.MouseActionPress, ghostty.MouseButtonLeft, 6+29, gridRow1+2, 0)341 s.mouse(ghostty.MouseActionMotion, ghostty.MouseButtonLeft, 6+25, gridRow1+6, 0)342 s.waitFor("Fill A2:C7")343 }},344 {name: "import-picker", files: importDir, setup: openImportPicker},345 {name: "import-picker-narrow", opts: options{cols: 60, rows: 16}, files: importDir, setup: openImportPicker},346 {name: "import-progress", setup: slowCSV},347 {name: "import-xlsx", files: importDir, setup: func(s *session) {348 openImportPicker(s)349 s.keys("q3", "<enter>")350 s.waitFor("Imported q3.xlsx")351 }},352 {name: "save-imported", files: importDir, setup: func(s *session) {353 openImportPicker(s)354 s.keys("q3", "<enter>")355 s.waitFor("Imported q3.xlsx")356 s.keys("<ctrl+s>")357 s.waitFor("q3.xlsx was imported.")358 }},359 {name: "menu-download", setup: func(s *session) {360 budget(s)361 s.keys("<alt+f>", "<down>", "<down>", "<down>", "<down>", "<down>", "<right>")362 s.waitFor("SQLite database (.sqlite)")363 }},364 {name: "sheets", setup: summary},365 {name: "sheets-wide", opts: options{cols: 200, rows: 30}, setup: summary},366 {name: "sheets-point", setup: func(s *session) {367 summary(s)368 s.keys("<down>", "<down>", "<left>", "Rent share", "<tab>", "=", "<ctrl+pgup>", "<up>", "<up>")369 s.waitForBar("Summary!B3", "=Sheet1!B3")370 }},371 {name: "sheets-many", opts: options{cols: 60, rows: 16}, setup: func(s *session) {372 budget(s)373 for range 11 {374 s.keys("<shift+f11>")375 }376 s.waitFor("Added Sheet12")377 for range 5 {378 s.keys("<ctrl+pgup>")379 }380 s.waitFor("‹")381 }},382 {name: "sheets-rename", setup: func(s *session) {383 summary(s)384 x := tabX(s.t, s, "Summary")385 s.click(ghostty.MouseButtonLeft, x, int(s.rows)-1)386 s.click(ghostty.MouseButtonLeft, x, int(s.rows)-1)387 s.waitFor("Rename sheet: Summary")388 s.keys("Q3 totals")389 }},390 {name: "sheets-menu", setup: func(s *session) {391 summary(s)392 s.click(ghostty.MouseButtonRight, tabX(s.t, s, "Summary"), int(s.rows)-1)393 s.waitFor("Move right")394 }},395 {name: "sheets-picker", setup: func(s *session) {396 summary(s)397 s.keys("<alt+shift+k>")398 s.waitFor("Go to sheet")399 }},400 {name: "vim-normal", setup: func(s *session) {401 budget(s)402 vimOn(s)403 s.keys("gg", "2d")404 s.waitFor("2d")405 }},406 {name: "vim-visual", setup: func(s *session) {407 budget(s)408 vimOn(s)409 s.keys("gg", "2j", "v", "2j", "h")410 s.waitFor("VISUAL")411 s.waitFor("Sum 2158.4")412 }},413 {name: "vim-command", setup: func(s *session) {414 budget(s)415 vimOn(s)416 s.keys(":", "find")417 s.waitFor("COMMAND")418 s.waitFor("edit.find")419 s.keys("<tab>", "<tab>")420 }},421 {name: "find-all-sheets", setup: func(s *session) {422 summary(s)423 s.keys("<ctrl+f>", "total", "<alt+s>")424 s.waitFor("in all sheets")425 s.waitFor("2 of 2 on Summary")426 s.keys("<enter>")427 s.waitFor("1 of 2 on Sheet1")428 }},429 {name: "pivot-editor", setup: pivotEditor},430 {name: "pivot-editor-narrow", opts: options{cols: 60, rows: 16}, setup: pivotEditor},431 {name: "pivot", setup: func(s *session) {432 pivotEditor(s)433 s.keys("<enter>", "<down>", "<down>", "<right>", "<right>", "5")434 s.waitFor("Pivot table results can't be edited")435 }},436 {name: "pivot-rename", setup: func(s *session) {437 pivotEditor(s)438 s.keys("r")439 s.waitFor("Value name: AVERAGE of Price")440 s.keys("Avg price")441 }},442 {name: "pivot-subtotals", setup: func(s *session) {443 // Reorder, then Category, in Columns: a subtotal column after444 // each Reorder value, and the average renamed.445 pivotEditor(s)446 s.keys("r")447 s.waitFor("Value name: AVERAGE of Price")448 s.keys("Avg price", "<enter>")449 s.waitFor(" Avg price")450 s.keys("<up>", "<up>", "<up>", "<up>", "<up>", "<delete>")451 s.waitFor("Columns")452 s.keys("<down>", "a", "categ", "<enter>")453 s.waitFor(" Category")454 s.keys("<enter>")455 s.waitFor("no Total")456 }},457 {name: "pivot-filter", setup: func(s *session) {458 pivotEditor(s)459 s.keys("<down>", "<space>", "item", "<enter>", "<space>", "bolts")460 s.waitFor("Filter Item")461 }},462 {name: "far-corner", setup: func(s *session) {463 // The bottom-right corner of the grid: seven-digit rows, the464 // widest row numbers, and three-letter columns.465 s.keys("<f5>", "XFB1048572", "<enter>", "Quarter", "<enter>", "Q1", "<enter>", "Q2", "<enter>", "Q3", "<enter>", "Total", "<enter>")466 s.keys("<f5>", "XFC1048572", "<enter>", "Sales", "<enter>", "1200", "<enter>", "1350", "<enter>", "990", "<enter>", "=SUM(XFC1048573:XFC1048575)", "<enter>")467 s.keys("<f5>", "XFD1048576", "<enter>", "=XFC1048576/4", "<enter>")468 s.waitFor("885")469 }},470 {name: "frequency", setup: func(s *session) {471 inventory(s)472 s.keys("<right>", "<right>", "<down>", "<alt+shift+f>")473 s.waitFor("Counted Bin: 12 distinct values")474 }},475}476477// Key screens are also recorded on a light terminal, where the app picks478// its light theme from the reported background color.479func init() {480 for _, name := range []string{"budget", "point-range", "selection-stats", "menu", "palette-search", "context-menu-column", "functions", "quit-confirm", "help", "resizing-column", "copy-marker", "copy-marker-selected", "formats", "menu-format-number", "find", "replace", "jev", "import-picker", "import-progress", "import-xlsx", "frozen", "filter-picker", "filtered", "sort-bar", "fill-handle", "chart", "chart-editor", "chart-line", "chart-pie", "links-errors", "autocomplete", "signature", "named-ranges", "trace-precedents", "sheets", "sheets-point", "sheets-menu", "sheets-many", "vim-normal", "vim-visual", "vim-command", "pivot-editor", "pivot", "pivot-rename", "pivot-subtotals", "frequency", "far-corner"} {481 for _, sc := range screens {482 if sc.name == name {483 sc.name += "-light"484 sc.opts.light = true485 screens = append(screens, sc)486 }487 }488 }489}