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 / notebook_test.go
notebook_test.go 6.6K
1package e2e23import (4 "os"5 "os/exec"6 "path/filepath"7 "strings"8 "testing"9 "unicode/utf8"1011 ghostty "go.mitchellh.com/libghostty"12)1314// Notebooks: cells written and run with the real nu, one reading15// another's output, saved with their outputs and opened again without16// running; an output sent to a sheet and kept up to date; the selection17// read as $selection; and a long pipeline wrapped, never cut.1819func needNu(t *testing.T) {20 t.Helper()21 if _, err := exec.LookPath("nu"); err != nil {22 t.Skip("nu isn't installed")23 }24}2526func TestNotebookWithNu(t *testing.T) {27 t.Parallel()28 needNu(t)29 dir := t.TempDir()30 s := startWith(t, options{dir: dir, startsOn: "EDIT"}, "nu", "book.012")31 s.keys("sizes = [[name size]; [a 2kb] [b 10b] [c 5mb]]", "<shift+enter>")32 s.waitFor("Out[1]:")33 s.waitFor("5.0 MB")34 s.keys("<enter>", "$sizes | where size > 1kb | sort-by size --reverse", "<shift+enter>")35 s.waitFor("Out[2]:")36 s.keys("<enter>", "nope-not-a-command", "<ctrl+enter>")37 s.waitFor("failed")38 s.keys("<ctrl+s>")39 s.eventually("saved notebook", func() bool {40 data, err := os.ReadFile(filepath.Join(dir, "book.012"))41 return err == nil && strings.Contains(string(data), `"notebookCells"`) && strings.Contains(string(data), "5000000b")42 })43 s.keys("<ctrl+q>")44 s.waitExit()4546 s = startWith(t, options{dir: dir, startsOn: "NOTEBOOK"}, "nu", "book.012")47 s.waitFor("5.0 MB") // the outputs, saved, show without running48 s.waitFor("saved")49 s.keys("<f9>")50 s.waitFor("[1]:")51}5253// A cell of two statements, the second reading the variable the first54// assigns, runs with the real nu: its output is the second's, and a55// later cell reads the variable.56func TestNotebookStatementsWithNu(t *testing.T) {57 t.Parallel()58 needNu(t)59 dir := t.TempDir()60 os.WriteFile(filepath.Join(dir, "big.txt"), []byte(strings.Repeat("x", 3000)), 0o644)61 os.WriteFile(filepath.Join(dir, "small.txt"), []byte("x"), 0o644)62 os.Mkdir(filepath.Join(dir, "sub"), 0o755)63 s := startWith(t, options{dir: dir, startsOn: "EDIT"}, "nu")64 s.keys("files = ls | where type == file # not sub", "<enter>", "$files | where size > 1kb | sort-by size --reverse", "<shift+enter>")65 s.waitFor("Out[1]:")66 s.waitFor("big.txt")67 s.keys("<enter>", "$files | length", "<shift+enter>")68 s.waitFor("Out[2]:")69 scr := s.screen()70 if strings.Contains(scr, "failed") || strings.Contains(scr, "small.txt") {71 t.Errorf("screen:\n%s", scr)72 }73 s.eventually("the second cell reads 2 files", func() bool {74 for _, l := range strings.Split(s.screen(), "\n") {75 if strings.Contains(l, "Out[2]:") && strings.HasSuffix(strings.TrimSpace(l), " 2") {76 return true77 }78 }79 return false80 })81}8283func TestNotebookSendsToSheetWithNu(t *testing.T) {84 t.Parallel()85 needNu(t)86 s := startWith(t, options{startsOn: "EDIT"}, "nu")87 s.keys("n = [[k]; [1] [2] [3]]", "<ctrl+enter>")88 s.waitFor("Out[1]:")89 s.keys("G", "<enter>")90 s.waitFor("Sent n to")91 s.keys("<ctrl+pgdown>", "<ctrl+home>", "<right>", "<right>", "=SUM(nu.n)", "<enter>")92 s.waitFor(" 6")93 s.keys("<ctrl+pgup>", "<enter>", " | append {k: 10}", "<ctrl+enter>")94 s.waitFor("Out[2]:")95 s.keys("<ctrl+pgdown>")96 s.waitFor(" 16")97}9899// With the real nu, a cell being written is highlighted by nu's shapes100// once typing pauses, and Tab completes what only nu knows: a flag.101func TestNotebookHighlightsAndCompletesWithNu(t *testing.T) {102 t.Parallel()103 needNu(t)104 s := startWith(t, options{startsOn: "EDIT"}, "nu")105 s.keys("ls | where size > 1kb | sort-by size")106 s.eventually("nu's highlighting", func() bool { return nuHighlighted(s.html(), "sort-by", "size") })107 s.keys(" --rev", "<tab>")108 s.waitFor("sort-by size --reverse")109}110111// With the real nu, the caret resting on a command says its signature112// on the context line, and on a flag what the flag does; F1 opens the113// command's help, with its page in nushell's docs as a link. On a114// cell's $name, the line says the cell and its output's shape.115func TestNotebookHoverWithNu(t *testing.T) {116 t.Parallel()117 needNu(t)118 s := startWith(t, options{cols: 120, rows: 30, startsOn: "EDIT"}, "nu")119 s.keys("n = [[k v]; [1 a] [2 b] [3 c]]", "<shift+enter>")120 s.waitFor("Out[1]:")121 s.keys("<enter>", "$n | sort-by k --reverse", "<home>")122 s.waitFor("$n: table, 3 rows × 2 columns from cell 1")123 s.keys("<right>", "<right>", "<right>", "<right>", "<right>", "<right>")124 s.waitFor("sort-by <...comparator: cell-path|closure>")125 s.waitFor("Sort by the given cell path or closure.")126 s.keys("<end>")127 s.waitFor("sort-by --reverse, -r")128 s.keys("<f1>")129 s.waitFor("Docs: https://www.nushell.sh/commands/docs/sort-by.html")130 s.waitFor("HELP")131 if got := s.linkAt("https://www.nushell.sh"); got != "https://www.nushell.sh/commands/docs/sort-by.html" {132 t.Errorf("the docs link to %q", got)133 }134 s.keys("<esc>")135 s.waitFor("EDIT")136}137138// Data > Shell from a workbook makes a Notebook tab; $selection is139// the range selected on the sheet shown before it.140func TestNotebookReadsSelection(t *testing.T) {141 t.Parallel()142 needNu(t)143 s := start(t, "")144 s.keys("n", "<enter>", "3", "<enter>", "4", "<enter>", "<up>", "<up>", "<up>", "<shift+down>", "<shift+down>", "<shift+down>")145 s.waitFor(" A1:A4 ")146 s.keys("<ctrl+k>", "Open notebook", "<enter>")147 s.waitFor("NOTEBOOK")148 s.keys("!", "$selection | get n | math sum", "<ctrl+enter>")149 s.waitFor("[1]")150 s.waitFor(" 7")151 if !strings.Contains(s.screen(), "Notebook") {152 t.Errorf("no Notebook tab:\n%s", s.screen())153 }154}155156// A 200-character pipeline at 80 columns is on the screen whole, wrapped157// before its pipes, while it's edited and once it isn't.158func TestNotebookLongPipeline(t *testing.T) {159 t.Parallel()160 s := startWith(t, options{cols: 80, rows: 24, startsOn: "EDIT", env: fakeNu}, "nu")161 var b strings.Builder162 b.WriteString("ls")163 for i := 0; b.Len() < 200; i++ {164 b.WriteString(" | where size > " + strings.Repeat("9", i%4+1) + "kb")165 }166 src := b.String()167 s.keys(src)168 whole := func() bool {169 var got []string170 for _, l := range strings.Split(s.screen(), "\n") {171 got = append(got, strings.Fields(boxText(l))...)172 }173 return strings.Join(got, " ") == strings.Join(strings.Fields(src), " ")174 }175 s.eventually("the whole pipeline, edited", whole)176 s.keys("<esc>")177 s.waitFor("NOTEBOOK")178 if !whole() {179 t.Errorf("the pipeline isn't whole after editing:\n%s", s.screen())180 }181}182183// linkAt is the hyperlink under the first cell of text on the screen.184func (s *session) linkAt(text string) string {185 for y, line := range strings.Split(s.screen(), "\n") {186 if i := strings.Index(line, text); i >= 0 {187 s.mu.Lock()188 defer s.mu.Unlock()189 ref, err := s.vt.GridRef(ghostty.Point{Tag: ghostty.PointTagActive, X: uint16(utf8.RuneCountInString(line[:i])), Y: uint32(y)})190 if err != nil {191 return ""192 }193 u, _ := ref.HyperlinkURI()194 return u195 }196 }197 return ""198}