1package e2e23import (4 "os"5 "path/filepath"6 "regexp"7 "strings"8)910// Golden screens for notebooks: cells with a note and table outputs, a11// cell edited with a long pipeline wrapped (at 80 and 60 columns), a12// failure, nu's highlighting and a problem nu found, a cell running and13// one waiting, a cell running as a stream, stale outputs, an output's14// grid entered with a range selected, its filter open, and full-screen,15// and an output sent to a sheet. Each is recorded on a light terminal16// and in the high-contrast theme too. nu is testdata/nu/nu, which17// answers what the screens run, and what the code editor asks, with18// fixed outputs.1920// fakeNu puts the stand-in nu first on the PATH.21var fakeNu = func() []string {22 dir, err := filepath.Abs(filepath.Join("testdata", "nu"))23 if err != nil {24 panic(err)25 }26 return []string{"PATH=" + dir + string(os.PathListSeparator) + os.Getenv("PATH")}27}()2829// nuHighlighted reports whether the line of screen html holding mark30// draws word twice in two styles, as nu's shapes of `where size > 1kb31// | sort-by size` do (a column, then a string) where the tokenizer32// leaves both plain.33func nuHighlighted(html, mark, word string) bool {34 for line := range strings.SplitSeq(html, "\n") {35 if !strings.Contains(line, mark) {36 continue37 }38 var styles []string39 for _, m := range regexp.MustCompile(`<span style="([^"]*)">`+regexp.QuoteMeta(word)+`</span>`).FindAllStringSubmatch(line, -1) {40 styles = append(styles, m[1])41 }42 return len(styles) == 2 && styles[0] != styles[1]43 }44 return false45}4647// longPipeline is a pipeline too long for one line at 80 columns.48const longPipeline = "$files | where size > 1kb | where type == file | sort-by size --reverse | select name size | first 10 | rename file bytes"4950// filesNotebook turns 012 nu's first cell into a note, then writes and51// runs a cell listing files and one reading it, leaving a new cell52// selected.53func filesNotebook(s *session) {54 s.keys("<esc>", "m", "<enter>", "# Files", "<enter>", "What's big in the repo, from `ls`.", "<esc>")55 s.keys("b", "<enter>", "files = ls", "<shift+enter>")56 s.waitFor("Out[1]:")57 s.keys("<enter>", "$files | where size > 1kb", "<shift+enter>")58 s.waitFor("Out[2]:")59 s.waitFor("NOTEBOOK")60}6162var notebookScreens = []screen{63 {name: "notebook-cells", setup: func(s *session) {64 filesNotebook(s)65 s.keys("<up>", "<up>")66 s.waitFor("reads $files")67 }},68 {name: "notebook-wrap", opts: options{cols: 80, rows: 24}, setup: func(s *session) {69 filesNotebook(s)70 s.keys("<enter>", longPipeline)71 s.waitFor("rename file bytes")72 }},73 {name: "notebook-wrap-narrow", opts: options{cols: 60, rows: 24}, setup: func(s *session) {74 filesNotebook(s)75 s.keys("<enter>", longPipeline, "<ctrl+enter>")76 s.waitFor("[3]:")77 }},78 {name: "notebook-error", setup: func(s *session) {79 filesNotebook(s)80 s.keys("<enter>", "lss -a", "<ctrl+enter>")81 s.waitFor("× Command `lss` not found")82 }},83 {name: "notebook-running", setup: func(s *session) {84 filesNotebook(s)85 s.keys("<enter>", "sleep 10min", "<esc>", "b", "<enter>", "{name: 1}", "<esc>", "<f9>")86 // F9 queues every cell; the first two finish, then sleep runs.87 s.waitFor("busy, 1 waiting")88 }},89 {name: "notebook-stale", setup: func(s *session) {90 filesNotebook(s)91 s.keys("<up>", "<up>", "<up>", "<up>", "<enter>", " | first 4", "<esc>")92 s.waitFor("stale")93 }},94 {name: "notebook-output", setup: func(s *session) {95 filesNotebook(s)96 s.keys("<up>", "<up>", "<up>", "<enter>", "<enter>")97 s.waitFor("◀ Back")98 }},99 {name: "notebook-grid", setup: func(s *session) {100 filesNotebook(s)101 s.keys("<up>", "<up>", "<up>", "<enter>", "<shift+down>", "<shift+right>", "<shift+right>")102 s.waitFor("Count 6")103 }},104 {name: "notebook-grid-filter", setup: func(s *session) {105 filesNotebook(s)106 s.keys("<up>", "<up>", "<up>", "<enter>", "<ctrl+k>", "Create a filter", "<enter>")107 s.waitFor("▾")108 s.keys("<right>", "<alt+down>")109 s.waitFor("Select all")110 }},111 {name: "notebook-nu-highlight", setup: func(s *session) {112 filesNotebook(s)113 s.keys("<enter>", "big = $files | where size > 1kb | sort-by size --reverse # largest")114 s.eventually("nu's highlighting", func() bool { return nuHighlighted(s.html(), "largest", "size") })115 }},116 {name: "notebook-nu-error", setup: func(s *session) {117 filesNotebook(s)118 s.keys("<enter>", "$files | sort-by size --revrse")119 s.waitFor("doesn't have flag `revrse`")120 }},121 {name: "notebook-nu-hover", setup: func(s *session) {122 filesNotebook(s)123 s.keys("<enter>", "$files | sort-by size --reverse", "<home>")124 s.keys(strings.Split(strings.Repeat("<right>,", 9), ",")[:9]...)125 s.waitFor("Sort by the given")126 }},127 {name: "notebook-nu-help", setup: func(s *session) {128 filesNotebook(s)129 s.keys("<enter>", "$files | sort-by size --reverse", "<home>")130 s.keys(strings.Split(strings.Repeat("<right>,", 9), ",")[:9]...)131 s.waitFor("Sort by the given")132 s.keys("<f1>")133 s.waitFor("Docs:")134 }},135 {name: "notebook-select", opts: options{cols: 120, rows: 30}, setup: func(s *session) {136 filesNotebook(s)137 s.keys("<up>", "<up>", "<shift+up>")138 s.waitFor("2 cells selected")139 }},140 {name: "notebook-scroll", setup: func(s *session) {141 filesNotebook(s)142 s.keys("<enter>", "1..30 | each {|i| {n: $i, square: ($i * $i)}}", "<ctrl+enter>")143 s.waitFor("Out[3]:")144 s.keys("<esc>", "<down>", "<down>", "<down>", "<down>")145 s.waitFor("rows 4 to 13 of 30")146 }},147 {name: "notebook-stream", setup: func(s *session) {148 filesNotebook(s)149 s.keys("<enter>", "log = tail -f app.log | lines | parse '{time} {level} {msg}'", "<esc>", "f")150 // The head counts the rows the output has, so the grid is fitted151 // to all three: only the first shows.152 s.waitFor("● live, 3 rows")153 }},154 {name: "notebook-sent", setup: func(s *session) {155 filesNotebook(s)156 s.keys("<up>", "<up>", "<up>", "<up>", "G", "<enter>")157 s.waitFor("Sent files to")158 s.keys("<ctrl+pgdown>", "<down>")159 s.waitFor("Output of files")160 }},161}162163func init() {164 for _, sc := range notebookScreens {165 sc.args, sc.opts.startsOn, sc.opts.env = []string{"nu"}, "EDIT", fakeNu166 light, hc := sc, sc167 light.name += "-light"168 light.opts.light = true169 hc.name += "-high-contrast"170 hc.opts.config = highContrastConfig171 screens = append(screens, sc, light, hc)172 }173}