package app import ( "os" "path/filepath" "runtime" "strings" "testing" "time" "github.com/gdamore/tcell/v2" "codeberg.org/turbo-editors/turbo-core/terminal" ) // newTerminalTestApp returns an editor whose terminals run a plain /bin/sh. // // The shell is pinned rather than taken from SHELL so the tests do not depend // on whoever is running them: a login shell with a themed prompt writes enough // to the screen to hide what a test is looking for. func newTerminalTestApp(t *testing.T) (*App, tcell.SimulationScreen) { t.Helper() skipWithoutPTY(t) t.Setenv("SHELL", "/bin/sh") return newTestApp(t) } // skipWithoutPTY skips a test on a platform with no pseudo-terminals, which is // where NewTerminal is expected to refuse rather than work. func skipWithoutPTY(t *testing.T) { t.Helper() if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { t.Skipf("pseudo-terminals are not supported on %s yet", runtime.GOOS) } if _, err := os.Stat("/dev/ptmx"); err != nil { t.Skipf("no /dev/ptmx here: %v", err) } } // runInTerminal types a command into the front window and waits for a string to // appear on the screen. // // It redraws while it waits, which is what makes it an end-to-end check: the // text only turns up if the key routing, the pseudo-terminal, the emulator and // the drawing all work. func runInTerminal(t *testing.T, a *App, screen tcell.SimulationScreen, command, want string) { t.Helper() typeText(a, command) press(a, tcell.KeyEnter, 0, tcell.ModNone) deadline := time.After(20 * time.Second) for { lines := render(t, a, screen) for _, line := range lines { if strings.Contains(line, want) { return } } select { case <-deadline: t.Fatalf("after %q the screen never showed %q; it shows:\n%s", command, want, strings.Join(lines, "\n")) case <-time.After(10 * time.Millisecond): } } } func TestNewTerminalOpensAWindowHoldingAShell(t *testing.T) { a, _ := newTerminalTestApp(t) a.NewTerminal() t.Cleanup(a.closeTerminals) if a.Desktop().Count() != 1 { t.Fatalf("Count() = %d, want 1", a.Desktop().Count()) } if !a.isTerminalWindow(a.Desktop().Active()) { t.Error("the new window does not hold a terminal") } if a.activeTerminal() == nil { t.Error("activeTerminal() returned nil with a terminal in front") } if a.activeView() != nil { t.Error("activeView() returned an editor for a terminal window") } } func TestATerminalRunsWhatIsTypedIntoIt(t *testing.T) { a, screen := newTerminalTestApp(t) a.NewTerminal() t.Cleanup(a.closeTerminals) // The quotes are what makes this a test of the shell rather than of the // echo: what is typed reads "turbo''-go-works", and only a shell that // really ran it puts "turbo-go-works" on the screen. runInTerminal(t, a, screen, "echo turbo''-go-works", "turbo-go-works") } func TestATerminalStartsBesideTheFileBeingEdited(t *testing.T) { a, screen := newTerminalTestApp(t) directory := t.TempDir() // The marker names the directory without naming it in the command, so // finding it on screen can only mean the shell really started there. writeTestFile(t, filepath.Join(directory, "marker-is-here"), "") writeTestFile(t, filepath.Join(directory, "main.go"), "package main\n") a.Open(filepath.Join(directory, "main.go")) a.NewTerminal() t.Cleanup(a.closeTerminals) runInTerminal(t, a, screen, "ls", "marker-is-here") } func TestATerminalStartsInTheWorkingDirectoryWithNoFileOpen(t *testing.T) { a, _ := newTerminalTestApp(t) working, err := os.Getwd() if err != nil { t.Fatalf("reading the working directory: %v", err) } if got := a.terminalDirectory(); got != working { t.Errorf("terminalDirectory() = %q, want the working directory %q", got, working) } } func TestClosingATerminalWindowTakesItAway(t *testing.T) { a, _ := newTerminalTestApp(t) a.NewTerminal() a.CloseFile() if a.Desktop().Count() != 0 { t.Errorf("Count() = %d, want 0", a.Desktop().Count()) } if len(a.terminals) != 0 { t.Errorf("%d terminals are still recorded", len(a.terminals)) } if a.Modals() != 0 { t.Error("closing a terminal asked about unsaved changes") } } func TestLeavingTheEditorClosesEveryTerminal(t *testing.T) { a, _ := newTerminalTestApp(t) a.NewTerminal() a.NewTerminal() a.Quit() if !a.quitting { t.Error("Quit() did not ask the editor to stop") } if len(a.terminals) != 0 { t.Errorf("%d terminals were left running", len(a.terminals)) } } func TestTheFileActionsLeaveATerminalAlone(t *testing.T) { a, _ := newTerminalTestApp(t) a.NewTerminal() t.Cleanup(a.closeTerminals) // None of these has anything to act on, and each of them used to assume // every window held an editor. a.SaveFile() a.SaveFileAs() a.ToggleLineNumbers() a.RequestCompletion() if a.Modals() != 0 { t.Errorf("%d dialogs opened over a terminal window", a.Modals()) } if a.Desktop().Count() != 1 { t.Errorf("Count() = %d, want the terminal to still be there", a.Desktop().Count()) } } func TestAFocusedTerminalKeepsTheKeysAShellNeeds(t *testing.T) { a, _ := newTerminalTestApp(t) a.NewTerminal() t.Cleanup(a.closeTerminals) // Ctrl-W deletes a word in a shell. If the editor kept it, it would close // the window instead, which is the whole reason terminalTakesKey exists. press(a, tcell.KeyCtrlW, 0, tcell.ModCtrl) if a.Desktop().Count() != 1 { t.Error("Ctrl-W closed the terminal instead of reaching the shell") } } func TestAFocusedTerminalStillLetsTheEditorHaveItsOwnKeys(t *testing.T) { a, _ := newTerminalTestApp(t) a.NewTerminal() t.Cleanup(a.closeTerminals) press(a, tcell.KeyF8, 0, tcell.ModNone) // Window ▸ New terminal if a.Desktop().Count() != 2 { t.Fatalf("Count() = %d, want F8 to have opened a second terminal", a.Desktop().Count()) } press(a, tcell.KeyRune, 'x', tcell.ModAlt) // Exit if !a.quitting { t.Error("Alt-X did not leave the editor with a terminal in front") } } func TestAPlatformWithoutPseudoTerminalsSaysSo(t *testing.T) { a, _ := newTestApp(t) a.reportTerminalFailure(terminal.ErrUnsupported) if a.Modals() != 1 { t.Fatalf("Modals() = %d, want the refusal to be shown", a.Modals()) } if a.Desktop().Count() != 0 { t.Errorf("Count() = %d, want no window for a terminal that never started", a.Desktop().Count()) } } func TestATerminalWindowIsNamedAfterTheProgramInIt(t *testing.T) { a, _ := newTerminalTestApp(t) a.NewTerminal() t.Cleanup(a.closeTerminals) window := a.Desktop().Active() if !strings.Contains(window.Title(), "sh") { t.Errorf("the window is called %q, want the name of the shell", window.Title()) } a.refreshTerminalTitles() if window.Title() == "" { t.Error("refreshing the titles emptied one") } }