package app import ( "os" "path/filepath" "strings" "testing" "github.com/gdamore/tcell/v2" "codeberg.org/turbo-editors/turbo-core/buffer" "codeberg.org/turbo-editors/turbo-core/editor" "codeberg.org/turbo-editors/turbo-core/theme" "codeberg.org/turbo-editors/turbo-core/ui" "codeberg.org/turbo-editors/turbo-core/version" ) // newTestApp returns an editor drawing on a simulated terminal. // // The whole application is exercised through it — layout, routing, dialogs, // drawing — with only the terminal itself replaced. func newTestApp(t *testing.T) (*App, tcell.SimulationScreen) { t.Helper() screen := tcell.NewSimulationScreen("UTF-8") if err := screen.Init(); err != nil { t.Fatalf("initialising the simulation screen: %v", err) } t.Cleanup(screen.Fini) screen.SetSize(80, 24) t.Setenv(testProfile().ThemeDirEnvVar(), t.TempDir()) // only the embedded themes t.Setenv(testProfile().SnippetDirEnvVar(), t.TempDir()) a := New(screen, "turbo-classic", testProfile()) a.layout() return a, screen } // press sends a key through the whole routing chain, exactly as the event loop // would. func press(a *App, key tcell.Key, r rune, mods tcell.ModMask) { a.handle(tcell.NewEventKey(key, r, mods)) } // typeText sends a run of printable characters. func typeText(a *App, text string) { for _, r := range text { press(a, tcell.KeyRune, r, tcell.ModNone) } } // click sends a left-button press at a screen position. func click(a *App, x, y int) { a.handle(tcell.NewEventMouse(x, y, tcell.Button1, tcell.ModNone)) } // render draws the app and returns what the screen shows, one string per row. func render(t *testing.T, a *App, screen tcell.SimulationScreen) []string { t.Helper() a.layout() a.draw() cells, width, height := screen.GetContents() lines := make([]string, height) for y := range height { var row strings.Builder for x := range width { runes := cells[y*width+x].Runes if len(runes) == 0 { row.WriteRune(' ') continue } row.WriteRune(runes[0]) } lines[y] = row.String() } return lines } // activeBuffer returns the buffer of the front window. func activeBuffer(t *testing.T, a *App) *buffer.Buffer { t.Helper() view := a.activeView() if view == nil { t.Fatal("no window is open") } return view.Buffer() } func TestANewEditorHasNoWindows(t *testing.T) { a, _ := newTestApp(t) if a.Desktop().Count() != 0 { t.Errorf("Count() = %d, want 0", a.Desktop().Count()) } if a.activeView() != nil { t.Error("activeView() returned a view with no window open") } } func TestNewFileOpensAnUntitledWindow(t *testing.T) { a, _ := newTestApp(t) a.NewFile() if a.Desktop().Count() != 1 { t.Fatalf("Count() = %d, want 1", a.Desktop().Count()) } if got := a.Desktop().Active().Title(); got != untitledName { t.Errorf("the window is called %q, want %q", got, untitledName) } } func TestOpenReadsAFile(t *testing.T) { a, _ := newTestApp(t) path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "package main\n") a.Open(path) if a.Desktop().Count() != 1 { t.Fatalf("Count() = %d, want 1", a.Desktop().Count()) } if got := activeBuffer(t, a).Text(); got != "package main\n" { t.Errorf("the buffer holds %q", got) } if got := a.Desktop().Active().Title(); got != "main.go" { t.Errorf("the window is called %q, want main.go", got) } } func TestOpeningTheSameFileTwiceRaisesTheExistingWindow(t *testing.T) { a, _ := newTestApp(t) path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "package main\n") a.Open(path) a.NewFile() a.Open(path) if a.Desktop().Count() != 2 { t.Errorf("Count() = %d, want 2 — the file must not open twice", a.Desktop().Count()) } if got := a.Desktop().Active().Title(); got != "main.go" { t.Errorf("the front window is %q, want the existing main.go raised", got) } } func TestOpeningAnUnreadableFileReportsIt(t *testing.T) { a, _ := newTestApp(t) a.Open(t.TempDir()) // a directory, not a file if a.Modals() != 1 { t.Fatalf("Modals() = %d, want a message box", a.Modals()) } if got := a.TopModal().Title(); got != "Cannot open" { t.Errorf("the box is titled %q", got) } } func TestTypingReachesTheFrontWindow(t *testing.T) { a, _ := newTestApp(t) a.NewFile() typeText(a, "package main") if got := activeBuffer(t, a).Text(); got != "package main" { t.Errorf("the buffer holds %q", got) } } func TestEditingStarsTheWindowTitle(t *testing.T) { a, _ := newTestApp(t) path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "package main\n") a.Open(path) typeText(a, "x") if got := a.Desktop().Active().Title(); got != "main.go *" { t.Errorf("the window is called %q, want a star for unsaved changes", got) } } func TestSaveWritesTheFileAndClearsTheStar(t *testing.T) { a, _ := newTestApp(t) path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "package main\n") a.Open(path) typeText(a, "// ") press(a, tcell.KeyF2, 0, tcell.ModNone) if got := readTestFile(t, path); got != "// package main\n" { t.Errorf("the file holds %q", got) } if got := a.Desktop().Active().Title(); got != "main.go" { t.Errorf("the window is called %q, want the star gone", got) } if !strings.Contains(a.StatusBar().Message(), "Saved") { t.Errorf("the status bar says %q, want it to confirm the save", a.StatusBar().Message()) } } func TestSavingAnUntitledFileAsksForAName(t *testing.T) { a, _ := newTestApp(t) a.NewFile() press(a, tcell.KeyF2, 0, tcell.ModNone) if a.Modals() != 1 { t.Fatalf("Modals() = %d, want the Save as dialog", a.Modals()) } if !strings.HasPrefix(a.TopModal().Title(), "Save as") { t.Errorf("the dialog is titled %q", a.TopModal().Title()) } } func TestF3OpensTheFileDialog(t *testing.T) { a, _ := newTestApp(t) press(a, tcell.KeyF3, 0, tcell.ModNone) if a.Modals() != 1 { t.Fatalf("Modals() = %d, want the Open dialog", a.Modals()) } if !strings.HasPrefix(a.TopModal().Title(), "Open") { t.Errorf("the dialog is titled %q", a.TopModal().Title()) } } func TestEscapeClosesADialogWithoutDoingAnything(t *testing.T) { a, _ := newTestApp(t) press(a, tcell.KeyF3, 0, tcell.ModNone) press(a, tcell.KeyEscape, 0, tcell.ModNone) if a.Modals() != 0 { t.Errorf("Modals() = %d, want the dialog gone", a.Modals()) } if a.Desktop().Count() != 0 { t.Error("a cancelled Open dialog opened a window anyway") } } func TestOpeningAFileThroughTheDialog(t *testing.T) { a, _ := newTestApp(t) directory := t.TempDir() writeTestFile(t, filepath.Join(directory, "hello.go"), "package hello\n") dialog := NewFileDialog("Open", directory, a.screenRect()) a.pushModal(dialog.Dialog(), func(result ui.Result) { if result == ui.ResultOK { a.Open(dialog.Path()) } }) typeText(a, "hello.go") press(a, tcell.KeyEnter, 0, tcell.ModNone) if a.Desktop().Count() != 1 { t.Fatalf("Count() = %d, want the file opened", a.Desktop().Count()) } if got := activeBuffer(t, a).Text(); got != "package hello\n" { t.Errorf("the buffer holds %q", got) } } func TestAModalSwallowsTypingMeantForTheEditor(t *testing.T) { a, _ := newTestApp(t) a.NewFile() press(a, tcell.KeyF3, 0, tcell.ModNone) typeText(a, "zzz") if got := activeBuffer(t, a).Text(); got != "" { t.Errorf("the buffer holds %q, want the typing to have gone to the dialog", got) } } func TestClosingAModifiedFileAsksFirst(t *testing.T) { a, _ := newTestApp(t) a.NewFile() typeText(a, "x") press(a, tcell.KeyCtrlW, 0, tcell.ModNone) if a.Modals() != 1 { t.Fatalf("Modals() = %d, want a confirmation", a.Modals()) } if a.Desktop().Count() != 1 { t.Error("the window was closed before the question was answered") } } func TestAnsweringNoClosesWithoutSaving(t *testing.T) { a, _ := newTestApp(t) a.NewFile() typeText(a, "x") press(a, tcell.KeyCtrlW, 0, tcell.ModNone) press(a, tcell.KeyRune, 'n', tcell.ModAlt) // the No button if a.Modals() != 0 { t.Errorf("Modals() = %d, want the question answered", a.Modals()) } if a.Desktop().Count() != 0 { t.Error("the window is still open after answering No") } } func TestClosingAnUnmodifiedFileAsksNothing(t *testing.T) { a, _ := newTestApp(t) a.NewFile() press(a, tcell.KeyCtrlW, 0, tcell.ModNone) if a.Modals() != 0 { t.Errorf("Modals() = %d, want no question for an untouched file", a.Modals()) } if a.Desktop().Count() != 0 { t.Error("the window is still open") } } func TestQuitAsksAboutUnsavedWork(t *testing.T) { a, _ := newTestApp(t) a.NewFile() typeText(a, "x") press(a, tcell.KeyRune, 'x', tcell.ModAlt) if a.Quitting() { t.Error("the editor quit with unsaved changes and no question") } if a.Modals() != 1 { t.Fatalf("Modals() = %d, want a confirmation", a.Modals()) } } func TestQuitLeavesStraightAwayWithNothingToSave(t *testing.T) { a, _ := newTestApp(t) a.NewFile() press(a, tcell.KeyRune, 'x', tcell.ModAlt) if !a.Quitting() { t.Error("the editor did not quit although nothing was unsaved") } } func TestUndoAndRedoThroughTheMenu(t *testing.T) { a, _ := newTestApp(t) a.NewFile() typeText(a, "abc") a.Undo() if got := activeBuffer(t, a).Text(); got != "" { t.Errorf("the buffer holds %q after undo, want it empty", got) } a.Redo() if got := activeBuffer(t, a).Text(); got != "abc" { t.Errorf("the buffer holds %q after redo", got) } } func TestClipboardIsSharedBetweenWindows(t *testing.T) { a, _ := newTestApp(t) a.NewFile() typeText(a, "shared") a.SelectAll() a.Copy() a.NewFile() a.Paste() if got := activeBuffer(t, a).Text(); got != "shared" { t.Errorf("the second window holds %q, want the copied text", got) } } func TestFindSelectsTheMatch(t *testing.T) { a, _ := newTestApp(t) a.NewFile() typeText(a, "one two three") activeBuffer(t, a).MoveBufferStart() a.lastSearch, a.lastMatchCase = "two", true a.FindNext() if got := activeBuffer(t, a).SelectedText(); got != "two" { t.Errorf("SelectedText() = %q, want the match selected", got) } } func TestFindNextMovesOffTheCurrentMatch(t *testing.T) { a, _ := newTestApp(t) a.NewFile() typeText(a, "x y x y x") activeBuffer(t, a).MoveBufferStart() a.lastSearch, a.lastMatchCase = "x", true a.FindNext() first := activeBuffer(t, a).Cursor() a.FindNext() second := activeBuffer(t, a).Cursor() if first == second { t.Error("Find next found the same match twice") } } func TestFindReportsWhenThereIsNoMatch(t *testing.T) { a, _ := newTestApp(t) a.NewFile() typeText(a, "abc") a.lastSearch = "zzz" a.FindNext() if !strings.Contains(a.StatusBar().Message(), "not found") { t.Errorf("the status bar says %q, want it to report the failure", a.StatusBar().Message()) } } func TestGoToLine(t *testing.T) { a, _ := newTestApp(t) a.NewFile() activeBuffer(t, a).SetText(strings.Repeat("line\n", 40)) press(a, tcell.KeyCtrlG, 0, tcell.ModNone) typeText(a, "25") press(a, tcell.KeyEnter, 0, tcell.ModNone) if got := activeBuffer(t, a).Cursor().Line; got != 24 { t.Errorf("Cursor().Line = %d, want 24", got) } } func TestGoToLineRejectsNonsense(t *testing.T) { a, _ := newTestApp(t) a.NewFile() press(a, tcell.KeyCtrlG, 0, tcell.ModNone) typeText(a, "banana") press(a, tcell.KeyEnter, 0, tcell.ModNone) if !strings.Contains(a.StatusBar().Message(), "Not a line number") { t.Errorf("the status bar says %q", a.StatusBar().Message()) } } func TestWindowNumbersSelectWindows(t *testing.T) { a, _ := newTestApp(t) a.NewFile() first := a.Desktop().Active() a.NewFile() press(a, tcell.KeyRune, '1', tcell.ModAlt) if a.Desktop().Active() != first { t.Error("Alt-1 did not bring the first window forward") } } func TestAltZeroListsTheWindows(t *testing.T) { a, _ := newTestApp(t) a.NewFile() a.NewFile() press(a, tcell.KeyRune, '0', tcell.ModAlt) if a.Modals() != 1 { t.Fatalf("Modals() = %d, want the window list", a.Modals()) } if got := a.TopModal().Title(); got != "Windows" { t.Errorf("the dialog is titled %q", got) } } func TestF6CyclesWindows(t *testing.T) { a, _ := newTestApp(t) a.NewFile() first := a.Desktop().Active() a.NewFile() press(a, tcell.KeyF6, 0, tcell.ModNone) if a.Desktop().Active() != first { t.Error("F6 did not cycle to the window behind") } } func TestChangingTheTheme(t *testing.T) { a, _ := newTestApp(t) before := a.Theme() a.setTheme("turbo-dark") if a.Theme() == before { t.Error("the theme did not change") } if got := a.ThemeName(); got != "turbo-dark" { t.Errorf("ThemeName() = %q", got) } } func TestAnUnknownThemeFallsBackToTheDefault(t *testing.T) { a, _ := newTestApp(t) a.setTheme("no-such-theme") if got := a.ThemeName(); got != "turbo-classic" { t.Errorf("ThemeName() = %q, want the default", got) } } func TestToggleLineNumbers(t *testing.T) { a, _ := newTestApp(t) a.NewFile() view := a.activeView() a.ToggleLineNumbers() if view.LineNumbers() { t.Error("the gutter is still shown after toggling it off") } } func TestF10OpensTheMenuAndEscapeClosesIt(t *testing.T) { a, _ := newTestApp(t) press(a, tcell.KeyF10, 0, tcell.ModNone) if !a.MenuBar().Open() { t.Fatal("F10 did not open the menu") } press(a, tcell.KeyEscape, 0, tcell.ModNone) if a.MenuBar().Open() { t.Error("Escape did not close the menu") } } func TestAMenuItemRunsItsAction(t *testing.T) { a, _ := newTestApp(t) press(a, tcell.KeyRune, 'f', tcell.ModAlt) // open File press(a, tcell.KeyRune, 'n', tcell.ModNone) // New if a.Desktop().Count() != 1 { t.Errorf("Count() = %d, want the File ▸ New item to have opened a window", a.Desktop().Count()) } if a.MenuBar().Open() { t.Error("the menu stayed open after an item ran") } } func TestItemsThatNeedAFileAreGreyedOutWithoutOne(t *testing.T) { a, _ := newTestApp(t) if a.hasWindow() { t.Fatal("hasWindow() = true with no window open") } a.NewFile() if !a.hasWindow() { t.Error("hasWindow() = false with a window open") } } func TestTheScreenShowsTheFurniture(t *testing.T) { a, screen := newTestApp(t) path := filepath.Join(t.TempDir(), "main.go") writeTestFile(t, path, "package main\n\nfunc main() {}\n") a.Open(path) lines := render(t, a, screen) if !strings.Contains(lines[0], "File") || !strings.Contains(lines[0], "Help") { t.Errorf("the menu bar is %q", lines[0]) } if !strings.Contains(lines[1], "main.go") { t.Errorf("row 1 is %q, want the window's title bar", lines[1]) } if !strings.Contains(lines[2], "package main") { t.Errorf("row 2 is %q, want the file's first line", lines[2]) } if !strings.Contains(lines[23], "F2 Save") { t.Errorf("the status bar is %q", lines[23]) } } func TestTheStatusBarShowsTheCursorPosition(t *testing.T) { a, screen := newTestApp(t) a.NewFile() activeBuffer(t, a).SetText("one\ntwo\nthree") activeBuffer(t, a).SetCursor(buffer.Position{Line: 2, Col: 3}) lines := render(t, a, screen) if !strings.Contains(lines[23], "3:4") { t.Errorf("the status bar is %q, want the cursor at 3:4", lines[23]) } } func TestAResizeIsHandledWithoutPanicking(t *testing.T) { a, screen := newTestApp(t) a.NewFile() screen.SetSize(40, 12) a.handle(tcell.NewEventResize(40, 12)) render(t, a, screen) if got := a.desktopRect(); got.W != 40 || got.H != 10 { t.Errorf("the desktop is %+v, want it to fill the smaller screen", got) } } func TestAVerySmallScreenDoesNotPanic(t *testing.T) { a, screen := newTestApp(t) a.NewFile() screen.SetSize(4, 2) a.handle(tcell.NewEventResize(4, 2)) render(t, a, screen) } func TestClickingTheMenuBarOpensAMenu(t *testing.T) { a, screen := newTestApp(t) render(t, a, screen) click(a, 2, 0) if !a.MenuBar().Open() { t.Error("clicking the menu bar did not open a menu") } } func TestClickingAStatusHintRunsIt(t *testing.T) { a, screen := newTestApp(t) render(t, a, screen) click(a, 2, 23) // "F1 Describe" if a.Modals() != 1 { t.Errorf("Modals() = %d, want the keyboard help that F1 opens with no file", a.Modals()) } } func TestWindowTitleForABufferWithNoPath(t *testing.T) { if got := windowTitle(buffer.New()); got != untitledName { t.Errorf("windowTitle() = %q, want %q", got, untitledName) } } func TestSaveAsChangesTheWindowTitleAndTurnsColouringOn(t *testing.T) { a, _ := newTestApp(t) a.NewFile() typeText(a, "package main") path := filepath.Join(t.TempDir(), "renamed.go") a.save(a.activeView(), path) if got := a.Desktop().Active().Title(); got != "renamed.go" { t.Errorf("the window is called %q", got) } if got := readTestFile(t, path); got != "package main" { t.Errorf("the file holds %q", got) } } func TestTheAboutBoxNamesTheEditorAndTheTheme(t *testing.T) { a, _ := newTestApp(t) a.ShowAbout() if a.Modals() != 1 { t.Fatalf("Modals() = %d, want the About box", a.Modals()) } } func TestTheAboutTextCarriesEveryFactTheBuildRecorded(t *testing.T) { got := aboutText(testProfile().Name, testProfile().Language, version.Info{ Number: "0.2.0", Commit: "88a4c38", Built: "2026-08-31T18:04:05Z", }, "turbo-dark") for _, want := range []string{ testProfile().Name + " 0.2.0", "Commit: 88a4c38", "Built: 2026-08-31 18:04 UTC", "Theme: turbo-dark", } { if !strings.Contains(got, want) { t.Errorf("the About box never says %q:\n%s", want, got) } } } func TestTheAboutTextNamesTheLanguageTheEditorIsFor(t *testing.T) { // This box is drawn by the library, and the library is not for Go. It said // "an editor for Go" in every editor built on it, so Turbo Rust's About box // named the wrong language. got := aboutText("Turbo Rust", "Rust", version.Info{Number: "0.2.0"}, "turbo-dark") if !strings.Contains(got, "A Turbo C-style editor for Rust,") { t.Errorf("the About box does not say what the editor is for:\n%s", got) } if strings.Contains(got, "editor for Go") { t.Errorf("the About box names Go in an editor for Rust:\n%s", got) } // It is written in Go whatever it edits, and that line stays. if !strings.Contains(got, "written in Go.") { t.Errorf("the About box lost the language it is implemented in:\n%s", got) } } func TestTheAboutTextLeavesOutWhatTheBuildDidNotRecord(t *testing.T) { // A binary from `go install …@v0.2.0` knows its version and nothing else. // A blank "Commit:" line would say only that the editor failed to fill it. got := aboutText(testProfile().Name, testProfile().Language, version.Info{Number: "0.2.0"}, "turbo-classic") for _, unwanted := range []string{"Commit", "Built"} { if strings.Contains(got, unwanted) { t.Errorf("the About box mentions %q with nothing to put after it:\n%s", unwanted, got) } } if !strings.Contains(got, "Theme: turbo-classic") { t.Errorf("the About box lost its theme line:\n%s", got) } } func TestTheAboutTextNeverShowsAHardCodedVersion(t *testing.T) { // The whole point of internal/version: the number comes from the build, so // a release cannot ship an About box still naming the previous one. got := aboutText(testProfile().Name, testProfile().Language, version.Current(), "turbo-dark") if strings.Contains(got, testProfile().Name+" 0.1.0\n") { t.Errorf("the About box named a version no build reported:\n%s", got) } } func TestWindowLayoutCommands(t *testing.T) { a, _ := newTestApp(t) a.NewFile() a.NewFile() a.NewFile() a.TileWindows() windows := a.Desktop().Windows() for i, first := range windows { for _, second := range windows[i+1:] { if !first.Bounds().Intersect(second.Bounds()).IsEmpty() { t.Error("tiled windows overlap") } } } a.CascadeWindows() a.MaximizeWindow() if got := a.Desktop().Active().Bounds(); got != a.desktopRect() { t.Errorf("the maximised window is %+v, want the whole desktop", got) } } func TestViewAndWindowStayPaired(t *testing.T) { a, _ := newTestApp(t) a.NewFile() view := a.activeView() if got := a.windowOf(view); got != a.Desktop().Active() { t.Error("windowOf did not find the view's own window") } if a.windowOf(editor.NewView(buffer.New(), &editor.Clipboard{})) != nil { t.Error("windowOf found a window for a view that is in none") } } func writeTestFile(t *testing.T, path, content string) { t.Helper() if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatalf("writing %s: %v", path, err) } } func readTestFile(t *testing.T, path string) string { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("reading %s: %v", path, err) } return string(data) } func TestOnlyTheActiveWindowShowsACursor(t *testing.T) { // Two painted cursor blocks would mean neither of them means anything. a, _ := newTestApp(t) a.NewFile() behind := a.activeView() a.NewFile() front := a.activeView() if behind == front { t.Fatal("the two windows share a view") } if behind.Focused() { t.Error("the window behind still holds the focus") } if !front.Focused() { t.Error("the front window does not hold the focus") } a.NextWindow() if !behind.Focused() || front.Focused() { t.Error("the focus did not follow the window that came forward") } } func TestResizingTheTerminalResizesTheWindow(t *testing.T) { a, screen := newTestApp(t) a.NewFile() render(t, a, screen) before := a.Desktop().Active().Bounds() marginRight := 80 - before.Right() marginBottom := 24 - before.Bottom() screen.SetSize(120, 40) a.handle(tcell.NewEventResize(120, 40)) render(t, a, screen) after := a.Desktop().Active().Bounds() if after.W <= before.W || after.H <= before.H { t.Fatalf("the window is %+v, want it grown from %+v", after, before) } // It kept the same distance from the terminal's far edges, which is what // "the window still fills the terminal" means. if got := 120 - after.Right(); got != marginRight { t.Errorf("the window ends %d cells from the right edge, want %d", got, marginRight) } if got := 40 - after.Bottom(); got != marginBottom { t.Errorf("the window ends %d rows from the bottom edge, want %d", got, marginBottom) } } func TestResizingTheTerminalSmallerKeepsTheWindowInside(t *testing.T) { a, screen := newTestApp(t) a.NewFile() render(t, a, screen) screen.SetSize(40, 12) a.handle(tcell.NewEventResize(40, 12)) render(t, a, screen) got := a.Desktop().Active().Bounds() if got.Right() > 40 || got.Bottom() > 11 { t.Errorf("the window is %+v, want it inside a 40x12 terminal", got) } } func TestTheEditorFollowsItsWindowWhenTheTerminalIsResized(t *testing.T) { a, screen := newTestApp(t) a.NewFile() activeBuffer(t, a).SetText(strings.Repeat("line\n", 60)) render(t, a, screen) before := a.activeView().VisibleLines() screen.SetSize(120, 40) a.handle(tcell.NewEventResize(120, 40)) render(t, a, screen) if after := a.activeView().VisibleLines(); after <= before { t.Errorf("the editor shows %d lines, want more than the %d it showed before", after, before) } } func TestResizingRecentresAnOpenDialog(t *testing.T) { a, screen := newTestApp(t) press(a, tcell.KeyF3, 0, tcell.ModNone) if a.Modals() != 1 { t.Fatal("the Open dialog did not appear") } screen.SetSize(120, 40) a.handle(tcell.NewEventResize(120, 40)) dialog := a.TopModal() want := dialog.Bounds().CenteredIn(a.screenRect()) if got := dialog.Bounds(); got.X != want.X || got.Y != want.Y { t.Errorf("the dialog is at %+v, want it recentred at %+v", got, want) } } func TestResizingDismissesTheCompletionPopup(t *testing.T) { // It is anchored to a cursor that has just moved, so the only honest thing // to do with it is put it away. a, screen := newTestApp(t) a.NewFile() a.Completion().Show(sampleItems(), "", 10, 5, a.screenRect()) screen.SetSize(120, 40) a.handle(tcell.NewEventResize(120, 40)) if a.Completion().Visible() { t.Error("the popup survived a resize") } } func TestTheTerminalCursorTakesItsColourFromTheTheme(t *testing.T) { // A terminal draws its cursor *over* the cell, in whatever colour the user // configured for some other palette. Painting the cell underneath is not // enough; the theme has to name the cursor's own colour. for _, name := range []string{"turbo-classic", "turbo-dark", "borland-light"} { t.Run(name, func(t *testing.T) { th, err := theme.Load(name, "") if err != nil { t.Fatalf("Load() error = %v", err) } _, want, _ := th.Style(theme.KeyEditorCursor).Decompose() if got := cursorColor(th); got != want { t.Errorf("cursorColor() = %v, want the cursor style's background %v", got, want) } }) } } func TestChangingTheThemeChangesTheCursorColour(t *testing.T) { a, _ := newTestApp(t) before := cursorColor(a.Theme()) a.setTheme("turbo-dark") if cursorColor(a.Theme()) == before { t.Error("the cursor colour did not follow the theme") } }