cat / 012
clone

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

main
012 / internal / ui / serve_test.go
serve_test.go 6.0K
1package ui23import (4 "os"5 "path/filepath"6 "slices"7 "strings"8 "testing"9 "time"1011 tea "charm.land/bubbletea/v2"1213 "src.rest/cat/012/internal/confine"14 "src.rest/cat/012/internal/fileio"15 "src.rest/cat/012/internal/sheet"16 "src.rest/cat/012/internal/ui/choicebar"17 "src.rest/cat/012/internal/ui/transfer"18)1920// writeSheet saves a one-cell sheet at path.21func writeSheet(t *testing.T, path, a1 string) {22 t.Helper()23 s := sheet.New()24 s.Set(addr("A1"), a1)25 f, err := os.Create(path)26 if err != nil {27 t.Fatal(err)28 }29 defer f.Close()30 if err := s.Write(f); err != nil {31 t.Fatal(err)32 }33}3435// A served session reads and writes only inside its directory, whatever36// the server's working directory is.37func TestServedFilesStayInside(t *testing.T) {38 top := t.TempDir()39 served, elsewhere := filepath.Join(top, "served"), filepath.Join(top, "elsewhere")40 os.Mkdir(served, 0o755)41 os.Mkdir(elsewhere, 0o755)42 writeSheet(t, filepath.Join(served, "inside.012"), "in")43 writeSheet(t, filepath.Join(elsewhere, "outside.012"), "out")44 os.WriteFile(filepath.Join(served, "data.csv"), []byte("a,b\n1,2\n"), 0o644)45 os.WriteFile(filepath.Join(elsewhere, "other.csv"), []byte("x\n"), 0o644)46 os.Symlink("../elsewhere/outside.012", filepath.Join(served, "link.012"))47 t.Chdir(elsewhere)4849 root, err := confine.New(served)50 if err != nil {51 t.Fatal(err)52 }53 m := newModel()54 m.Serve(root, []string{"TERM=xterm-256color"})5556 // File > Open lists the served directory's sheets.57 press(t, m, "<ctrl+o>")58 if m.prompt == nil || !slices.Equal(m.prompt.files, []string{"inside.012", "link.012"}) {59 t.Fatalf("Open lists %v", m.prompt.files)60 }61 press(t, m, "inside", "<enter>")62 if m.mode != modeReady || input(m, "A1") != "in" || m.filename != "inside.012" {63 t.Fatalf("open inside: mode %v A1 %q file %q err %q", m.mode, input(m, "A1"), m.filename, m.errMsg)64 }6566 for _, name := range []string{"../elsewhere/outside", "link", filepath.Join(elsewhere, "outside")} {67 press(t, m, "<ctrl+o>", name, "<enter>")68 if m.mode != modeError || !strings.Contains(m.errMsg, "outside the served directory") {69 t.Errorf("open %s: mode %v, %q", name, m.mode, m.errMsg)70 }71 if !filepath.IsAbs(name) && strings.Contains(m.errMsg, top) {72 t.Errorf("the error names the server's directories: %q", m.errMsg)73 }74 press(t, m, "<esc>")75 }7677 // Save as writes into the served directory, not the working one.78 press(t, m, "<ctrl+s>")79 if _, err := os.Stat(filepath.Join(served, "inside.012")); err != nil {80 t.Fatal(err)81 }82 run(m, m.openSave())83 press(t, m, "copy", "<enter>")84 if _, err := os.Stat(filepath.Join(served, "copy.012")); err != nil {85 t.Errorf("Save as copy: %v (%q)", err, m.errMsg)86 }87 if _, err := os.Stat(filepath.Join(elsewhere, "copy.012")); err == nil {88 t.Error("Save as wrote to the working directory")89 }90 run(m, m.openSave())91 press(t, m, "../escaped", "<enter>")92 if m.mode != modeError {93 t.Errorf("Save as ../escaped: mode %v", m.mode)94 }95 press(t, m, "<esc>")96 if _, err := os.Stat(filepath.Join(top, "escaped.012")); err == nil {97 t.Error("Save as escaped the served directory")98 }99100 // The import picker lists the served directory's files.101 names, _ := importable(m.root)102 if !slices.Equal(names, []string{"data.csv"}) {103 t.Errorf("importable = %v", names)104 }105 run(m, m.startImport("../elsewhere/other.csv", fileio.Options{}, transfer.Book))106 if m.mode != modeError || m.xfer.Busy() {107 t.Errorf("import outside: mode %v busy %v", m.mode, m.xfer.Busy())108 }109}110111// Saving over a file someone else wrote since it was opened asks first.112func TestSaveAsksWhenFileChanged(t *testing.T) {113 t.Chdir(t.TempDir())114 writeSheet(t, "book.012", "first")115 s, err := readSheet("book.012")116 if err != nil {117 t.Fatal(err)118 }119 m := New(s, "book.012")120 m.Update(tea.WindowSizeMsg{Width: 100, Height: 20})121122 // Nobody else wrote it: saving just saves.123 press(t, m, "mine", "<enter>", "<ctrl+s>")124 if m.overlay != nil || m.changed {125 t.Fatalf("plain save: overlay %T changed %v", m.overlay, m.changed)126 }127128 // Another session saves the file.129 writeSheet(t, "book.012", "theirs, longer")130 later := time.Now().Add(time.Minute)131 os.Chtimes("book.012", later, later)132133 press(t, m, "<up>", "again", "<enter>", "<ctrl+s>")134 if _, ok := m.overlay.(*choicebar.Bar); !ok || !strings.Contains(line(m, contextLine), "book.012 changed on disk since it was opened") {135 t.Fatalf("context line %q, overlay %T", line(m, contextLine), m.overlay)136 }137 press(t, m, "<esc>")138 if !m.changed {139 t.Error("cancelled save cleared the modified flag")140 }141 if got, _ := readSheet("book.012"); a1Of(got) != "theirs, longer" {142 t.Error("cancelled save wrote the file")143 }144145 // Asked again, Enter overwrites; saving after that doesn't ask.146 press(t, m, "<ctrl+s>", "<enter>")147 if m.overlay != nil || m.changed {148 t.Fatalf("overwrite: overlay %T changed %v", m.overlay, m.changed)149 }150 if got, _ := readSheet("book.012"); a1Of(got) != "again" {151 t.Errorf("A1 on disk %q", a1Of(got))152 }153 press(t, m, "<up>", "more", "<enter>", "<ctrl+s>")154 if m.overlay != nil || m.changed {155 t.Errorf("save after overwrite: overlay %T changed %v", m.overlay, m.changed)156 }157158 // A file deleted meanwhile is written again without asking.159 os.Remove("book.012")160 press(t, m, "<up>", "back", "<enter>", "<ctrl+s>")161 if got, _ := readSheet("book.012"); m.overlay != nil || a1Of(got) != "back" {162 t.Errorf("save after delete: overlay %T, A1 on disk %q", m.overlay, a1Of(got))163 }164165 // Quitting with Save and quit waits for the answer, then quits.166 writeSheet(t, "book.012", "theirs again, longer")167 press(t, m, "<up>", "last", "<enter>")168 quit := press(t, m, "<ctrl+q>", "<enter>")169 if quit != nil || !strings.Contains(line(m, contextLine), "changed on disk") {170 t.Fatalf("Save and quit didn't ask: %v, %q", quit, line(m, contextLine))171 }172 if _, ok := press(t, m, "<enter>").(tea.QuitMsg); !ok {173 t.Error("Overwrite didn't finish Save and quit")174 }175}176177func a1Of(s *sheet.Sheet) string {178 if s == nil || s.Cell(addr("A1")) == nil {179 return ""180 }181 return s.Cell(addr("A1")).Input182}183184func readSheet(name string) (*sheet.Sheet, error) {185 f, err := os.Open(name)186 if err != nil {187 return nil, err188 }189 defer f.Close()190 return sheet.Read(f)191}