1package ui23import (4 "strings"5 "testing"6 "time"78 tea "charm.land/bubbletea/v2"910 "src.rest/cat/012/internal/sheet"11)1213// Scripts run against the model: the API, errors, limits and Esc.1415func TestAScriptOutlivesNoProgram(t *testing.T) {16 defer func(d time.Duration) { orphanAfter = d }(orphanAfter)17 orphanAfter = 20 * time.Millisecond18 m := newModel()19 m.book().SaveMacro("", sheet.Macro{Name: "Spin", Source: "while True:\n move(0, 0)\n"}, "add")20 m.trustHere()21 m.runMacro(mustMacro(t, m, "Spin")) // its wait is never run: the program is gone22 select {23 case d := <-m.macros.run.done:24 if d.err == nil {25 t.Error("finished without an error")26 }27 case <-time.After(5 * time.Second):28 t.Fatal("the script's goroutine is still waiting")29 }30}3132// script saves src as a trusted macro and runs it to the end.33func script(t *testing.T, m *Model, src string) {34 t.Helper()35 old := ""36 if hasMacro(m.book(), "S") {37 old = "S"38 }39 if err := m.book().SaveMacro(old, sheet.Macro{Name: "S", Source: src}, "add"); err != nil {40 t.Fatal(err)41 }42 m.trustHere()43 run(m, m.runMacro(mustMacro(t, m, "S")))44}4546func TestScriptsReadWhatTheyWrote(t *testing.T) {47 m := newModel()48 script(t, m, `49set("A1:A3", [[1], [2], [3]])50set_formula("B1:B3", "A1*10")51total = 052for row in get("B1:B3"):53 total += row[0]54set("C1", total)55set("C2", get_formula("B2"))56number_format("C1", "currency", decimals=0)57set("C3", get_number_format("C1"))58add_sheet("Two")59set("A1", "on " + active_sheet())60activate_sheet("Sheet1")61select("A:B", active="B5")62set("D1", selection() + " " + active_cell())63print("total", total)64`)65 if m.warn != "" {66 t.Fatal(m.warn)67 }68 for a, want := range map[string]string{"C1": "60", "C2": "=A2*10", "C3": "currency", "D1": "A:B B5"} {69 if got := input(m, a); got != want {70 t.Errorf("%s = %q, want %q", a, got, want)71 }72 }73 if two := m.book().Lookup("Two"); two == nil || two.Cell(addr("A1")).Input != "on Two" {74 t.Error("sheet Two")75 }76 if m.whole != wholeCols || m.cur != addr("B5") {77 t.Errorf("selection %v whole %v", m.selection(), m.whole)78 }79 if m.note != "S: total 60" {80 t.Errorf("note %q", m.note)81 }82}8384func TestScriptsAnswerQuestions(t *testing.T) {85 m := newModel()86 script(t, m, `87add_sheet("Two")88set("A1", 1)89run("sheet.delete", answer="enter")90select("A1:A3")91run("data.define_name", answer="Sales")92run("sheet.rename", answer="Budget")93`)94 if m.warn != "" {95 t.Fatal(m.warn)96 }97 if m.book().Len() != 1 || m.sheet.Name() != "Budget" {98 t.Errorf("sheets %d, %q", m.book().Len(), m.sheet.Name())99 }100 if n, ok := m.sheet.LookupName("Sales"); !ok || n.Range.String() != "A1:A3" {101 t.Errorf("name %+v %v", n, ok)102 }103}104105func TestScriptErrorsShowWhere(t *testing.T) {106 m := newModel()107 script(t, m, "set(\"A1\", 5)\nrun(\"format.bold\")\nget(\"nowhere\")\n")108 want := "S:3:4: get: not a cell or range: nowhere Ctrl+Z undoes what it did"109 if m.warn != want {110 t.Errorf("warn %q\nwant %q", m.warn, want)111 }112 if !strings.Contains(line(m, contextLine), "S:3:4: get: not a cell") {113 t.Errorf("context line %q", line(m, contextLine))114 }115 press(t, m, "<ctrl+z>")116 if m.sheet.Len() != 0 {117 t.Errorf("undo left cells: note %q A1 %q undo %q", m.note, input(m, "A1"), m.book().UndoLabel())118 }119120 for src, want := range map[string]string{121 `run("file.save")`: "S:1:4: run: Save (file.save) can't run in a macro",122 `run("column.width")`: `S:1:4: run: Column width asks "Column width (1-240)": give run("column.width", answer=...)`,123 `run("no.such")`: `S:1:4: run: no command "no.such"`,124 `run("data.sort_range")`: `S:1:4: run: Sort range opens a dialog: give run("data.sort_range", answer={...})`,125 `run("data.named_ranges")`: "S:1:4: run: Named ranges opens a dialog; it can't run in a macro",126 "move(0, -1)": "S:1:5: move: moving 0, -1 from A1 goes off the sheet",127 } {128 m := newModel()129 script(t, m, src)130 if !strings.HasPrefix(m.warn, want) {131 t.Errorf("%s: %q, want %q", src, m.warn, want)132 }133 if m.mode != modeReady || m.overlay != nil || m.prompt != nil {134 t.Errorf("%s left mode %v", src, m.mode)135 }136 }137}138139func TestStepLimitStopsARunaway(t *testing.T) {140 defer func(n uint64) { macroMaxSteps = n }(macroMaxSteps)141 macroMaxSteps = 50_000142 m := newModel()143 script(t, m, "set(\"A1\", 1)\nwhile True:\n pass\n")144 if !strings.HasPrefix(m.warn, "S:") || !strings.Contains(m.warn, ": stopped after 50") || !strings.HasSuffix(m.warn, "Ctrl+Z undoes what it did") {145 t.Errorf("warn %q", m.warn)146 }147 if m.macros.run != nil || m.indicator() != "READY" {148 t.Errorf("still running: %q", m.indicator())149 }150}151152func TestEscStopsARun(t *testing.T) {153 m := newModel()154 m.book().SaveMacro("", sheet.Macro{Name: "Spin", Source: "while True:\n move(0, 0)\n"}, "add")155 m.trustHere()156 cmd := m.runMacro(mustMacro(t, m, "Spin"))157 if m.indicator() != "CMD" || !strings.Contains(line(m, contextLine), "Running Spin") {158 t.Errorf("indicator %q, context %q", m.indicator(), line(m, contextLine))159 }160 msg := cmd()161 press(t, m, "x") // held back while running162 m.Update(tea.KeyPressMsg{Code: tea.KeyEscape})163 for range 1000 {164 if _, done := msg.(macroDoneMsg); done {165 break166 }167 _, next := m.Update(msg)168 if next == nil {169 break170 }171 msg = next()172 if b, ok := msg.(tea.BatchMsg); ok {173 msg = b[len(b)-1]()174 }175 }176 if m.macros.run != nil {177 m.Update(msg)178 }179 if m.macros.run != nil || m.warn != "Stopped Spin with Esc" || m.sheet.Len() != 0 {180 t.Errorf("run %v warn %q cells %d", m.macros.run, m.warn, m.sheet.Len())181 }182}