package app import ( "os" "path/filepath" "strings" "testing" "github.com/gdamore/tcell/v2" "rickub.com/turbo-editors/turbo-core/tools" "rickub.com/turbo-editors/turbo-core/ui" ) // parametersBox returns the parameters dialog on screen, failing the test when // there is none. func parametersBox(t *testing.T, a *App) *ui.Dialog { t.Helper() dialog := a.TopModal() if dialog == nil { t.Fatal("no dialog opened; the command must not run before its values are given") } return dialog } // typeInto sends a run of characters to the dialog in front. func typeInto(a *App, text string) { for _, r := range text { a.handle(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone)) } } // answer types a value into the first field and presses Enter on OK. func answer(t *testing.T, a *App, value string) { t.Helper() parametersBox(t, a) typeInto(a, value) a.handle(tcell.NewEventKey(tcell.KeyEnter, 0, tcell.ModNone)) } func TestAToolWithAPlaceholderAsksBeforeItRuns(t *testing.T) { // The whole point: nothing is started until the value is given, so a // half-formed command never reaches the shell. a, _ := newToolsApp(t, "") a.RunTool(tools.Tool{Name: "~I~nit", Command: "touch {{file name}}", Output: tools.OutputPopup}) dialog := parametersBox(t, a) if got := dialog.Title(); got != "Init" { t.Errorf("the box is titled %q, want the tool's name without its hot-key markers", got) } if a.running != nil { t.Error("the command started before the box was answered") } } func TestAToolWithNoPlaceholderRunsStraightAway(t *testing.T) { a, _ := newToolsApp(t, "") runAndDrain(t, a, tools.Tool{Name: "Echo", Command: "echo plain", Output: tools.OutputPopup}) if a.running == nil { t.Fatal("an ordinary tool did not run") } if lines := a.running.run.Lines(); len(lines) == 0 || lines[0] != "plain" { t.Errorf("the command printed %v, want plain", lines) } } func TestAnsweringTheBoxRunsTheFilledCommand(t *testing.T) { a, _ := newToolsApp(t, "") a.RunTool(tools.Tool{Name: "Init", Command: "echo {{message}}", Output: tools.OutputPopup}) answer(t, a, "hello there") waitForLoopTurn(t, a, "the filled command to finish", func() bool { return a.running != nil && a.runningFinished() }) lines := a.running.run.Lines() if len(lines) == 0 || lines[0] != "hello there" { t.Fatalf("the command printed %v, want %q — the value did not reach it", lines, "hello there") } // Shell-quoted, so the space stayed inside one argument rather than // becoming two. if got := a.running.run.Command(); !strings.Contains(got, "'hello there'") { t.Errorf("the command was %q, want the value quoted", got) } } func TestCancellingTheBoxRunsNothing(t *testing.T) { a, _ := newToolsApp(t, "") a.RunTool(tools.Tool{Name: "Init", Command: "touch {{name}}", Output: tools.OutputPopup}) parametersBox(t, a) typeInto(a, "cancelled.txt") a.handle(tcell.NewEventKey(tcell.KeyEscape, 0, tcell.ModNone)) if a.Modals() != 0 { t.Fatal("Escape left the box open") } if a.running != nil { t.Error("the command ran although the box was cancelled") } if _, err := os.Stat("cancelled.txt"); err == nil { t.Error("the cancelled command created its file") } } func TestTheValuesAreRememberedForTheNextRun(t *testing.T) { // Running the same parameterised command twice should not mean typing the // same thing twice. a, _ := newToolsApp(t, "") tool := tools.Tool{Name: "Init", Command: "echo {{message}}", Output: tools.OutputPopup} a.RunTool(tool) answer(t, a, "remembered") waitForLoopTurn(t, a, "the first run to finish", func() bool { return a.running != nil && a.runningFinished() }) a.finishRun() a.RunTool(tool) parametersBox(t, a) if got := a.toolValues[tool.Name]["message"]; got != "remembered" { t.Fatalf("the remembered value is %q, want %q", got, "remembered") } // Enter alone, with nothing typed, must re-run what was typed before. a.handle(tcell.NewEventKey(tcell.KeyEnter, 0, tcell.ModNone)) waitForLoopTurn(t, a, "the second run to finish", func() bool { return a.running != nil && a.runningFinished() }) if lines := a.running.run.Lines(); len(lines) == 0 || lines[0] != "remembered" { t.Errorf("the second run printed %v, want the remembered value", lines) } } func TestValuesAreRememberedPerToolAndNotShared(t *testing.T) { a, _ := newToolsApp(t, "") a.RunTool(tools.Tool{Name: "First", Command: "echo {{message}}", Output: tools.OutputPopup}) answer(t, a, "one") waitForLoopTurn(t, a, "the first tool to finish", func() bool { return a.running != nil && a.runningFinished() }) a.finishRun() a.RunTool(tools.Tool{Name: "Second", Command: "echo {{message}}", Output: tools.OutputPopup}) parametersBox(t, a) if got := a.toolValues["Second"]["message"]; got != "" { t.Errorf("the second tool starts with %q; a value belongs to the tool it was typed for", got) } } func TestNothingIsWrittenToTheProjectDirectory(t *testing.T) { // The values live for the session only. A value somebody typed this // afternoon is not a decision the project made. a, root := newToolsApp(t, "") a.RunTool(tools.Tool{Name: "Init", Command: "echo {{message}}", Output: tools.OutputPopup}) answer(t, a, "not persisted") waitForLoopTurn(t, a, "the command to finish", func() bool { return a.running != nil && a.runningFinished() }) if entries, err := os.ReadDir(filepath.Join(root, testProfile().ProjectDir())); err == nil { for _, entry := range entries { if entry.Name() != tools.FileName { t.Errorf("running a parameterised tool left %q behind", entry.Name()) } } } } func TestSeveralPlaceholdersGetAFieldEach(t *testing.T) { a, _ := newToolsApp(t, "") a.RunTool(tools.Tool{Name: "Copy", Command: "echo {{from}} {{to}}", Output: tools.OutputPopup}) parametersBox(t, a) // Tab moves between the fields; type into each in turn. typeInto(a, "here") a.handle(tcell.NewEventKey(tcell.KeyTab, 0, tcell.ModNone)) typeInto(a, "there") a.handle(tcell.NewEventKey(tcell.KeyEnter, 0, tcell.ModNone)) waitForLoopTurn(t, a, "the command to finish", func() bool { return a.running != nil && a.runningFinished() }) if lines := a.running.run.Lines(); len(lines) == 0 || lines[0] != "here there" { t.Errorf("the command printed %v, want both values", lines) } } func TestARawPlaceholderReachesTheShellUnquoted(t *testing.T) { // The escape hatch, end to end: one field standing for several arguments. a, _ := newToolsApp(t, "") a.RunTool(tools.Tool{Name: "Echo", Command: "echo {{words...}}", Output: tools.OutputPopup}) answer(t, a, "a b c") waitForLoopTurn(t, a, "the command to finish", func() bool { return a.running != nil && a.runningFinished() }) if got := a.running.run.Command(); got != "echo a b c" { t.Errorf("the command was %q, want the value verbatim", got) } } func TestATerminalToolIsAlsoAskedFirst(t *testing.T) { // The other output destination takes the same path, and the window's title // is the filled command rather than the one with braces in it. a, _ := newToolsApp(t, "") a.RunTool(tools.Tool{Name: "Run", Command: "echo {{message}}", Output: tools.OutputTerminal}) parametersBox(t, a) answer(t, a, "in a terminal") window := a.Desktop().Active() if window == nil { t.Fatal("no terminal window opened") } if got := window.Title(); !strings.Contains(got, "'in a terminal'") { t.Errorf("the window is titled %q, want the filled command", got) } } func TestATallDemandIsRefusedRatherThanShownOffScreen(t *testing.T) { // A dialog whose OK button is below the bottom of the screen cannot be // answered, which is worse than being told it will not fit. a, _ := newToolsApp(t, "") command := "echo" for i := range MaxParameterFields(24) + 1 { command += " {{value " + string(rune('a'+i)) + "}}" } a.RunTool(tools.Tool{Name: "Too many", Command: command, Output: tools.OutputPopup}) dialog := a.TopModal() if dialog == nil { t.Fatal("nothing was shown at all") } if got := dialog.Title(); got != a.toolsMenuName() { t.Errorf("the box is titled %q, want a message from the toolchain menu", got) } if a.running != nil { t.Error("the command ran although its values were never given") } } func TestMaxParameterFieldsGrowsWithTheScreen(t *testing.T) { if got := MaxParameterFields(24); got != 9 { t.Errorf("MaxParameterFields(24) = %d, want 9", got) } if MaxParameterFields(50) <= MaxParameterFields(24) { t.Error("a taller screen must fit more fields") } // A screen too small for even one still offers one, because refusing every // parameterised tool on a short terminal is worse than a clipped box. if got := MaxParameterFields(4); got != 1 { t.Errorf("MaxParameterFields(4) = %d, want 1", got) } } func TestTheBoxStartsFromWhatWasRemembered(t *testing.T) { box := NewParametersDialog("Init", []string{"module"}, map[string]string{"module": "example.com/mine"}, ui.Rect{W: 80, H: 24}) if got := box.Values()["module"]; got != "example.com/mine" { t.Errorf("the field holds %q, want the remembered value", got) } } func TestTheBoxAsksForEveryLabelItWasGiven(t *testing.T) { box := NewParametersDialog("Copy", []string{"from", "to"}, nil, ui.Rect{W: 80, H: 24}) values := box.Values() if len(values) != 2 { t.Fatalf("Values() = %v, want one entry per label", values) } for _, label := range []string{"from", "to"} { if _, asked := values[label]; !asked { t.Errorf("Values() has no entry for %q", label) } } }